Skip to content

feat: add live screen recording preview#319

Open
hamidlabs wants to merge 8 commits intosiddharthvaddem:mainfrom
hamidlabs:feature/live-preview
Open

feat: add live screen recording preview#319
hamidlabs wants to merge 8 commits intosiddharthvaddem:mainfrom
hamidlabs:feature/live-preview

Conversation

@hamidlabs
Copy link
Copy Markdown

@hamidlabs hamidlabs commented Apr 4, 2026

Summary

Adds a real-time live preview to the launch window, similar to OBS Studio's preview panel. Users can now see exactly what will be recorded before and during recording.

  • Live preview window: Replaces the 500x155 HUD pill bar with a 480x420 resizable preview window showing a real-time screen capture feed
  • Canvas compositor: Renders screen capture + circular webcam PiP overlay at 30fps using a throttled requestAnimationFrame loop, capped at 960px internal resolution for GPU efficiency
  • Stream handoff: Preview stream is seamlessly handed off to MediaRecorder when recording starts — no double getUserMedia calls, no Wayland PipeWire re-prompts
  • Linux PipeWire/Wayland support: Enables WebRTCPipeWireCapturer and ozone-platform-hint flags, adds session type detection IPC for Wayland-aware behavior

Changes

File Change
electron/main.ts PipeWire/Wayland flags for Linux
electron/windows.ts Resized HUD overlay to 480x420, resizable, bottom-right
electron/ipc/handlers.ts get-session-type IPC handler
electron/preload.ts Exposed getSessionType to renderer
electron/electron-env.d.ts Type declaration for new IPC
src/vite-env.d.ts Type declaration for new IPC
src/hooks/usePreviewStream.ts New — preview stream lifecycle management
src/hooks/useScreenRecorder.ts Accept PreviewStreamHandoff to reuse preview streams
src/components/launch/LivePreview.tsx New — canvas-based preview with webcam PiP
src/components/launch/LaunchWindow.tsx Redesigned with integrated live preview

Test plan

  • Select a screen source → live preview should start automatically
  • Toggle webcam → circular PiP overlay appears/disappears in preview
  • Click REC → recording starts using the preview stream (no second permission prompt)
  • Stop recording → preview restarts after brief delay
  • Resize the preview window (380-640px width range)
  • Test on Wayland session (PipeWire screen capture)
  • Test on X11 session (desktopCapturer source enumeration)

Summary by CodeRabbit

  • New Features

    • Live canvas-based screen preview with optional webcam picture-in-picture
    • Automatic session-type detection to improve display capture on Linux
    • Linux startup tuned to better support PipeWire-based screen capture
  • Refactor

    • HUD redesigned with top title bar, dedicated preview region, repositioned controls, resizable bounds, and taskbar visibility
    • Recording flow refactored to reuse preview streams and streamline preview/record toggles

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 4, 2026

📝 Walkthrough

Walkthrough

Adds Linux session-type detection and IPC/preload bridging; enables PipeWire capture on Linux; introduces a canvas-based live preview system with preview lifecycle hook; updates recording hook to accept preview stream handoff; refactors LaunchWindow to integrate preview, and adjusts HUD overlay window sizing/positioning.

Changes

Cohort / File(s) Summary
Session Type Detection & Preload API
electron/electron-env.d.ts, electron/preload.ts, electron/ipc/handlers.ts, src/vite-env.d.ts
Added getSessionType IPC handler and preload bridge to expose window.electronAPI.getSessionType(): Promise<string>; handler returns platform session type (Linux: XDG_SESSION_TYPE / WAYLAND_DISPLAY inference; non-Linux: "x11").
Linux PipeWire / Startup Flags
electron/main.ts
On Linux, adds command-line flags to enable WebRTCPipeWireCapturer and set --ozone-platform-hint=auto.
HUD Overlay Window Layout
electron/windows.ts
Changed HUD overlay from fixed 500×155 to resizable 480×420 with min/max bounds, moved anchoring to bottom-right, enabled resizing and taskbar visibility while keeping frameless/transparent/always-on-top settings.
Preview Lifecycle Hook
src/hooks/usePreviewStream.ts
New hook managing preview lifecycle: start/stop preview, optional webcam handling, detach helpers, and cleanup; exposes streams, previewActive, sourceId, and control methods.
Live Preview Component
src/components/launch/LivePreview.tsx
New component rendering preview to a canvas (~30 FPS), with hidden video elements for decoding and a circular webcam PiP overlay; handles autoplay, resizing, draw loop and cleanup.
Recording Handoff Integration
src/hooks/useScreenRecorder.ts
Recording hook updated to accept optional PreviewStreamHandoff; can reuse preview streams (screen/webcam), adjust audio acquisition and skip final applyConstraints when reusing streams.
Launch Window Refactor & UI
src/components/launch/LaunchWindow.tsx
Large refactor: removed Mac-specific positioning, integrated preview hook and LivePreview, added polling to auto-start preview on selected source, changed recording toggle to detach/pass preview streams to recorder, reorganized UI layout and callbacks.
Type Declarations
src/vite-env.d.ts, electron/electron-env.d.ts
Added getSessionType: () => Promise<string> to Window.electronAPI declarations.

Sequence Diagram

sequenceDiagram
    participant Renderer as React Renderer
    participant LaunchWindow as LaunchWindow Component
    participant UsePreview as usePreviewStream Hook
    participant MediaAPI as Browser Media APIs
    participant LivePreview as LivePreview Component
    participant UseRecorder as useScreenRecorder Hook
    Note over Renderer,UseRecorder: Preview → Detach → Handoff → Record flow

    Renderer->>LaunchWindow: render (webcamEnabled)
    LaunchWindow->>UsePreview: initialize hook
    LaunchWindow->>UsePreview: startPreview(desktopSourceId)
    UsePreview->>MediaAPI: getUserMedia(desktop capture)
    MediaAPI-->>UsePreview: screenStream
    alt webcamEnabled
        UsePreview->>MediaAPI: getUserMedia(webcam)
        MediaAPI-->>UsePreview: webcamStream
    end
    UsePreview-->>LaunchWindow: streams, previewActive=true
    LaunchWindow->>LivePreview: render streams
    LivePreview->>MediaAPI: create video elements + RAF loop
    LivePreview->>LivePreview: draw to canvas (~30 FPS)
    Renderer->>LaunchWindow: user clicks Record
    LaunchWindow->>UsePreview: detachScreenStream & detachWebcamStream
    UsePreview-->>LaunchWindow: returned streams (refs cleared)
    LaunchWindow->>UseRecorder: toggleRecording({screen, webcam})
    UseRecorder->>UseRecorder: startRecording(handoff)
    UseRecorder->>MediaAPI: applyConstraints (best-effort) / get audio if needed
    UseRecorder-->>Renderer: recording active
    Renderer->>LaunchWindow: user stops
    UseRecorder->>UseRecorder: stopRecording
    LaunchWindow->>UsePreview: startPreview (restart after ~500ms)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Suggested reviewers

  • siddharthvaddem

Poem

🐰
I hop and I whisk as the canvas draws night,
Preview to recording, a feather-light flight,
Wayland and X now politely replied,
The HUD tucks in corner with streams by my side,
Hooray — the rabbit hands off the capture with pride!

🚥 Pre-merge checks | ✅ 2 | ❌ 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 (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat: add live screen recording preview' clearly and concisely describes the main feature addition — a live preview for screen recording. It matches the primary objective and is specific enough for history scanning.
Description check ✅ Passed The description covers purpose, motivation, changes table, and a comprehensive test plan. While some template sections (screenshots/video, type of change checkboxes, self-review checklist) are not formally filled, the core informational content is complete and well-structured.

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

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

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.

❤️ Share

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

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: c69debc1fe

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Copy link
Copy Markdown

@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: 5

🧹 Nitpick comments (2)
electron/ipc/handlers.ts (1)

221-224: Consider normalizing session type to known values.

The handler returns process.env.XDG_SESSION_TYPE directly, which could contain unexpected values beyond "wayland" or "x11" (e.g., "tty", "mir", or custom values). If the consumer code expects only "wayland" or "x11", consider normalizing:

♻️ Optional: Normalize to known session types
 ipcMain.handle("get-session-type", () => {
   if (process.platform !== "linux") return "x11";
-  return process.env.XDG_SESSION_TYPE || (process.env.WAYLAND_DISPLAY ? "wayland" : "x11");
+  const sessionType = process.env.XDG_SESSION_TYPE?.toLowerCase();
+  if (sessionType === "wayland") return "wayland";
+  if (sessionType === "x11") return "x11";
+  // Fallback detection
+  return process.env.WAYLAND_DISPLAY ? "wayland" : "x11";
 });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@electron/ipc/handlers.ts` around lines 221 - 224, The get-session-type IPC
handler returns process.env.XDG_SESSION_TYPE directly which may be unexpected
values; update the ipcMain.handle("get-session-type", ...) implementation to
normalize the session type to only "wayland" or "x11": read XDG_SESSION_TYPE
(and fall back to checking WAYLAND_DISPLAY as before), lowercase it, then return
"wayland" if it equals "wayland" and return "x11" for any other value. Ensure
you update the logic inside the ipcMain.handle callback so consumers always
receive one of the two known values.
src/components/launch/LaunchWindow.tsx (1)

145-172: prevSourceId resets when effect dependencies change, potentially causing redundant preview starts.

The prevSourceId variable is declared inside the effect, so it resets to null whenever the effect re-runs due to dependency changes (recording or startPreview). This means when recording stops, the effect restarts, prevSourceId becomes null, and startPreview may be called even for the same source.

♻️ Use useRef to persist prevSourceId across effect runs
+const prevSourceIdRef = useRef<string | null>(null);

 // Poll for source selection and start preview when source is picked
 useEffect(() => {
-  let prevSourceId: string | null = null;

   const checkSelectedSource = async () => {
     if (window.electronAPI) {
       const source = await window.electronAPI.getSelectedSource();
       if (source) {
         setSelectedSource(source.name);
         setHasSelectedSource(true);

         // Auto-start preview when source changes
-        if (source.id !== prevSourceId && !recording) {
-          prevSourceId = source.id;
+        if (source.id !== prevSourceIdRef.current && !recording) {
+          prevSourceIdRef.current = source.id;
           startPreview(source.id);
         }
       } else {
         setSelectedSource("Screen");
         setHasSelectedSource(false);
       }
     }
   };
🤖 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 145 - 172, prevSourceId
is declared inside the useEffect so it resets whenever the effect re-runs
causing duplicate startPreview calls; move that state to a ref (e.g.,
prevSourceIdRef = useRef<string | null>(null)) at component scope and replace
uses of prevSourceId in the effect with prevSourceIdRef.current, updating
prevSourceIdRef.current = source.id after calling startPreview(source.id) and
when selecting a source so the previous source id persists across effect runs;
keep the rest of the checkSelectedSource logic and the interval handling the
same.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/components/launch/LaunchWindow.tsx`:
- Around line 180-213: The setTimeout inside handleToggleRecording can fire
after unmount; add a ref (e.g., restartPreviewTimeoutRef) to store the timeout
id when scheduling the 500ms restart and use that ref to clearTimeout if needed,
update the callback to assign the timeout to restartPreviewTimeoutRef.current
instead of an anonymous timeout, and add a cleanup useEffect that clears
restartPreviewTimeoutRef.current on unmount to prevent calling startPreview
after the component has been unmounted.

In `@src/components/launch/LivePreview.tsx`:
- Around line 151-163: The effect in LivePreview that creates a video element
when streams.webcam becomes available can leak resources; update the useEffect
(the one that checks webcamVideoRef.current and streams?.webcam) to return a
cleanup function that tears down the created element: if webcamVideoRef.current
exists and its srcObject equals the stream, pause the video, remove it from the
DOM, set webcamVideoRef.current.srcObject = null, stop any tracks on the
MediaStream, and clear webcamVideoRef.current; this ensures toggling the webcam
or unmounting releases streams and DOM nodes even when the parent streams object
identity does not change.
- Around line 105-121: The crop math uses webcamVideo.videoWidth/height and can
divide by zero before metadata loads; inside the drawing routine in
LivePreview.tsx where ww, wh, aspectRatio, sx, sy, sw, sh are computed, guard by
checking webcamVideo.videoWidth and webcamVideo.videoHeight > 0 and bail out or
skip cropping/drawing until they are valid (or set safe defaults like 1) so
aspectRatio is never Infinity/NaN and drawImage gets valid sx/sy/sw/sh values.

In `@src/hooks/usePreviewStream.ts`:
- Around line 24-35: The preview startup/stop code must serialize and abort
stale webcam requests to avoid double prompts and leaked tracks: add a
latestRequestRef (useRef<symbol|null>) and, in startPreview and in the effect
that also calls getUserMedia, create a new unique symbol and assign it to
latestRequestRef before awaiting navigator.mediaDevices.getUserMedia; after the
await verify the symbol still matches latestRequestRef.current and only then
assign webcamStreamRef.current and setStreams; if it does not match, stop the
newly obtained tracks immediately; update stopPreview to clear
latestRequestRef.current (set to null) so late resolves know the request was
cancelled and always stop any stream returned by stale requests.

In `@src/hooks/useScreenRecorder.ts`:
- Around line 453-456: When reusing the preview handoff stream in
useScreenRecorder, the preview track must be upgraded to recording constraints
instead of using the preview resolution as-is; locate the branch that sets
webcamStream.current = previewHandoff.webcamStream and before assigning or
before starting the recorder call applyConstraints on the preview video track
(the track from previewHandoff.webcamStream) with the same constraints used in
the fresh-capture branch (e.g. width:1280, height:720, frameRate:30), await it
and catch errors so you can fall back to the original track if applyConstraints
fails; adjust references to webcamStream.current and the code that starts the
recorder so it uses the upgraded track.

---

Nitpick comments:
In `@electron/ipc/handlers.ts`:
- Around line 221-224: The get-session-type IPC handler returns
process.env.XDG_SESSION_TYPE directly which may be unexpected values; update the
ipcMain.handle("get-session-type", ...) implementation to normalize the session
type to only "wayland" or "x11": read XDG_SESSION_TYPE (and fall back to
checking WAYLAND_DISPLAY as before), lowercase it, then return "wayland" if it
equals "wayland" and return "x11" for any other value. Ensure you update the
logic inside the ipcMain.handle callback so consumers always receive one of the
two known values.

In `@src/components/launch/LaunchWindow.tsx`:
- Around line 145-172: prevSourceId is declared inside the useEffect so it
resets whenever the effect re-runs causing duplicate startPreview calls; move
that state to a ref (e.g., prevSourceIdRef = useRef<string | null>(null)) at
component scope and replace uses of prevSourceId in the effect with
prevSourceIdRef.current, updating prevSourceIdRef.current = source.id after
calling startPreview(source.id) and when selecting a source so the previous
source id persists across effect runs; keep the rest of the checkSelectedSource
logic and the interval handling the same.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: cb9512f1-35fc-4232-8232-fa7c1dd78fd9

📥 Commits

Reviewing files that changed from the base of the PR and between b101820 and c69debc.

📒 Files selected for processing (10)
  • electron/electron-env.d.ts
  • electron/ipc/handlers.ts
  • electron/main.ts
  • electron/preload.ts
  • electron/windows.ts
  • src/components/launch/LaunchWindow.tsx
  • src/components/launch/LivePreview.tsx
  • src/hooks/usePreviewStream.ts
  • src/hooks/useScreenRecorder.ts
  • src/vite-env.d.ts

Enable WebRTCPipeWireCapturer and ozone-platform-hint flags on Linux
to support screen capture via PipeWire on Wayland sessions.
Expand from 500x155 fixed pill bar to 480x420 resizable window
(380-640 width range) to accommodate the live preview area.
Position bottom-right instead of bottom-center.
Add get-session-type IPC handler to detect display server type
on Linux, enabling Wayland-aware source selection in the renderer.
Manages the MediaStream lifecycle for screen capture preview:
- Starts/stops preview streams with source switching support
- Handles webcam stream alongside screen capture
- Supports stream detachment for seamless handoff to MediaRecorder
  (avoids double getUserMedia calls and Wayland re-prompts)
Real-time canvas-based preview that composites screen capture with
a circular webcam PiP overlay. Renders at 30fps with throttling,
caps internal resolution at 960px for GPU efficiency. Shows a
placeholder when no source is selected.
Accept optional PreviewStreamHandoff in toggleRecording/startRecording
to reuse existing preview MediaStreams instead of creating new ones.
This avoids double getUserMedia calls and PipeWire re-prompts on Wayland.
When a handoff is provided, video constraints are upgraded in-place.
Replace the 500x155 HUD pill bar with a full preview window featuring:
- Live screen capture preview that starts when a source is selected
- Canvas-composited webcam PiP overlay in the preview
- Recording indicator in the title bar
- Stream handoff from preview to recorder (no double getUserMedia)
- Auto-restart preview after recording stops
- Glass-morphism container styling
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

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

⚠️ Outside diff range comments (1)
src/components/launch/LaunchWindow.tsx (1)

187-214: ⚠️ Potential issue | 🟠 Major

Persist prevSourceId across effect reruns.

Line 189 recreates prevSourceId every time recording changes, so the same source looks “new” again after each stop. In the current flow that can call startPreview(source.id) once from the polling effect and again from the 500 ms restart path, which risks duplicate capture restarts and extra portal prompts.

💡 Proposed fix
-import { useCallback, useEffect, useState } from "react";
+import { useCallback, useEffect, useRef, useState } from "react";
...
+	const prevSourceIdRef = useRef<string | null>(null);
...
 	useEffect(() => {
-		let prevSourceId: string | null = null;
-
 		const checkSelectedSource = async () => {
 			if (window.electronAPI) {
 				const source = await window.electronAPI.getSelectedSource();
 				if (source) {
 					setSelectedSource(source.name);
 					setHasSelectedSource(true);

-					if (source.id !== prevSourceId && !recording) {
-						prevSourceId = source.id;
+					if (!recording && source.id !== prevSourceIdRef.current) {
+						prevSourceIdRef.current = source.id;
 						startPreview(source.id);
 					}
 				} else {
 					setSelectedSource("Screen");
 					setHasSelectedSource(false);
+					prevSourceIdRef.current = null;
 				}
 			}
 		};

Also applies to: 222-231

🤖 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 187 - 214, The bug is
that prevSourceId is reinitialized inside the useEffect on every rerun
(dependent on recording/startPreview), causing the same source to be treated as
new; move prevSourceId out of the effect and persist it across renders (e.g.,
use a React ref or component-level state) so checkSelectedSource can compare
against the previous value consistently; update references to prevSourceId in
the checkSelectedSource function and ensure cleanup/interval logic remains the
same so startPreview(source.id) is only called when the actual source id
changes.
♻️ Duplicate comments (1)
src/hooks/useScreenRecorder.ts (1)

456-459: ⚠️ Potential issue | 🟠 Major

Upgrade the handed-off webcam track before starting webcamRecorder.

Lines 456-459 still reuse the preview webcam stream as-is, so handoff recordings can fall back to preview quality even though the fresh-capture branch requests 1280×720 at 30 fps. Apply the same best-effort applyConstraints(...) upgrade on the reused track before creating webcamRecorder.

💡 Proposed fix
 			if (previewHandoff?.webcamStream) {
 				// Reuse preview webcam stream
 				webcamStream.current = previewHandoff.webcamStream;
+				const webcamTrack = webcamStream.current.getVideoTracks()[0];
+				if (webcamTrack) {
+					try {
+						await webcamTrack.applyConstraints({
+							width: { ideal: WEBCAM_TARGET_WIDTH },
+							height: { ideal: WEBCAM_TARGET_HEIGHT },
+							frameRate: {
+								ideal: WEBCAM_TARGET_FRAME_RATE,
+								max: WEBCAM_TARGET_FRAME_RATE,
+							},
+						});
+					} catch {
+						// Best-effort upgrade; keep preview settings if unsupported.
+					}
+				}
 			} else if (webcamEnabled) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/hooks/useScreenRecorder.ts` around lines 456 - 459, When reusing a
handed-off preview stream (previewHandoff.webcamStream) we must attempt to
upgrade its track constraints to match the fresh-capture settings before
creating the webcamRecorder; modify the branch that currently sets
webcamStream.current = previewHandoff.webcamStream so that you get the
MediaStreamTrack from previewHandoff.webcamStream (e.g., getVideoTracks()[0])
and call track.applyConstraints({ width: 1280, height: 720, frameRate: 30 }) in
a best-effort try/catch, falling back silently on failure, then set
webcamStream.current to the (possibly-upgraded) stream and proceed to create
webcamRecorder as in the fresh-capture path; reference previewHandoff,
webcamStream, webcamEnabled, webcamRecorder and applyConstraints when making
this change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/components/launch/LaunchWindow.tsx`:
- Around line 360-374: The select currently uses undefined names
(selectedDeviceId, setSelectedDeviceId, devices); update it to use the names
returned by useMicrophoneDevices: replace selectedDeviceId with selectedMicId,
setSelectedDeviceId with setSelectedMicId, and devices with micDevices, while
keeping the existing microphoneDeviceId and setMicrophoneDeviceId logic; ensure
the select value uses (microphoneDeviceId || selectedMicId), onchange calls
setSelectedMicId and setMicrophoneDeviceId, and the option list maps micDevices
(keying by device.deviceId and showing device.label) so the component
(LaunchWindow and the useMicrophoneDevices hook usage) compiles.
- Around line 384-492: The JSX control-bar has unbalanced tags causing a parse
error: remove the stray closing </button> after the restartRecording Tooltip and
the extraneous `)}` after the openVideoFile Tooltip, then rewrap the conditional
blocks so the outer divs and fragments are balanced; specifically, ensure the
block that renders the Record/Stop button, the conditional {recording &&
<Tooltip>...restartRecording...</Tooltip>} and the subsequent Tooltip buttons
(openVideoFile -> openProjectFile) are siblings inside the same container div,
and that you keep Tooltip components (and their inner buttons using
hudIconBtnClasses, openVideoFile, openProjectFile, restartRecording) properly
opened and closed with matching JSX tags and no leftover fragments or
parentheses.

---

Outside diff comments:
In `@src/components/launch/LaunchWindow.tsx`:
- Around line 187-214: The bug is that prevSourceId is reinitialized inside the
useEffect on every rerun (dependent on recording/startPreview), causing the same
source to be treated as new; move prevSourceId out of the effect and persist it
across renders (e.g., use a React ref or component-level state) so
checkSelectedSource can compare against the previous value consistently; update
references to prevSourceId in the checkSelectedSource function and ensure
cleanup/interval logic remains the same so startPreview(source.id) is only
called when the actual source id changes.

---

Duplicate comments:
In `@src/hooks/useScreenRecorder.ts`:
- Around line 456-459: When reusing a handed-off preview stream
(previewHandoff.webcamStream) we must attempt to upgrade its track constraints
to match the fresh-capture settings before creating the webcamRecorder; modify
the branch that currently sets webcamStream.current =
previewHandoff.webcamStream so that you get the MediaStreamTrack from
previewHandoff.webcamStream (e.g., getVideoTracks()[0]) and call
track.applyConstraints({ width: 1280, height: 720, frameRate: 30 }) in a
best-effort try/catch, falling back silently on failure, then set
webcamStream.current to the (possibly-upgraded) stream and proceed to create
webcamRecorder as in the fresh-capture path; reference previewHandoff,
webcamStream, webcamEnabled, webcamRecorder and applyConstraints when making
this change.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: b19ee7d9-96d0-415c-b086-b45a59d325af

📥 Commits

Reviewing files that changed from the base of the PR and between c69debc and 90539bf.

📒 Files selected for processing (2)
  • src/components/launch/LaunchWindow.tsx
  • src/hooks/useScreenRecorder.ts

Comment on lines +360 to +374
<div className="relative flex-1" style={{ maxWidth: "70%" }}>
<select
value={microphoneDeviceId || selectedDeviceId}
onChange={(e) => {
setSelectedDeviceId(e.target.value);
setMicrophoneDeviceId(e.target.value);
}}
className="w-full appearance-none bg-white/10 text-white text-xs rounded-full pl-3 pr-7 py-2 border border-white/20 outline-none truncate"
>
<div className="relative flex-1 min-w-0">
{!webcamExpanded && (
<div className="text-white/60 text-[10px] font-medium truncate">
{selectedCameraLabel}
</div>
)}
{webcamExpanded &&
(isCameraDevicesLoading ? (
<span className="text-white/40 text-[10px] italic">
{t("webcam.searching")}
</span>
) : cameraDevicesError ? (
<span className="text-white/40 text-[10px] italic">
{t("webcam.unavailable")}
</span>
) : cameraDevices.length === 0 ? (
<span className="text-white/40 text-[10px] italic">
{t("webcam.noneFound")}
</span>
) : (
<>
<select
value={webcamDeviceId || selectedCameraId}
onChange={(e) => {
setSelectedCameraId(e.target.value);
setWebcamDeviceId(e.target.value);
}}
className="w-full appearance-none bg-white/5 text-white text-[11px] rounded-lg pl-2 pr-6 py-1 border border-white/10 outline-none hover:bg-white/10 transition-colors cursor-pointer"
>
{cameraDevices.map((device) => (
<option
key={device.deviceId}
value={device.deviceId}
className="bg-[#1c1c24]"
>
{device.label}
</option>
))}
</select>
<ChevronDown
size={12}
className="absolute right-1.5 top-1/2 -translate-y-1/2 text-white/40 pointer-events-none"
/>
</>
))}
{(!webcamExpanded || cameraDevices.length === 0) && (
<select
value={webcamDeviceId || selectedCameraId}
onChange={(e) => {
setSelectedCameraId(e.target.value);
setWebcamDeviceId(e.target.value);
}}
className="sr-only"
>
{cameraDevices.map((device) => (
<option key={device.deviceId} value={device.deviceId}>
{device.label}
</option>
))}
</select>
)}
</div>
</div>
)}
{devices.map((device) => (
<option key={device.deviceId} value={device.deviceId}>
{device.label}
</option>
))}
</select>
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

cat -n src/components/launch/LaunchWindow.tsx | sed -n '350,380p'

Repository: siddharthvaddem/openscreen

Length of output: 1433


🏁 Script executed:

rg -A 2 -B 2 "useState|useCallback|const.*Mic|const.*Device" src/components/launch/LaunchWindow.tsx | head -80

Repository: siddharthvaddem/openscreen

Length of output: 1796


🏁 Script executed:

rg "const \[.*Mic.*\]|const \[.*mic.*\]|selectedMicId|selectedDeviceId|micDevices|devices" src/components/launch/LaunchWindow.tsx | head -40

Repository: siddharthvaddem/openscreen

Length of output: 624


🏁 Script executed:

rg -B 5 -A 5 "const.*useMicrophoneDevices|const.*=.*useMicrophone" src/components/launch/LaunchWindow.tsx

Repository: siddharthvaddem/openscreen

Length of output: 52


🏁 Script executed:

rg -B 10 "selectedMicId|micDevices" src/components/launch/LaunchWindow.tsx | head -50

Repository: siddharthvaddem/openscreen

Length of output: 1308


Fix stale microphone selector identifiers—this is a compile error.

The select element at lines 362, 364, and 369 references undefined identifiers. The useMicrophoneDevices hook destructures these with different names:

  • selectedDeviceIdselectedMicId
  • setSelectedDeviceIdsetSelectedMicId
  • devicesmicDevices
Proposed fix
-							value={microphoneDeviceId || selectedDeviceId}
+							value={microphoneDeviceId || selectedMicId}
 							onChange={(e) => {
-								setSelectedDeviceId(e.target.value);
+								setSelectedMicId(e.target.value);
 								setMicrophoneDeviceId(e.target.value);
 							}}
@@
-							{devices.map((device) => (
+							{micDevices.map((device) => (
📝 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.

Suggested change
<div className="relative flex-1" style={{ maxWidth: "70%" }}>
<select
value={microphoneDeviceId || selectedDeviceId}
onChange={(e) => {
setSelectedDeviceId(e.target.value);
setMicrophoneDeviceId(e.target.value);
}}
className="w-full appearance-none bg-white/10 text-white text-xs rounded-full pl-3 pr-7 py-2 border border-white/20 outline-none truncate"
>
<div className="relative flex-1 min-w-0">
{!webcamExpanded && (
<div className="text-white/60 text-[10px] font-medium truncate">
{selectedCameraLabel}
</div>
)}
{webcamExpanded &&
(isCameraDevicesLoading ? (
<span className="text-white/40 text-[10px] italic">
{t("webcam.searching")}
</span>
) : cameraDevicesError ? (
<span className="text-white/40 text-[10px] italic">
{t("webcam.unavailable")}
</span>
) : cameraDevices.length === 0 ? (
<span className="text-white/40 text-[10px] italic">
{t("webcam.noneFound")}
</span>
) : (
<>
<select
value={webcamDeviceId || selectedCameraId}
onChange={(e) => {
setSelectedCameraId(e.target.value);
setWebcamDeviceId(e.target.value);
}}
className="w-full appearance-none bg-white/5 text-white text-[11px] rounded-lg pl-2 pr-6 py-1 border border-white/10 outline-none hover:bg-white/10 transition-colors cursor-pointer"
>
{cameraDevices.map((device) => (
<option
key={device.deviceId}
value={device.deviceId}
className="bg-[#1c1c24]"
>
{device.label}
</option>
))}
</select>
<ChevronDown
size={12}
className="absolute right-1.5 top-1/2 -translate-y-1/2 text-white/40 pointer-events-none"
/>
</>
))}
{(!webcamExpanded || cameraDevices.length === 0) && (
<select
value={webcamDeviceId || selectedCameraId}
onChange={(e) => {
setSelectedCameraId(e.target.value);
setWebcamDeviceId(e.target.value);
}}
className="sr-only"
>
{cameraDevices.map((device) => (
<option key={device.deviceId} value={device.deviceId}>
{device.label}
</option>
))}
</select>
)}
</div>
</div>
)}
{devices.map((device) => (
<option key={device.deviceId} value={device.deviceId}>
{device.label}
</option>
))}
</select>
<div className="relative flex-1" style={{ maxWidth: "70%" }}>
<select
value={microphoneDeviceId || selectedMicId}
onChange={(e) => {
setSelectedMicId(e.target.value);
setMicrophoneDeviceId(e.target.value);
}}
className="w-full appearance-none bg-white/10 text-white text-xs rounded-full pl-3 pr-7 py-2 border border-white/20 outline-none truncate"
>
{micDevices.map((device) => (
<option key={device.deviceId} value={device.deviceId}>
{device.label}
</option>
))}
</select>
🧰 Tools
🪛 Biome (2.4.10)

[error] 362-362: The selectedDeviceId variable is undeclared.

(lint/correctness/noUndeclaredVariables)


[error] 364-364: The setSelectedDeviceId variable is undeclared.

(lint/correctness/noUndeclaredVariables)


[error] 369-369: The devices variable is undeclared.

(lint/correctness/noUndeclaredVariables)

🤖 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 360 - 374, The select
currently uses undefined names (selectedDeviceId, setSelectedDeviceId, devices);
update it to use the names returned by useMicrophoneDevices: replace
selectedDeviceId with selectedMicId, setSelectedDeviceId with setSelectedMicId,
and devices with micDevices, while keeping the existing microphoneDeviceId and
setMicrophoneDeviceId logic; ensure the select value uses (microphoneDeviceId ||
selectedMicId), onchange calls setSelectedMicId and setMicrophoneDeviceId, and
the option list maps micDevices (keying by device.deviceId and showing
device.label) so the component (LaunchWindow and the useMicrophoneDevices hook
usage) compiles.

Comment on lines +384 to 492
{/* Control bar */}
<div className={`px-3 py-2.5 ${styles.electronNoDrag}`}>
<div className="flex items-center justify-center gap-1.5 px-2 py-1.5 rounded-full bg-white/[0.03]">
{/* Source selector */}
<button
className={`${hudIconBtnClasses} ${systemAudioEnabled ? "drop-shadow-[0_0_4px_rgba(74,222,128,0.4)]" : ""}`}
onClick={() => !recording && setSystemAudioEnabled(!systemAudioEnabled)}
className={`${hudGroupClasses} p-2`}
onClick={openSourceSelector}
disabled={recording}
title={
systemAudioEnabled ? t("audio.disableSystemAudio") : t("audio.enableSystemAudio")
}
>
{systemAudioEnabled
? getIcon("volumeOn", "text-green-400")
: getIcon("volumeOff", "text-white/40")}
</button>
<button
className={`${hudIconBtnClasses} ${microphoneEnabled ? "drop-shadow-[0_0_4px_rgba(74,222,128,0.4)]" : ""}`}
onClick={toggleMicrophone}
disabled={recording}
title={microphoneEnabled ? t("audio.disableMicrophone") : t("audio.enableMicrophone")}
>
{microphoneEnabled
? getIcon("micOn", "text-green-400")
: getIcon("micOff", "text-white/40")}
{getIcon("monitor", "text-white/80")}
<span className="text-white/70 text-[11px] max-w-[80px] truncate">
{selectedSource}
</span>
</button>

{/* Audio controls group */}
<div className={hudGroupClasses}>
<button
className={`${hudIconBtnClasses} ${systemAudioEnabled ? "drop-shadow-[0_0_4px_rgba(74,222,128,0.4)]" : ""}`}
onClick={() => !recording && setSystemAudioEnabled(!systemAudioEnabled)}
disabled={recording}
title={
systemAudioEnabled ? t("audio.disableSystemAudio") : t("audio.enableSystemAudio")
}
>
{systemAudioEnabled
? getIcon("volumeOn", "text-green-400")
: getIcon("volumeOff", "text-white/40")}
</button>
<button
className={`${hudIconBtnClasses} ${microphoneEnabled ? "drop-shadow-[0_0_4px_rgba(74,222,128,0.4)]" : ""}`}
onClick={toggleMicrophone}
disabled={recording}
title={microphoneEnabled ? t("audio.disableMicrophone") : t("audio.enableMicrophone")}
>
{microphoneEnabled
? getIcon("micOn", "text-green-400")
: getIcon("micOff", "text-white/40")}
</button>
<button
className={`${hudIconBtnClasses} ${webcamEnabled ? "drop-shadow-[0_0_4px_rgba(74,222,128,0.4)]" : ""}`}
onClick={async () => {
await setWebcamEnabled(!webcamEnabled);
}}
title={webcamEnabled ? t("webcam.disableWebcam") : t("webcam.enableWebcam")}
>
{webcamEnabled
? getIcon("webcamOn", "text-green-400")
: getIcon("webcamOff", "text-white/40")}
</button>
</div>

{/* Record/Stop */}
<button
className={`${hudIconBtnClasses} ${webcamEnabled ? "drop-shadow-[0_0_4px_rgba(74,222,128,0.4)]" : ""}`}
onClick={async () => {
await setWebcamEnabled(!webcamEnabled);
}}
title={webcamEnabled ? t("webcam.disableWebcam") : t("webcam.enableWebcam")}
className={`flex items-center gap-1 rounded-full px-3 py-2 transition-colors duration-150 ${
recording ? "animate-record-pulse bg-red-500/10" : "bg-white/5 hover:bg-white/[0.08]"
}`}
onClick={handleToggleRecording}
disabled={!hasSelectedSource && !recording}
style={{ flex: "0 0 auto" }}
>
{webcamEnabled
? getIcon("webcamOn", "text-green-400")
: getIcon("webcamOff", "text-white/40")}
{recording ? (
<>
{getIcon("stop", "text-red-400")}
<span className="text-red-400 text-xs font-semibold tabular-nums">
{formatTimePadded(elapsed)}
</span>
</>
) : (
<>
{getIcon("record", hasSelectedSource ? "text-white/80" : "text-white/30")}
<span
className={`text-xs font-medium ${hasSelectedSource ? "text-white/60" : "text-white/20"}`}
>
REC
</span>
</>
)}
</button>
</div>

{/* Record/Stop group */}
<button
className={`flex items-center gap-0.5 rounded-full p-2 transition-colors duration-150 ${styles.electronNoDrag} ${
recording ? "animate-record-pulse bg-red-500/10" : "bg-white/5 hover:bg-white/[0.08]"
}`}
onClick={toggleRecording}
disabled={!hasSelectedSource && !recording}
style={{ flex: "0 0 auto" }}
>
{recording ? (
<>
{getIcon("stop", "text-red-400")}
<span className="text-red-400 text-xs font-semibold tabular-nums">
{formatTimePadded(elapsed)}
</span>
</>
) : (
getIcon("record", hasSelectedSource ? "text-white/80" : "text-white/30")
{/* Restart recording */}
{recording && (
<Tooltip content={t("tooltips.restartRecording")}>
<button className={hudIconBtnClasses} onClick={restartRecording}>
{getIcon("restart", "text-white/60")}
</button>
</Tooltip>
)}
</button>

{/* Restart recording */}
{recording && (
<Tooltip content={t("tooltips.restartRecording")}>
<button
className={`${hudIconBtnClasses} ${styles.electronNoDrag}`}
onClick={restartRecording}
>
{getIcon("restart", "text-white/60")}
{/* Open video file */}
<Tooltip content={t("tooltips.openVideoFile")}>
<button className={hudIconBtnClasses} onClick={openVideoFile} disabled={recording}>
{getIcon("videoFile", "text-white/60")}
</button>
</Tooltip>
)}

{/* Open video file */}
<Tooltip content={t("tooltips.openVideoFile")}>
<button
className={`${hudIconBtnClasses} ${styles.electronNoDrag}`}
onClick={openVideoFile}
disabled={recording}
>
{getIcon("videoFile", "text-white/60")}
</button>
</Tooltip>

{/* Open project */}
<Tooltip content={t("tooltips.openProject")}>
<button
className={`${hudIconBtnClasses} ${styles.electronNoDrag}`}
onClick={openProjectFile}
disabled={recording}
>
{getIcon("folder", "text-white/60")}
</button>
</Tooltip>

{/* Window controls */}
<div className={`flex items-center gap-0.5 ${styles.electronNoDrag}`}>
<button
className={windowBtnClasses}
title={t("tooltips.hideHUD")}
onClick={sendHudOverlayHide}
>
{getIcon("minimize", "text-white")}
</button>
<button
className={windowBtnClasses}
title={t("tooltips.closeApp")}
onClick={sendHudOverlayClose}
>
{getIcon("close", "text-white")}
</button>
{/* Open project */}
<Tooltip content={t("tooltips.openProject")}>
<button className={hudIconBtnClasses} onClick={openProjectFile} disabled={recording}>
{getIcon("folder", "text-white/60")}
</button>
</Tooltip>
</div>
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

cat -n src/components/launch/LaunchWindow.tsx | sed -n '384,492p'

Repository: siddharthvaddem/openscreen

Length of output: 4594


Repair the control-bar JSX structure before merging.

This block has mismatched closing tags: the stray </button> at line 476 (after the restart tooltip) and the extra )} at line 484 (after the video-file tooltip) leave the surrounding <div> tree unbalanced and prevent the file from parsing.

💡 One workable structure
				</div>

					{/* Restart recording */}
					{recording && (
						<Tooltip content={t("tooltips.restartRecording")}>
							<button className={hudIconBtnClasses} onClick={restartRecording}>
								{getIcon("restart", "text-white/60")}
							</button>
						</Tooltip>
					)}
-				</button>
-
					{/* Open video file */}
					<Tooltip content={t("tooltips.openVideoFile")}>
						<button className={hudIconBtnClasses} onClick={openVideoFile} disabled={recording}>
							{getIcon("videoFile", "text-white/60")}
						</button>
					</Tooltip>
-				)}
-
					{/* Open project */}
					<Tooltip content={t("tooltips.openProject")}>
						<button className={hudIconBtnClasses} onClick={openProjectFile} disabled={recording}>
							{getIcon("folder", "text-white/60")}
						</button>
					</Tooltip>
				</div>
📝 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.

Suggested change
{/* Control bar */}
<div className={`px-3 py-2.5 ${styles.electronNoDrag}`}>
<div className="flex items-center justify-center gap-1.5 px-2 py-1.5 rounded-full bg-white/[0.03]">
{/* Source selector */}
<button
className={`${hudIconBtnClasses} ${systemAudioEnabled ? "drop-shadow-[0_0_4px_rgba(74,222,128,0.4)]" : ""}`}
onClick={() => !recording && setSystemAudioEnabled(!systemAudioEnabled)}
className={`${hudGroupClasses} p-2`}
onClick={openSourceSelector}
disabled={recording}
title={
systemAudioEnabled ? t("audio.disableSystemAudio") : t("audio.enableSystemAudio")
}
>
{systemAudioEnabled
? getIcon("volumeOn", "text-green-400")
: getIcon("volumeOff", "text-white/40")}
</button>
<button
className={`${hudIconBtnClasses} ${microphoneEnabled ? "drop-shadow-[0_0_4px_rgba(74,222,128,0.4)]" : ""}`}
onClick={toggleMicrophone}
disabled={recording}
title={microphoneEnabled ? t("audio.disableMicrophone") : t("audio.enableMicrophone")}
>
{microphoneEnabled
? getIcon("micOn", "text-green-400")
: getIcon("micOff", "text-white/40")}
{getIcon("monitor", "text-white/80")}
<span className="text-white/70 text-[11px] max-w-[80px] truncate">
{selectedSource}
</span>
</button>
{/* Audio controls group */}
<div className={hudGroupClasses}>
<button
className={`${hudIconBtnClasses} ${systemAudioEnabled ? "drop-shadow-[0_0_4px_rgba(74,222,128,0.4)]" : ""}`}
onClick={() => !recording && setSystemAudioEnabled(!systemAudioEnabled)}
disabled={recording}
title={
systemAudioEnabled ? t("audio.disableSystemAudio") : t("audio.enableSystemAudio")
}
>
{systemAudioEnabled
? getIcon("volumeOn", "text-green-400")
: getIcon("volumeOff", "text-white/40")}
</button>
<button
className={`${hudIconBtnClasses} ${microphoneEnabled ? "drop-shadow-[0_0_4px_rgba(74,222,128,0.4)]" : ""}`}
onClick={toggleMicrophone}
disabled={recording}
title={microphoneEnabled ? t("audio.disableMicrophone") : t("audio.enableMicrophone")}
>
{microphoneEnabled
? getIcon("micOn", "text-green-400")
: getIcon("micOff", "text-white/40")}
</button>
<button
className={`${hudIconBtnClasses} ${webcamEnabled ? "drop-shadow-[0_0_4px_rgba(74,222,128,0.4)]" : ""}`}
onClick={async () => {
await setWebcamEnabled(!webcamEnabled);
}}
title={webcamEnabled ? t("webcam.disableWebcam") : t("webcam.enableWebcam")}
>
{webcamEnabled
? getIcon("webcamOn", "text-green-400")
: getIcon("webcamOff", "text-white/40")}
</button>
</div>
{/* Record/Stop */}
<button
className={`${hudIconBtnClasses} ${webcamEnabled ? "drop-shadow-[0_0_4px_rgba(74,222,128,0.4)]" : ""}`}
onClick={async () => {
await setWebcamEnabled(!webcamEnabled);
}}
title={webcamEnabled ? t("webcam.disableWebcam") : t("webcam.enableWebcam")}
className={`flex items-center gap-1 rounded-full px-3 py-2 transition-colors duration-150 ${
recording ? "animate-record-pulse bg-red-500/10" : "bg-white/5 hover:bg-white/[0.08]"
}`}
onClick={handleToggleRecording}
disabled={!hasSelectedSource && !recording}
style={{ flex: "0 0 auto" }}
>
{webcamEnabled
? getIcon("webcamOn", "text-green-400")
: getIcon("webcamOff", "text-white/40")}
{recording ? (
<>
{getIcon("stop", "text-red-400")}
<span className="text-red-400 text-xs font-semibold tabular-nums">
{formatTimePadded(elapsed)}
</span>
</>
) : (
<>
{getIcon("record", hasSelectedSource ? "text-white/80" : "text-white/30")}
<span
className={`text-xs font-medium ${hasSelectedSource ? "text-white/60" : "text-white/20"}`}
>
REC
</span>
</>
)}
</button>
</div>
{/* Record/Stop group */}
<button
className={`flex items-center gap-0.5 rounded-full p-2 transition-colors duration-150 ${styles.electronNoDrag} ${
recording ? "animate-record-pulse bg-red-500/10" : "bg-white/5 hover:bg-white/[0.08]"
}`}
onClick={toggleRecording}
disabled={!hasSelectedSource && !recording}
style={{ flex: "0 0 auto" }}
>
{recording ? (
<>
{getIcon("stop", "text-red-400")}
<span className="text-red-400 text-xs font-semibold tabular-nums">
{formatTimePadded(elapsed)}
</span>
</>
) : (
getIcon("record", hasSelectedSource ? "text-white/80" : "text-white/30")
{/* Restart recording */}
{recording && (
<Tooltip content={t("tooltips.restartRecording")}>
<button className={hudIconBtnClasses} onClick={restartRecording}>
{getIcon("restart", "text-white/60")}
</button>
</Tooltip>
)}
</button>
{/* Restart recording */}
{recording && (
<Tooltip content={t("tooltips.restartRecording")}>
<button
className={`${hudIconBtnClasses} ${styles.electronNoDrag}`}
onClick={restartRecording}
>
{getIcon("restart", "text-white/60")}
{/* Open video file */}
<Tooltip content={t("tooltips.openVideoFile")}>
<button className={hudIconBtnClasses} onClick={openVideoFile} disabled={recording}>
{getIcon("videoFile", "text-white/60")}
</button>
</Tooltip>
)}
{/* Open video file */}
<Tooltip content={t("tooltips.openVideoFile")}>
<button
className={`${hudIconBtnClasses} ${styles.electronNoDrag}`}
onClick={openVideoFile}
disabled={recording}
>
{getIcon("videoFile", "text-white/60")}
</button>
</Tooltip>
{/* Open project */}
<Tooltip content={t("tooltips.openProject")}>
<button
className={`${hudIconBtnClasses} ${styles.electronNoDrag}`}
onClick={openProjectFile}
disabled={recording}
>
{getIcon("folder", "text-white/60")}
</button>
</Tooltip>
{/* Window controls */}
<div className={`flex items-center gap-0.5 ${styles.electronNoDrag}`}>
<button
className={windowBtnClasses}
title={t("tooltips.hideHUD")}
onClick={sendHudOverlayHide}
>
{getIcon("minimize", "text-white")}
</button>
<button
className={windowBtnClasses}
title={t("tooltips.closeApp")}
onClick={sendHudOverlayClose}
>
{getIcon("close", "text-white")}
</button>
{/* Open project */}
<Tooltip content={t("tooltips.openProject")}>
<button className={hudIconBtnClasses} onClick={openProjectFile} disabled={recording}>
{getIcon("folder", "text-white/60")}
</button>
</Tooltip>
</div>
{/* Control bar */}
<div className={`px-3 py-2.5 ${styles.electronNoDrag}`}>
<div className="flex items-center justify-center gap-1.5 px-2 py-1.5 rounded-full bg-white/[0.03]">
{/* Source selector */}
<button
className={`${hudGroupClasses} p-2`}
onClick={openSourceSelector}
disabled={recording}
title={
systemAudioEnabled ? t("audio.disableSystemAudio") : t("audio.enableSystemAudio")
}
>
{getIcon("monitor", "text-white/80")}
<span className="text-white/70 text-[11px] max-w-[80px] truncate">
{selectedSource}
</span>
</button>
{/* Audio controls group */}
<div className={hudGroupClasses}>
<button
className={`${hudIconBtnClasses} ${systemAudioEnabled ? "drop-shadow-[0_0_4px_rgba(74,222,128,0.4)]" : ""}`}
onClick={() => !recording && setSystemAudioEnabled(!systemAudioEnabled)}
disabled={recording}
title={
systemAudioEnabled ? t("audio.disableSystemAudio") : t("audio.enableSystemAudio")
}
>
{systemAudioEnabled
? getIcon("volumeOn", "text-green-400")
: getIcon("volumeOff", "text-white/40")}
</button>
<button
className={`${hudIconBtnClasses} ${microphoneEnabled ? "drop-shadow-[0_0_4px_rgba(74,222,128,0.4)]" : ""}`}
onClick={toggleMicrophone}
disabled={recording}
title={microphoneEnabled ? t("audio.disableMicrophone") : t("audio.enableMicrophone")}
>
{microphoneEnabled
? getIcon("micOn", "text-green-400")
: getIcon("micOff", "text-white/40")}
</button>
<button
className={`${hudIconBtnClasses} ${webcamEnabled ? "drop-shadow-[0_0_4px_rgba(74,222,128,0.4)]" : ""}`}
onClick={async () => {
await setWebcamEnabled(!webcamEnabled);
}}
title={webcamEnabled ? t("webcam.disableWebcam") : t("webcam.enableWebcam")}
>
{webcamEnabled
? getIcon("webcamOn", "text-green-400")
: getIcon("webcamOff", "text-white/40")}
</button>
</div>
{/* Record/Stop */}
<button
className={`flex items-center gap-1 rounded-full px-3 py-2 transition-colors duration-150 ${
recording ? "animate-record-pulse bg-red-500/10" : "bg-white/5 hover:bg-white/[0.08]"
}`}
onClick={handleToggleRecording}
disabled={!hasSelectedSource && !recording}
style={{ flex: "0 0 auto" }}
>
{recording ? (
<>
{getIcon("stop", "text-red-400")}
<span className="text-red-400 text-xs font-semibold tabular-nums">
{formatTimePadded(elapsed)}
</span>
</>
) : (
<>
{getIcon("record", hasSelectedSource ? "text-white/80" : "text-white/30")}
<span
className={`text-xs font-medium ${hasSelectedSource ? "text-white/60" : "text-white/20"}`}
>
REC
</span>
</>
)}
</button>
</div>
{/* Restart recording */}
{recording && (
<Tooltip content={t("tooltips.restartRecording")}>
<button className={hudIconBtnClasses} onClick={restartRecording}>
{getIcon("restart", "text-white/60")}
</button>
</Tooltip>
)}
{/* Open video file */}
<Tooltip content={t("tooltips.openVideoFile")}>
<button className={hudIconBtnClasses} onClick={openVideoFile} disabled={recording}>
{getIcon("videoFile", "text-white/60")}
</button>
</Tooltip>
{/* Open project */}
<Tooltip content={t("tooltips.openProject")}>
<button className={hudIconBtnClasses} onClick={openProjectFile} disabled={recording}>
{getIcon("folder", "text-white/60")}
</button>
</Tooltip>
</div>
🧰 Tools
🪛 Biome (2.4.10)

[error] 385-385: Expected corresponding JSX closing tag for 'div'.

(parse)


[error] 484-484: Unexpected token. Did you mean {'}'} or &rbrace;?

(parse)

🤖 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 384 - 492, The JSX
control-bar has unbalanced tags causing a parse error: remove the stray closing
</button> after the restartRecording Tooltip and the extraneous `)}` after the
openVideoFile Tooltip, then rewrap the conditional blocks so the outer divs and
fragments are balanced; specifically, ensure the block that renders the
Record/Stop button, the conditional {recording &&
<Tooltip>...restartRecording...</Tooltip>} and the subsequent Tooltip buttons
(openVideoFile -> openProjectFile) are siblings inside the same container div,
and that you keep Tooltip components (and their inner buttons using
hudIconBtnClasses, openVideoFile, openProjectFile, restartRecording) properly
opened and closed with matching JSX tags and no leftover fragments or
parentheses.

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.

1 participant