feat: live camera preview overlay (visible before & during recording)#441
Conversation
Shows a floating, always-on-top circular webcam preview window as soon as the camera is enabled — visible before and during recording so the user can see their face overlay in real time, matching the final export. - Add `createCameraPreviewWindow` / `closeCameraPreviewWindow` in `electron/windows.ts` — transparent, frameless, always-on-top window - Wire `show-camera-preview` / `hide-camera-preview` IPC in `main.ts` - Expose both IPC calls via `preload.ts` and `vite-env.d.ts` - Register `camera-preview` window type in `App.tsx` - New `CameraPreviewWindow` component: live `getUserMedia` feed rendered in a mirrored circle; hover reveals an eye-off button to collapse it to a small pill badge without disabling the webcam - Trigger `showCameraPreview` from `LaunchWindow` when webcam is toggled on (not tied to recording state), and `hideCameraPreview` when off Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
📝 WalkthroughWalkthroughIntroduced IPC-driven camera preview window functionality across the electron and React layers. Added window creation/management utilities, exposed API methods through preload, implemented a frameless draggable preview component with media stream handling, and hooked up the preview toggle in LaunchWindow based on webcam enabled state. Changes
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 5
🧹 Nitpick comments (1)
src/components/launch/CameraPreviewWindow.tsx (1)
57-58: New UI strings are hardcoded (not localized).
"Show","Show camera preview", and"Hide camera preview"should go through i18n like the rest of launch UI. nit: cleaner for multilingual builds.Also applies to: 80-80, 135-136
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/components/launch/CameraPreviewWindow.tsx` around lines 57 - 58, The UI strings in CameraPreviewWindow are hardcoded; update the component to use the app i18n API (e.g., useTranslation or t) instead of literal "Show", "Show camera preview", and "Hide camera preview" so they are localized. Import and call the translation helper at the top of CameraPreviewWindow, add translation keys (e.g., launch.cameraPreview.show, launch.cameraPreview.showTitle, launch.cameraPreview.hideTitle) to replace the button label and title props wherever "Show"/"Show camera preview"/"Hide camera preview" are used, and ensure the translated values are passed to the title and visible text in the component.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@electron/main.ts`:
- Around line 371-377: The IPC handlers for "show-camera-preview" and
"hide-camera-preview" must validate the sender before toggling the camera
preview: in the ipcMain.on callbacks for "show-camera-preview" and
"hide-camera-preview" check event.senderFrame?.url (the same HUD overlay check
used elsewhere) and only call createCameraPreviewWindow(deviceId ?? "") or
closeCameraPreviewWindow() when the URL matches the HUD overlay window;
otherwise return early and do not perform any action. Ensure you reference the
existing handlers for "show-camera-preview"/"hide-camera-preview" and reuse the
same URL matching logic used for other HUD-gated IPCs to keep consistent
validation.
In `@electron/windows.ts`:
- Around line 100-101: The current y computation (using workArea, windowSize,
margin and a hardcoded -170) can push the preview off-screen on short displays;
change the logic around the y variable so you compute the desired Y (workArea.y
+ workArea.height - windowSize - margin - 170) and then clamp it within safe
bounds (e.g., at least workArea.y + margin and at most workArea.y +
workArea.height - windowSize - margin) so the preview never extends above or
below the visible workArea; update the y assignment in electron/windows.ts (the
y variable that uses workArea, windowSize, margin and the -170 offset) to use
this clamped value.
In `@src/components/launch/CameraPreviewWindow.tsx`:
- Around line 44-52: Collapsed outer container in CameraPreviewWindow lacks the
CSS WebkitAppRegion: "drag", so the window becomes non-draggable when collapsed;
update the collapsed outer div (the top-level <div> rendered in collapsed mode
inside the CameraPreviewWindow component) to include WebkitAppRegion: "drag" in
its style object, keeping the existing button style that uses WebkitAppRegion:
"no-drag" intact so the button remains clickable and behavior matches the
expanded state.
- Around line 27-39: The getUserMedia promise can resolve after the component's
cleanup and create tracks that are never stopped; add a disposed guard: declare
a local let disposed = false in the effect, set disposed = true inside the
cleanup, and in the getUserMedia .then handler check if disposed before
assigning stream or videoRef.current.srcObject — if disposed is true immediately
stop any obtained tracks instead of attaching them; keep the existing cleanup
that stops tracks from the outer-scoped stream variable to cover the normal
path.
In `@src/components/launch/LaunchWindow.tsx`:
- Around line 157-166: The effect that calls
window.electronAPI.showCameraPreview/hideCameraPreview only lists webcamEnabled
in its dependency array, so changes to webcamDeviceId or selectedCameraId won't
trigger an update; update the useEffect dependencies to include webcamDeviceId
and selectedCameraId (or the single chosen deviceId variable) so that when
webcamEnabled is true and the selected device changes the effect re-runs and
calls showCameraPreview with the new deviceId; keep the early return on missing
window.electronAPI and preserve calling hideCameraPreview when webcamEnabled is
false.
---
Nitpick comments:
In `@src/components/launch/CameraPreviewWindow.tsx`:
- Around line 57-58: The UI strings in CameraPreviewWindow are hardcoded; update
the component to use the app i18n API (e.g., useTranslation or t) instead of
literal "Show", "Show camera preview", and "Hide camera preview" so they are
localized. Import and call the translation helper at the top of
CameraPreviewWindow, add translation keys (e.g., launch.cameraPreview.show,
launch.cameraPreview.showTitle, launch.cameraPreview.hideTitle) to replace the
button label and title props wherever "Show"/"Show camera preview"/"Hide camera
preview" are used, and ensure the translated values are passed to the title and
visible text in the component.
🪄 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: 2d99762b-428f-4f2b-8357-f54744f7c169
📒 Files selected for processing (8)
electron/main.tselectron/preload.tselectron/windows.tssrc/App.tsxsrc/components/launch/CameraPreviewWindow.tsxsrc/components/launch/LaunchWindow.tsxsrc/hooks/useScreenRecorder.tssrc/vite-env.d.ts
| // Camera preview window — shown during recording so user can see their face | ||
| ipcMain.on("show-camera-preview", (_, deviceId: string) => { | ||
| createCameraPreviewWindow(deviceId ?? ""); | ||
| }); | ||
| ipcMain.on("hide-camera-preview", () => { | ||
| closeCameraPreviewWindow(); | ||
| }); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# Find and read the electron/main.ts file around the specified lines
cd /
find . -name "electron" -type d | head -5Repository: siddharthvaddem/openscreen
Length of output: 160
🏁 Script executed:
# List files in electron directory
fd . electron --type f --max-depth 1Repository: siddharthvaddem/openscreen
Length of output: 170
🏁 Script executed:
# Read the main.ts file around lines 371-377
cat -n electron/main.ts | sed -n '360,390p'Repository: siddharthvaddem/openscreen
Length of output: 1135
🏁 Script executed:
# Check the full context of IPC handler registration
rg "show-camera-preview|hide-camera-preview" -B 5 -A 10Repository: siddharthvaddem/openscreen
Length of output: 1883
🏁 Script executed:
# Search for createCameraPreviewWindow and closeCameraPreviewWindow implementation
rg "createCameraPreviewWindow|closeCameraPreviewWindow" -B 3 -A 8Repository: siddharthvaddem/openscreen
Length of output: 2689
🏁 Script executed:
# Check the createCameraPreviewWindow implementation details
cat -n electron/windows.ts | sed -n '1,150p'Repository: siddharthvaddem/openscreen
Length of output: 5193
🏁 Script executed:
# Look at createHudOverlayWindow and related window creation
rg "createHudOverlayWindow|createKalturaBrowseWindow" -B 2 -A 20Repository: siddharthvaddem/openscreen
Length of output: 3287
🏁 Script executed:
# Check preload configuration for each window type
rg "preload:" electron/windows.ts -B 5 -A 2Repository: siddharthvaddem/openscreen
Length of output: 920
🏁 Script executed:
# Check if there are any other references to showCameraPreview or hideCameraPreview
rg "showCameraPreview|hideCameraPreview" -B 3 -A 3Repository: siddharthvaddem/openscreen
Length of output: 1851
🏁 Script executed:
# Check which windows load preload and what their purposes are
rg "loadURL|loadFile" electron/windows.ts | head -20Repository: siddharthvaddem/openscreen
Length of output: 583
🏁 Script executed:
# Verify preload.mjs exports and what electronAPI contains
cat -n electron/preload.ts | head -100Repository: siddharthvaddem/openscreen
Length of output: 4381
🏁 Script executed:
# Check for createKalturaBrowseWindow to verify if it has preload attached
rg "createKalturaBrowseWindow" -B 5 -A 30Repository: siddharthvaddem/openscreen
Length of output: 52
🏁 Script executed:
# Also check the KalturaBrowseDialog mentioned in learnings
rg "KalturaBrowseDialog" --type tsx -B 3 -A 15Repository: siddharthvaddem/openscreen
Length of output: 97
🏁 Script executed:
# Search for Kaltura-related files
fd Kaltura . --type fRepository: siddharthvaddem/openscreen
Length of output: 52
🏁 Script executed:
# Try searching for KalturaBrowseDialog without type specification
rg "KalturaBrowseDialog" -B 3 -A 15Repository: siddharthvaddem/openscreen
Length of output: 52
🏁 Script executed:
# Check if there are any window creation functions that load remote URLs
rg "loadURL.*https" electron/Repository: siddharthvaddem/openscreen
Length of output: 52
🏁 Script executed:
# List all TypeScript files to understand the structure
fd . src --type f --extension ts --extension tsx | head -20Repository: siddharthvaddem/openscreen
Length of output: 700
🏁 Script executed:
# Check what windows can call these IPC handlers - verify all windows have preload
rg "windowType=" electron/windows.tsRepository: siddharthvaddem/openscreen
Length of output: 355
🏁 Script executed:
# Check how LaunchWindow is loaded and in which window
rg "LaunchWindow" src/ -B 3 -A 3Repository: siddharthvaddem/openscreen
Length of output: 1892
🏁 Script executed:
# Look at all IPC listeners registered and check which ones lack sender validation
rg "ipcMain.on\(|ipcMain.handle\(" electron/main.ts -A 2 | head -50Repository: siddharthvaddem/openscreen
Length of output: 542
IPC handlers should validate sender before toggling camera preview window.
These listeners currently accept calls from any renderer with access to window.electronAPI. Since all windows share the same preload bridge, an attacker who compromises any window could spam open/close on the camera preview—kinda cursed to have that persist across the app without validation.
The hardening is straightforward: check event.senderFrame?.url to gate these to the HUD overlay window only, same pattern already established elsewhere.
Hardening patch (recommended)
import {
app,
BrowserWindow,
dialog,
+ IpcMainEvent,
ipcMain,
@@
+function isHudOverlaySender(event: IpcMainEvent): boolean {
+ const url = event.senderFrame?.url ?? "";
+ return url.includes("windowType=hud-overlay");
+}
+
// Camera preview window — shown during recording so user can see their face
- ipcMain.on("show-camera-preview", (_, deviceId: string) => {
+ ipcMain.on("show-camera-preview", (event, deviceId: string) => {
+ if (!isHudOverlaySender(event)) return;
createCameraPreviewWindow(deviceId ?? "");
});
- ipcMain.on("hide-camera-preview", () => {
+ ipcMain.on("hide-camera-preview", (event) => {
+ if (!isHudOverlaySender(event)) return;
closeCameraPreviewWindow();
});📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| // Camera preview window — shown during recording so user can see their face | |
| ipcMain.on("show-camera-preview", (_, deviceId: string) => { | |
| createCameraPreviewWindow(deviceId ?? ""); | |
| }); | |
| ipcMain.on("hide-camera-preview", () => { | |
| closeCameraPreviewWindow(); | |
| }); | |
| function isHudOverlaySender(event: IpcMainEvent): boolean { | |
| const url = event.senderFrame?.url ?? ""; | |
| return url.includes("windowType=hud-overlay"); | |
| } | |
| // Camera preview window — shown during recording so user can see their face | |
| ipcMain.on("show-camera-preview", (event, deviceId: string) => { | |
| if (!isHudOverlaySender(event)) return; | |
| createCameraPreviewWindow(deviceId ?? ""); | |
| }); | |
| ipcMain.on("hide-camera-preview", (event) => { | |
| if (!isHudOverlaySender(event)) return; | |
| closeCameraPreviewWindow(); | |
| }); |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@electron/main.ts` around lines 371 - 377, The IPC handlers for
"show-camera-preview" and "hide-camera-preview" must validate the sender before
toggling the camera preview: in the ipcMain.on callbacks for
"show-camera-preview" and "hide-camera-preview" check event.senderFrame?.url
(the same HUD overlay check used elsewhere) and only call
createCameraPreviewWindow(deviceId ?? "") or closeCameraPreviewWindow() when the
URL matches the HUD overlay window; otherwise return early and do not perform
any action. Ensure you reference the existing handlers for
"show-camera-preview"/"hide-camera-preview" and reuse the same URL matching
logic used for other HUD-gated IPCs to keep consistent validation.
| const x = Math.floor(workArea.x + workArea.width - windowSize - margin); | ||
| const y = Math.floor(workArea.y + workArea.height - windowSize - margin - 170); // above HUD |
There was a problem hiding this comment.
Window position math can place preview off-screen on small displays.
Line 101 uses a fixed -170 offset with no clamp, so the preview can become partially inaccessible on low-height work areas.
nit: cleaner bounds-safe positioning
-const x = Math.floor(workArea.x + workArea.width - windowSize - margin);
-const y = Math.floor(workArea.y + workArea.height - windowSize - margin - 170); // above HUD
+const desiredX = Math.floor(workArea.x + workArea.width - windowSize - margin);
+const desiredY = Math.floor(workArea.y + workArea.height - windowSize - margin - 170); // above HUD
+const x = Math.max(workArea.x, Math.min(desiredX, workArea.x + workArea.width - windowSize));
+const y = Math.max(workArea.y, Math.min(desiredY, workArea.y + workArea.height - windowSize));📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const x = Math.floor(workArea.x + workArea.width - windowSize - margin); | |
| const y = Math.floor(workArea.y + workArea.height - windowSize - margin - 170); // above HUD | |
| const desiredX = Math.floor(workArea.x + workArea.width - windowSize - margin); | |
| const desiredY = Math.floor(workArea.y + workArea.height - windowSize - margin - 170); // above HUD | |
| const x = Math.max(workArea.x, Math.min(desiredX, workArea.x + workArea.width - windowSize)); | |
| const y = Math.max(workArea.y, Math.min(desiredY, workArea.y + workArea.height - windowSize)); |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@electron/windows.ts` around lines 100 - 101, The current y computation (using
workArea, windowSize, margin and a hardcoded -170) can push the preview
off-screen on short displays; change the logic around the y variable so you
compute the desired Y (workArea.y + workArea.height - windowSize - margin - 170)
and then clamp it within safe bounds (e.g., at least workArea.y + margin and at
most workArea.y + workArea.height - windowSize - margin) so the preview never
extends above or below the visible workArea; update the y assignment in
electron/windows.ts (the y variable that uses workArea, windowSize, margin and
the -170 offset) to use this clamped value.
| navigator.mediaDevices | ||
| .getUserMedia(constraints) | ||
| .then((s) => { | ||
| stream = s; | ||
| if (videoRef.current) { | ||
| videoRef.current.srcObject = s; | ||
| } | ||
| }) | ||
| .catch(console.error); | ||
|
|
||
| return () => { | ||
| stream?.getTracks().forEach((t) => t.stop()); | ||
| }; |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
cat -n src/components/launch/CameraPreviewWindow.tsx | head -60Repository: siddharthvaddem/openscreen
Length of output: 2097
getUserMedia cleanup has an async unmount race — camera won't release.
If the component unmounts before the promise resolves, the .then() handler runs after cleanup and creates tracks that never get stopped. lowkey cursed for resource lifecycle.
Fix with disposed guard
let stream: MediaStream | null = null;
+ let disposed = false;
navigator.mediaDevices
.getUserMedia(constraints)
.then((s) => {
+ if (disposed) {
+ s.getTracks().forEach((t) => t.stop());
+ return;
+ }
stream = s;
if (videoRef.current) {
videoRef.current.srcObject = s;
}
})
.catch(console.error);
return () => {
+ disposed = true;
stream?.getTracks().forEach((t) => t.stop());
};📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| navigator.mediaDevices | |
| .getUserMedia(constraints) | |
| .then((s) => { | |
| stream = s; | |
| if (videoRef.current) { | |
| videoRef.current.srcObject = s; | |
| } | |
| }) | |
| .catch(console.error); | |
| return () => { | |
| stream?.getTracks().forEach((t) => t.stop()); | |
| }; | |
| let stream: MediaStream | null = null; | |
| let disposed = false; | |
| navigator.mediaDevices | |
| .getUserMedia(constraints) | |
| .then((s) => { | |
| if (disposed) { | |
| s.getTracks().forEach((t) => t.stop()); | |
| return; | |
| } | |
| stream = s; | |
| if (videoRef.current) { | |
| videoRef.current.srcObject = s; | |
| } | |
| }) | |
| .catch(console.error); | |
| return () => { | |
| disposed = true; | |
| stream?.getTracks().forEach((t) => t.stop()); | |
| }; |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/components/launch/CameraPreviewWindow.tsx` around lines 27 - 39, The
getUserMedia promise can resolve after the component's cleanup and create tracks
that are never stopped; add a disposed guard: declare a local let disposed =
false in the effect, set disposed = true inside the cleanup, and in the
getUserMedia .then handler check if disposed before assigning stream or
videoRef.current.srcObject — if disposed is true immediately stop any obtained
tracks instead of attaching them; keep the existing cleanup that stops tracks
from the outer-scoped stream variable to cover the normal path.
| <div | ||
| style={{ | ||
| width: "100vw", | ||
| height: "100vh", | ||
| display: "flex", | ||
| alignItems: "center", | ||
| justifyContent: "center", | ||
| background: "transparent", | ||
| }} |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
fd "CameraPreviewWindow.tsx" --type fRepository: siddharthvaddem/openscreen
Length of output: 115
🏁 Script executed:
cat -n src/components/launch/CameraPreviewWindow.tsx | head -120Repository: siddharthvaddem/openscreen
Length of output: 4139
Collapsed state loses drag behavior — add WebkitAppRegion: "drag" to outer container.
Expanded mode sets WebkitAppRegion: "drag" on the outer div (line 96), but collapsed mode's outer container (lines 44-52) is missing this property. The button correctly has WebkitAppRegion: "no-drag" (line 72) to stay clickable, but the surrounding area won't be draggable after collapse. Users won't be able to reposition the window until expanding it again — kinda cursed UX. Add WebkitAppRegion: "drag" to the collapsed outer div to match the expanded state.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/components/launch/CameraPreviewWindow.tsx` around lines 44 - 52,
Collapsed outer container in CameraPreviewWindow lacks the CSS WebkitAppRegion:
"drag", so the window becomes non-draggable when collapsed; update the collapsed
outer div (the top-level <div> rendered in collapsed mode inside the
CameraPreviewWindow component) to include WebkitAppRegion: "drag" in its style
object, keeping the existing button style that uses WebkitAppRegion: "no-drag"
intact so the button remains clickable and behavior matches the expanded state.
| useEffect(() => { | ||
| if (!window.electronAPI) return; | ||
| if (webcamEnabled) { | ||
| const deviceId = webcamDeviceId || selectedCameraId || ""; | ||
| window.electronAPI.showCameraPreview(deviceId); | ||
| } else { | ||
| window.electronAPI.hideCameraPreview(); | ||
| } | ||
| // eslint-disable-next-line react-hooks/exhaustive-deps | ||
| }, [webcamEnabled]); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
fd "LaunchWindow.tsx" --type fRepository: siddharthvaddem/openscreen
Length of output: 108
🏁 Script executed:
if [ -f "src/components/launch/LaunchWindow.tsx" ]; then
wc -l "src/components/launch/LaunchWindow.tsx"
fiRepository: siddharthvaddem/openscreen
Length of output: 112
🏁 Script executed:
if [ -f "src/components/launch/LaunchWindow.tsx" ]; then
sed -n '150,175p' "src/components/launch/LaunchWindow.tsx"
fiRepository: siddharthvaddem/openscreen
Length of output: 885
Camera preview stays stale when you switch devices while recording (missing deps at line 165).
Rn the eslint-disable-next-line silences a legit warning — you're using webcamDeviceId and selectedCameraId but only have [webcamEnabled] in the dependency array. so if someone picks a different camera while the preview is live, they won't see it update until they toggle the webcam off/on. lowkey breaks the device-selection flow.
Proposed fix
-import { useEffect, useState } from "react";
+import { useEffect, useRef, useState } from "react";
@@
+const lastPreviewDeviceIdRef = useRef<string | null>(null);
+
useEffect(() => {
if (!window.electronAPI) return;
- if (webcamEnabled) {
- const deviceId = webcamDeviceId || selectedCameraId || "";
- window.electronAPI.showCameraPreview(deviceId);
- } else {
+ if (!webcamEnabled) {
window.electronAPI.hideCameraPreview();
+ lastPreviewDeviceIdRef.current = null;
+ return;
}
- // eslint-disable-next-line react-hooks/exhaustive-deps
-}, [webcamEnabled]);
+ const deviceId = webcamDeviceId || selectedCameraId || "";
+ if (lastPreviewDeviceIdRef.current === deviceId) return;
+ window.electronAPI.showCameraPreview(deviceId);
+ lastPreviewDeviceIdRef.current = deviceId;
+}, [webcamEnabled, webcamDeviceId, selectedCameraId]);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/components/launch/LaunchWindow.tsx` around lines 157 - 166, The effect
that calls window.electronAPI.showCameraPreview/hideCameraPreview only lists
webcamEnabled in its dependency array, so changes to webcamDeviceId or
selectedCameraId won't trigger an update; update the useEffect dependencies to
include webcamDeviceId and selectedCameraId (or the single chosen deviceId
variable) so that when webcamEnabled is true and the selected device changes the
effect re-runs and calls showCameraPreview with the new deviceId; keep the early
return on missing window.electronAPI and preserve calling hideCameraPreview when
webcamEnabled is false.
|
@antonioromero1220 whats the behvaiour if i enable webcam and am recoridng the whole screen? |
Summary
Changes
electron/windows.tscreateCameraPreviewWindow()+closeCameraPreviewWindow()— transparent, frameless, 220×220 always-on-top windowelectron/main.tsshow-camera-preview/hide-camera-previewIPC handlerselectron/preload.tssrc/vite-env.d.tssrc/App.tsxcamera-previewwindow type with transparent backgroundsrc/components/launch/CameraPreviewWindow.tsxgetUserMediafeed in a mirrored draggable circle with hover hide/show togglesrc/hooks/useScreenRecorder.tssrc/components/launch/LaunchWindow.tsxuseEffecttriggersshowCameraPreviewwhen webcam enabled,hideCameraPreviewwhen disabledBehavior
ShowpillTest plan
🤖 Generated with Claude Code
Summary by CodeRabbit
New Features