feat: add live screen recording preview#319
feat: add live screen recording preview#319hamidlabs wants to merge 8 commits intosiddharthvaddem:mainfrom
Conversation
📝 WalkthroughWalkthroughAdds 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
Sequence DiagramsequenceDiagram
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)
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 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. ✨ Finishing Touches🧪 Generate unit tests (beta)
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.
💡 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".
There was a problem hiding this comment.
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_TYPEdirectly, 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:prevSourceIdresets when effect dependencies change, potentially causing redundant preview starts.The
prevSourceIdvariable is declared inside the effect, so it resets tonullwhenever the effect re-runs due to dependency changes (recordingorstartPreview). This means when recording stops, the effect restarts,prevSourceIdbecomesnull, andstartPreviewmay 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
📒 Files selected for processing (10)
electron/electron-env.d.tselectron/ipc/handlers.tselectron/main.tselectron/preload.tselectron/windows.tssrc/components/launch/LaunchWindow.tsxsrc/components/launch/LivePreview.tsxsrc/hooks/usePreviewStream.tssrc/hooks/useScreenRecorder.tssrc/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
There was a problem hiding this comment.
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 | 🟠 MajorPersist
prevSourceIdacross effect reruns.Line 189 recreates
prevSourceIdevery timerecordingchanges, so the same source looks “new” again after each stop. In the current flow that can callstartPreview(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 | 🟠 MajorUpgrade 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 creatingwebcamRecorder.💡 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
📒 Files selected for processing (2)
src/components/launch/LaunchWindow.tsxsrc/hooks/useScreenRecorder.ts
| <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> |
There was a problem hiding this comment.
🧩 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 -80Repository: siddharthvaddem/openscreen
Length of output: 1796
🏁 Script executed:
rg "const \[.*Mic.*\]|const \[.*mic.*\]|selectedMicId|selectedDeviceId|micDevices|devices" src/components/launch/LaunchWindow.tsx | head -40Repository: siddharthvaddem/openscreen
Length of output: 624
🏁 Script executed:
rg -B 5 -A 5 "const.*useMicrophoneDevices|const.*=.*useMicrophone" src/components/launch/LaunchWindow.tsxRepository: siddharthvaddem/openscreen
Length of output: 52
🏁 Script executed:
rg -B 10 "selectedMicId|micDevices" src/components/launch/LaunchWindow.tsx | head -50Repository: 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:
selectedDeviceId→selectedMicIdsetSelectedDeviceId→setSelectedMicIddevices→micDevices
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.
| <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.
| {/* 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> |
There was a problem hiding this comment.
🧩 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.
| {/* 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 }?
(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.
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.
requestAnimationFrameloop, capped at 960px internal resolution for GPU efficiencyMediaRecorderwhen recording starts — no doublegetUserMediacalls, no Wayland PipeWire re-promptsWebRTCPipeWireCapturerandozone-platform-hintflags, adds session type detection IPC for Wayland-aware behaviorChanges
electron/main.tselectron/windows.tselectron/ipc/handlers.tsget-session-typeIPC handlerelectron/preload.tsgetSessionTypeto rendererelectron/electron-env.d.tssrc/vite-env.d.tssrc/hooks/usePreviewStream.tssrc/hooks/useScreenRecorder.tsPreviewStreamHandoffto reuse preview streamssrc/components/launch/LivePreview.tsxsrc/components/launch/LaunchWindow.tsxTest plan
Summary by CodeRabbit
New Features
Refactor