Skip to content

Studio Dasboard with Separated UI for future scalability and Fixes#577

Open
makaradam wants to merge 43 commits into
siddharthvaddem:mainfrom
makaradam:feature/open-studio-and-import
Open

Studio Dasboard with Separated UI for future scalability and Fixes#577
makaradam wants to merge 43 commits into
siddharthvaddem:mainfrom
makaradam:feature/open-studio-and-import

Conversation

@makaradam
Copy link
Copy Markdown
Contributor

@makaradam makaradam commented May 11, 2026

Pull Request Template

Description

Studio / Editor Dashboard (Empty State)

Closes #576

Studio / Editor Dashboard (Empty State)

TOOLBAR BEFORE

Screenshot 2026-05-11 184414

TOOLBAR AFTER

image
  • Removed Open Video and Load Project from the main recorder toolbar; replaced with a single Studio button that opens the editor — a clean entry point that can scale to tutorials, FAQ, changelogs, or sponsor spots later

  • Built the editor empty state dashboard with Import Video File… and Load Project… buttons, supported formats hint, and a drag & drop zone

image

Project Management Logic

  • Importing a video now correctly creates an unsaved project (same treatment as a freshly recorded session), so the save flow is consistent from the start
  • File → New Project is now protected: if you have unsaved changes it shows a Save / Discard / Cancel dialog before clearing
  • File → Load Project (and the new header button) now has the same unsaved-changes protection — added a new "Load Project" variant to the dialog with its own copy
  • Added a New Project button to the editor header bar (next to Load Project) so you don't have to go through the File menu
  • Fixed a bug where New Project didn't actually clear the main process state — currentProjectPath was never nulled out, so switching to the recorder and back would reopen the discarded project

Design Decision — No "Import Video" in the File Menu

The File menu intentionally does not include an Import Video option.
The editor currently operates on a single video / single timeline model.
Adding File → Import Video would have effectively triggered a "new project"
action under the hood — silently discarding the current session and loading
a new video — which would be deeply confusing for end users with no visual
indication that their work was replaced.

The deliberate choice is to keep video import on the empty state dashboard
only
, where the context is unambiguous (there is nothing open to lose).
Once the editor evolves to support multi-clip timelines or video stitching,
a proper File → Import Video flow can be introduced with the right UX
scaffolding around it.

Drag & Drop

image
  • Drop zone in the empty state accepts .openscreen project files only
  • Dropping a non-.openscreen file shows an "Unsupported Format" dialog explaining to use Import Video instead
  • If the file can't be loaded, shows a "Could Not Open File" dialog
image
  • Fixed the actual drag & drop being broken entirely — File.path was removed in Electron 32+ (this project runs Electron 41); replaced with webUtils.getPathForFile()

Editor Loading & White Flash Fix

image
  • The editor window previously flashed a white screen before React had a chance to paint — a jarring UX issue on every open
  • Fixed at two levels: index.html now sets background: #09090b as an inline body style so the dark background is applied instantly before any JavaScript loads; VideoEditor is now lazy-loaded via React.lazy + Suspense with a custom dark-background spinner as the fallback, so the code-split chunk download no longer shows a blank white screen
  • The spinner displays contextual loading text: "Loading editor…" on cold open, switching to "Loading video…" when an existing session or video is detected — both messages are fully localized across all 11 languages

Bug Fixes

image
  • Webcam state leak — webcam video from a previous recording was bleeding into a new project after discarding or creating a new one; fixed by explicitly clearing webcam path state on both new project and video import
  • Device selector positioning — webcam and microphone selector panels were appearing behind the toolbar due to a mismatched pixel value (bottom-[68px] vs the actual 80px toolbar height); corrected so they float above it properly
  • Dialog variant flash (UnsavedChangesDialog) — when dismissing the unsaved changes dialog, content briefly flashed between the "New Project" and "Close" variants during the Radix UI closing animation; fixed with a frozen ref that preserves the last non-null variant throughout the animation
  • Dialog variant flash (drop error dialog) — same issue in the empty state: "Unsupported Format" was flashing to "Could Not Open File" for a frame on close; same ref pattern applied
  • Double dialog bug — clicking Load Project was opening two file picker dialogs back to back because both EditorEmptyState and VideoEditor were independently calling loadProjectFile(); fixed by unifying to a single callback

Custom Zoom Regression (Restored Fix)

image

The image represents that even if you wanted to use "Custom zoom" to put it to the corner, you couldn't because of an "invisible boundary". In the previous PR when "Custom-zoom" was created I already handled this edge case, but for some reason with a possible merge from an earlier version, the code might have been erased causing the bug again.

  • The custom zoom area drag-to-reposition feature had a known edge case where invisible boundaries near the edges of the canvas prevented the zoom region from being dragged to the desired position
  • This edge case was originally identified, handled, and shipped in a previous PR
  • It reappeared in this session, most likely due to a subsequent PR being merged that conflicted with or inadvertently overwrote that boundary handling code — a common occurrence in active parallel development branches
  • The fix has been restored to its original intended behavior

MOST functional bug fixes (based on recent main):

1103c93 — When isNativeWindowsCaptureAvailable returned reason="missing-helper", the code was throwing an error instead of gracefully falling back. This killed the recording entirely rather than continuing with the standard web MediaRecorder. Fixed by treating missing-helper the same as unsupported-os — silently return false and let the web recorder take over.
28819bd — Even after that fallback was unblocked, recording still failed on Windows because the fallback code path used navigator.mediaDevices.getDisplayMedia(), which requires a setDisplayMediaRequestHandler registered in the main process — that handler was never implemented. Fixed by replacing the platform-branched capture logic with a single getUserMedia + chromeMediaSource: "desktop" path that works on both macOS and Windows.

Localization

  • Full i18n for the empty state dashboard (title, buttons, formats hint, drag hint, drop overlay) across all 11 languages
  • Drop error dialog titles and messages translated to all 11 languages, including the "use Import Video File button" copy
  • Load Project unsaved-changes dialog translated to all 11 languages
  • Editor loading states ("Loading editor…" / "Loading video…") localized across all 11 languages

Motivation

Type of Change

  • New Feature
  • Bug Fix

Related Issue(s)

Closes #576

Screenshots / Video

Video (if applicable):

I'll send an unlisted YouTube Video for this, as this is quite of a big PR

Test Plan

Empty State & Navigation

  • Studio button opens editor; HUD toolbar no longer has Open Video / Load Project
  • Empty state shows Import Video, Load Project buttons, formats hint, drag hint

Drag & Drop

  • Drop a .openscreen file → project opens
  • Drop an unsupported file → "Unsupported Format" dialog, no flash on close
  • Drop an invalid/corrupt file → "Could Not Open File" dialog

Unsaved Changes Guard

  • New Project with unsaved changes → Save / Discard / Cancel dialog
  • Load Project with unsaved changes → same dialog with Load Project copy
  • Discard → switch to recorder → reopen Studio → empty state (not the old project)

UI & Layout

  • No white flash on editor open
  • Device selector panels appear above the toolbar
  • New Project button visible in editor header

Localization

  • Switch language → empty state, dialogs, and loading text all update correctly

Checklist

  • I have performed a self-review of my code.
  • I have added any necessary screenshots or videos.
  • I have linked related issue(s) and updated the changelog if applicable.

Edge Cases & Fixes

1103c93 + 28819bd — Recording fallback on Windows

Native Windows capture helper is absent in dev mode and optional in
production. The code was throwing instead of falling back, killing
recording entirely. Fixed by treating missing-helper as graceful
fallback to web getUserMedia + chromeMediaSource: "desktop" — works
on both macOS and Windows without the binary.

4cf966a — Drag & drop broken on Electron 32+ (File.path removed)

File.path was silently returning undefined on Electron 41 — dropped
files appeared to load but nothing happened. Replaced with
webUtils.getPathForFile() exposed via contextBridge in preload.

61de5ea — New Project didn't actually reset state

clearCurrentVideoPath() only nulled currentVideoPath. currentProjectPath
stayed set in the main process, so switching to the recorder and reopening
the editor reloaded the discarded project. Fixed by also clearing
currentProjectPath and currentRecordingSession.

c46aea5 — Double file picker on Load Project

Both EditorEmptyState and VideoEditor were independently calling
loadProjectFile(), opening two file pickers back to back. Fixed by
unifying to a single callback passed down as a prop.

4694ea4 + 2faa676 — Dialog content flash during close animation

Radix UI's closing animation sets the controlled value to null before
the exit animation completes. Both UnsavedChangesDialog (New Project vs
Close variants) and the drop error dialog (Unsupported Format vs Could Not
Open variants) were snapping to the wrong content mid-animation. Fixed with
a frozen ref that preserves the last non-null variant for the duration of
the animation.

801f1f6 — Webcam state leak between projects

After discarding a project or importing a new video, the webcam preview
path from the previous session was still mounted. Fixed by explicitly
clearing webcam state on both New Project and video import.

7dd438e — Device selector panels hidden behind toolbar

Webcam and microphone selector panels had bottom-[68px] but the toolbar
is actually 80px tall, causing them to render behind it. Corrected to
the right offset.

7e4595f + 7700c19 — Imported/recorded video not treated as unsaved

Importing a video or finishing a recording bypassed the unsaved changes
flag, so closing or starting a new project skipped the Save dialog
entirely. Fixed by marking the project dirty on both entry points.

96dc6d5 — Custom zoom drag stuck near canvas edges

The drag boundary clamping used a hardcoded scale instead of the custom
zoom scale, creating invisible walls near the canvas edges. The zoom region
couldn't be dragged to the intended position. Restored the correct
scale-aware boundary calculation.

edf5953 + 3c84f11 + 770f05f — White flash on editor open

Three compounding causes: (1) <body> had no background so the OS default
white showed before React painted; (2) VideoEditor was eagerly bundled,
so the JS parse delay showed a blank screen; (3) an insertCSS call wasn't
applied early enough. Fixed with an inline background: #09090b on <body>
in index.html and lazy-loading VideoEditor via React.lazy + Suspense
with a dark spinner fallback.


Thank you for contributing!

Summary by CodeRabbit

  • New Features

    • Drag‑and‑drop and direct filesystem project loading, new empty-editor landing UI, and virtual cursor overlay for Windows
    • "New Project" and "Import Video" menu actions + menu subscription hooks; editor shows localized loading spinner
    • Wider video import support and a helper to resolve dropped file paths
  • Bug Fixes

    • Prevented window/flash artifacts, improved HUD/countdown timing, and safer cursor restore on exit
    • Clear state for new recordings; refined zoom-focus and unsaved-change dialogs
  • Chores

    • Expanded localization across many languages (editor, dialogs, settings)

Review Change Stack

makaradam added 30 commits May 11, 2026 10:47
…mport

- Add Clapperboard icon button to HUD toolbar that calls switchToEditor(),
  opening the editor without a recording (Open Studio)
- Add EditorEmptyState component shown when no video is loaded, featuring:
  - Drag-and-drop zone for .openscreen project files
  - Import Video File button (MP4, MOV, WebM, MKV, AVI, M4V, WMV, FLV, TS)
  - Load Project button
- Add File menu items: New Project (Ctrl+N) and Import Video File (Ctrl+I)
- Add loadProjectFileFromPath to the full native-bridge chain so drag-dropped
  .openscreen files can be loaded without a file-picker dialog
- Expand ALLOWED_IMPORT_VIDEO_EXTENSIONS and file picker filters for all
  common video formats from any screen recorder
- Add i18n keys: tooltips.openStudio, unsavedChanges.newProject/importVideo
When switchToEditor() is called from the HUD (Open Studio), there is
no current video/session/project. Previously this set an error state
causing a white/broken screen. Now we simply leave videoPath as null,
which lets the EditorEmptyState component render correctly.
Using the CSS 'hidden' class kept VideoPlayback mounted with an empty
src, which fired an error event and triggered setError(). Switch to
proper conditional rendering so VideoPlayback only mounts when a video
is actually loaded.
… video

- Add missing ipcMain.handle('start-new-recording') that calls switchToHud,
  fixing the broken Return to Recorder confirm button
- Skip the Return to Recorder confirmation dialog when no video is loaded
  (Open Studio with nothing imported) — just switch back immediately
Replace light bg-background with dark #09090b on the loading and error
screens so they match the editor theme. Add a green spinning SVG loader
and muted text instead of the jarring white flash.
- Call clearCurrentVideoPath before startNewRecording so the session is
  properly dropped; next editor open starts fresh instead of reloading
  the previous video
- Set body background to #09090b in index.html so the pre-React paint
  is dark, eliminating the white flash before the spinner appears
The handler was closing mainWindow (the HUD) directly, then calling
createEditorWindowWrapper which closes it again. This double-close left
a ghost transparent window on each open/close cycle, causing the HUD
shadow to visually compound darker on every return-to-recorder.
createEditorWindowWrapper already handles closing the current window
cleanly, so the redundant close is removed.
The HUD pill uses box-shadow with a 60px blur radius. The previous
600x160 window was a tight fit, causing the shadow to be hard-clipped
at the transparent window edge. Increase to 800x260 to give the shadow
room — the pill's visual screen position is unchanged since it uses
CSS 'fixed bottom-5' relative to the window bottom.
- Use show:false + ready-to-show event so window only appears once
  content is painted, eliminating the black rectangle flash
- Increase window height to 320px and shift y down 55px so the pill
  (now bottom-20 = 80px from window bottom) has 80px of transparent
  space below it for the downward shadow to render unclipped
VideoPlayback's pointer drag handler was calling clampFocusToStage
with region.depth, which ignored customScale entirely. When dragging
the zoom indicator with a custom scale set, the drag was clamped to
the preset depth boundaries instead of the actual scale — making it
impossible to drag beyond those boundaries.

Switch to clampFocusToScale(focus, getZoomScale(region)) so the drag
respects customScale the same way the overlay indicator and export do.
The Open Studio button (Clapperboard) now handles opening the editor
where users can import any video. The separate open-video-file icon
and its handler are redundant and have been removed.
Open Studio already leads to the editor empty state which has a
Load Project button. The separate HUD toolbar shortcut is redundant.
Removed the button, its handler, and the now-unused folder icon import.
Use show:false + ready-to-show on the editor window so it stays
hidden until the first paint. Also set backgroundColor to #09090b
so even if the window becomes visible before React mounts, it shows
the correct dark background rather than white.
…nsertCSS

- Remove File > Import Video File (Ctrl+I) — importing lives in the
  editor empty state only, avoiding confusion with 'add on top of
  existing video' semantics
- Inject 'background: #09090b' on dom-ready so html/body/#root are
  dark before React mounts, eliminating the white sub-titlebar flash
  even on a cold first Vite load
The flash was caused by two compounding issues:
1. The Tailwind/shadcn CSS uses `.dark` class to switch from light
   (--background: white) to dark variables. Since no code applied `.dark`
   to <html> before React mounted, every `bg-background` usage resolved
   to white.
2. The lazy-loaded VideoEditor Suspense fallback (`bg-background`) was
   the visible culprit — it rendered white for the ~50-100ms while
   VideoEditor was being imported.

Fix: add `class="dark"` directly to the <html> element in index.html.
The attribute is parsed before any CSS or JS runs, so dark CSS variables
are in effect from frame 0 — no flash. HUD/source-selector windows are
unaffected because they override `background: transparent` via inline
styles (which beat stylesheet rules).

Also harden the Suspense fallback to use an explicit `bg-[#09090b]`
instead of `bg-background` as a belt-and-suspenders guard.
- Add `loadingEditor` translation key to all 11 locale editor.json files
- Suspense fallback in App.tsx now shows "Loading editor..." since at
  that point we don't yet know whether there is a video to load
- VideoEditor loading spinner starts as "Loading editor..." and flips
  to "Loading video..." only once loadInitialData discovers a recording
  session or video path to load — no video means it stays as
  "Loading editor..." throughout
…no project file

hasProjectUnsavedChanges only fires when the current snapshot differs from
the baseline — but for a fresh import or recording, the baseline IS the
current state (nothing edited yet), so the app considered it "saved" even
though no .openscreen file exists on disk.

Extend the hasUnsavedChanges check: if a video is loaded but
currentProjectPath is null (no file saved yet), treat it as unsaved
regardless of the snapshot diff. After a successful Save, currentProjectPath
is set and this clause becomes false, handing control back to the normal
snapshot comparison.
When File > New Project is triggered with an unsaved video/project loaded,
show the UnsavedChangesDialog with a "newProject" variant instead of
immediately clearing state. The variant swaps the copy to:
  - "Do you want to save your project before creating a new one?"
  - "Save & New Project" / "Discard & New Project" / Cancel

Implementation:
- UnsavedChangesDialog accepts an optional variant prop ("close"|"newProject")
  that switches detail text and button labels via translation keys
- showCloseConfirmDialog boolean replaced by confirmDialogVariant state
  ("close"|"newProject"|null) — one dialog handles both cases
- handleNewProject checks hasUnsavedChanges before clearing state;
  new doNewProject helper resets videoPath, currentProjectPath and
  lastSavedSnapshot cleanly
- Three new translation keys added to all 11 locale dialogs.json files:
  detailNewProject, saveAndNewProject, discardAndNewProject
The countdown-overlay-show handler was calling showInactive() before
waiting for the page to load — Chromium showed a black rectangle while
still painting the first frame.

Fix: wait for the ready-to-show event (fires after first paint) before
calling showInactive(), so the window only becomes visible once its
content is fully rendered. The redundant did-finish-load wait is replaced
by ready-to-show which is the correct Electron signal for this purpose.
…sing

When isNativeWindowsCaptureAvailable returns reason="missing-helper"
(helper binary not installed — always the case in dev mode), the code
was throwing instead of returning false, which killed the recording
entirely instead of falling through to the standard web MediaRecorder
path. Treat missing-helper the same as unsupported-os: silently return
false and let the web recorder take over.
…dia on Windows

The Windows code path used navigator.mediaDevices.getDisplayMedia() which
requires setDisplayMediaRequestHandler to be registered in the main process.
That handler was never implemented, causing a "Not supported" error and
preventing recording from starting on Windows when falling back from the
native helper.

Replace the platform-branched capture logic with a single getUserMedia +
chromeMediaSource: "desktop" path that works on both macOS and Windows.
Cursor capture mode is already handled separately via setRecordingState.
Use a ref to keep the last non-null confirmDialogVariant stable while the
dialog animates out. Previously, setting variant to null caused the fallback
??\"close\" to kick in mid-animation, briefly showing \"Save & Close\" content
and firing a spurious sendCloseConfirmResponse(\"cancel\") IPC call.
The mic/webcam selectors were positioned at bottom-[68px] but the HUD bar
sits at bottom-20 (80px), causing selectors to render behind the bar. Moved
to bottom-[136px] (bar bottom 80px + bar height ~46px + 10px gap).
doNewProject() was not resetting webcamVideoPath/webcamVideoSourcePath,
causing the webcam track from a previous recording to bleed into the next
project. Also clear them in handleImportVideo since imported external videos
never carry a webcam track.
The drop handler called onProjectLoaded() which was wired to handleLoadProject,
causing it to re-open a file picker dialog after the file was already loaded.
Added onProjectFileDropped callback so the loaded project data is passed
directly to applyLoadedProject without a second dialog.

Also improved the empty state UI: moved the drag-and-drop hint below the
supported formats line with a small upload icon for visual clarity.
- Non-.openscreen files now show "Unsupported Format" dialog instead of
  silently doing nothing
- Failed project loads (e.g. video path no longer accessible) show a
  "Could Not Open File" dialog with a helpful message
- Both use the same design language as the rest of the app (dark dialog,
  app icon, close button)
- Unified EditorEmptyState to a single onProjectOpened(project, path)
  callback used by both Load Project button and drag-and-drop, eliminating
  the double file-picker that was opening (EditorEmptyState + VideoEditor
  each calling loadProjectFile independently)
- Made loadProjectFileFromPath resilient to getApprovedProjectSession
  failures: path approval errors no longer block the project from loading,
  they just skip setting the recording session (video player handles the
  "file not found" case gracefully)
…Path via preload

- Import webUtils from electron in preload and expose getPathForFile(file)
  via contextBridge — replaces the removed File.path property (Electron 32+)
- Expose loadProjectFileFromPath(filePath) as a direct ipcRenderer.invoke
  call, bypassing the native-bridge routing layer for drag-drop use
- Add matching TypeScript declarations to electron-env.d.ts so the
  renderer can call window.electronAPI.getPathForFile /
  window.electronAPI.loadProjectFileFromPath with full type safety

Files changed:
  electron/preload.ts        — webUtils import + two new contextBridge entries
  electron/electron-env.d.ts — type declarations for both new API methods
…w project

clearCurrentVideoPath() previously only nulled currentVideoPath, leaving
currentProjectPath set. On next editor open, loadCurrentProjectFile() would
find a stale path and reload the discarded project. Fix by also clearing
currentProjectPath and calling setCurrentRecordingSessionState(null) so the
main-process state is fully reset when the user chooses New Project.

Files changed:
  electron/ipc/handlers.ts — clearCurrentVideoPath clears all three state vars
makaradam added 5 commits May 11, 2026 18:20
…alog polish

- Replace File.path (removed in Electron 32+) with
  window.electronAPI.getPathForFile() for drag-drop path resolution
- Use window.electronAPI.loadProjectFileFromPath() with try-catch for
  robust error handling on dropped project files
- Wire all UI strings through useScopedT("editor") / useScopedT("common")
  so the empty state is fully localised across 11 languages
- Add frozen-ref flash fix (lastDropErrorRef) so the drop-error dialog
  doesn't snap to the wrong content during the Radix closing animation
- Polish drop-error dialog layout: icon wrapped in a ring container
  (w-10 h-10 rounded-full bg-white/5 ring-1 ring-white/10) with a
  smaller w-5 h-5 AlertCircle inside for correct visual balance
- Add drag-over overlay and drag-leave boundary guard

Files changed:
  src/components/video-editor/EditorEmptyState.tsx — full rewrite
…ader button

UnsavedChangesDialog:
- Add "loadProject" as a third variant alongside "close" and "newProject"
- Detail text, save label, and discard label all branch on the new variant
  using nested ternaries for zero-duplication logic

VideoEditor:
- Rename old handleLoadProject to doLoadProject (the raw IPC work)
- New handleLoadProject wrapper: shows confirmDialogVariant="loadProject"
  when hasUnsavedChanges, otherwise calls doLoadProject directly
- Add handleLoadProjectConfirmSave / handleLoadProjectConfirmDiscard
  callbacks wired to the UnsavedChangesDialog for the loadProject path
- Add "New Project" button (FilePlus icon) to the editor header bar,
  positioned before the existing Load Project button
- Expand confirmDialogVariant state to "close"|"newProject"|"loadProject"|null
  with a matching lastConfirmVariantRef to prevent flash on dialog close

Files changed:
  src/components/video-editor/UnsavedChangesDialog.tsx — loadProject variant
  src/components/video-editor/VideoEditor.tsx          — guard + header button
…uages

Add three keys to every locale's dialogs.json:
  detailLoadProject   — context sentence shown under the dialog title
  saveAndLoadProject  — primary action button label
  discardAndLoadProject — secondary (destructive) action button label

Locales updated: ar, en, es, fr, ja-JP, ko-KR, ru, tr, vi, zh-CN, zh-TW

Files changed:
  src/i18n/locales/*/dialogs.json — 11 files, 3 new keys each
Add the full "emptyState" section to every locale's editor.json:
  title, description, importVideoButton, loadProjectButton,
  supportedFormats, dragDropHint, dropOverlay, and the nested
  dropErrors object (unsupportedFormatTitle/Message, couldNotOpenTitle/Message)

Locales updated: ar, en, es, fr, ja-JP, ko-KR, ru, tr, vi, zh-CN, zh-TW

Files changed:
  src/i18n/locales/*/editor.json — 11 files, full emptyState section each
Add project.new key to every locale's settings.json alongside the
existing project.save and project.load keys. Used by the new New Project
button in the editor header bar.

Locales updated: ar, en, es, fr, ja-JP, ko-KR, ru, tr, vi, zh-CN, zh-TW

Files changed:
  src/i18n/locales/*/settings.json — 11 files, 1 new key each
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 11, 2026

📝 Walkthrough

Walkthrough

Adds preload/native-bridge and IPC to load projects by filesystem path, a new Editor empty-state with drag/drop and menu flows, unsaved-change dialog variants for new/load/close, ready-to-show window polish, Windows cursor overlay and HUD cursor polling, editor-history reset, recorder unification, and broad i18n additions.

Changes

Studio Dashboard & Empty State Workflow

Layer / File(s) Summary
Type contracts & Renderer Bridge
electron/electron-env.d.ts, electron/preload.ts, src/native/contracts.ts, src/native/client.ts
Extended renderer bridge with getPathForFile, loadProjectFileFromPath, menu/cursor subscription helpers; added native request variant and native client helper for load-by-path.
Native Bridge & ProjectService
electron/ipc/nativeBridge.ts, electron/native-bridge/services/projectService.ts
Registered loadProjectFileFromPath on native bridge and ProjectService; ProjectService delegates to provided loader and refreshes context.
IPC Handlers & Recording Flow
electron/ipc/handlers.ts
Added load-project-file-from-path and start-new-recording IPCs with extension/readability/JSON validation and tolerant approval; expanded allowed video extensions; added PowerShell hide/restore cursor helpers and before-quit safety; recording lifecycle now manages HUD polling and cursor overlay.
Window Creation & Visual Setup
electron/windows.ts, index.html, electron/main.ts
HUD overlay resized/repositioned and shown on ready-to-show; editor window created hidden with darker bg and CSS injection; cursor overlay window helpers and HUD polling added; File menu gains “New Project” (CmdOrCtrl+N).
Preload Exposure
electron/preload.ts
Preload exposes loadProjectFileFromPath, getPathForFile via webUtils, and onMenuNewProject/onMenuImportVideo and onCursorTypeChange subscription helpers returning unsubscribe functions.
EditorEmptyState Component
src/components/video-editor/EditorEmptyState.tsx
New component: import-video and load-project buttons, drag-and-drop .openscreen handling using getPathForFile + loadProjectFileFromPath, stable drop-error dialog, and success callbacks.
Unsaved Changes Dialog Variants
src/components/video-editor/UnsavedChangesDialog.tsx
Dialog accepts variant prop and derives detail, saveLabel, and discardLabel from variant-specific i18n keys.
VideoEditor Integration
src/components/video-editor/VideoEditor.tsx
Refactor: conditional empty-state render when no videoPath; loadingMessage state; confirmDialogVariant + lastConfirmVariantRef for stable dialogs; unsaved-change detection includes loaded-video-without-project; doNewProject() resets state and clears paths; consolidated IPC menu listeners.
Cursor overlay frontend
src/components/launch/CursorOverlay.tsx, src/App.tsx
New CursorOverlay component subscribes to cursor-type events, renders bitmap or SVG cursors, hides document cursor, and App renders this window type; editor loading fallback now localized.
SettingsPanel & VideoPlayback
src/components/video-editor/SettingsPanel.tsx, src/components/video-editor/VideoPlayback.tsx
Added cursorDisplayMode (native-os
Hooks & history
src/hooks/useScreenRecorder.ts, src/hooks/useEditorHistory.ts
useScreenRecorder unified desktop capture and non-fatal missing-helper fallback; useEditorHistory adds resetState for new-project flow.
Localization (many locales)
src/i18n/locales/{ar,en,es,fr,ja-JP,ko-KR,ru,tr,vi,zh-CN,zh-TW}/*
Added loadingEditor, emptyState, unsavedChanges new/load variants, project.new keys, openStudio tooltip (en), and BOM adjustments across JSON files.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • siddharthvaddem
  • FabLrc

Poem

studio hums, a file slips in like a ghost,
dialogs ask, cursors hide — then reappear.
menus, locales, windows wait till paint is right,
small polish, big surface — ship it at night.

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: 370323c417

ℹ️ 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".

Comment thread src/components/video-editor/VideoEditor.tsx Outdated
Comment thread src/hooks/useScreenRecorder.ts
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 10

Caution

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

⚠️ Outside diff range comments (1)
electron/main.ts (1)

114-127: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Lowkey risky: Line 117 can force-close an existing editor and nuke unsaved work.

The concern is valid. At Line 114, if the focused window isn't an editor, createEditorWindowWrapper() gets called (Line 117). That function force-closes mainWindow by setting isForceClosing = true (Line 365), which bypasses the unsaved-changes save prompt in the close handler (Line 374). So if an editor is already open but unfocused, this drops all unsaved changes without warning.

Better approach: first search BrowserWindow.getAllWindows() for an existing unfocused editor using isEditorWindow() and route the action there. Only call createEditorWindowWrapper() if no editor exists.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@electron/main.ts` around lines 114 - 127, The current logic calls
createEditorWindowWrapper() (which force-closes mainWindow) whenever the focused
window isn't an editor, risking loss of unsaved work; change it to first search
BrowserWindow.getAllWindows() for an existing editor via isEditorWindow() and
route the action to that unfocused editor (set targetWindow to that window and
send the channel or wait for did-finish-load if necessary); only call
createEditorWindowWrapper() to create a new editor and assign mainWindow if no
existing editor window is found. Ensure you reference
BrowserWindow.getFocusedWindow(), BrowserWindow.getAllWindows(),
isEditorWindow(), createEditorWindowWrapper(), and mainWindow when making the
change so you avoid invoking the force-close path when an editor already exists.
🧹 Nitpick comments (8)
src/i18n/locales/zh-CN/settings.json (1)

1-1: 💤 Low value

nit: utf-8 bom at the start

there's a byte order mark (BOM) before the opening brace. most modern parsers handle it fine, but json specs kinda discourage it. lowkey might wanna check if your editor is adding this automatically - could cause weird issues with stricter parsers down the line.

not blocking anything though, electron/node will parse this just fine.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/i18n/locales/zh-CN/settings.json` at line 1, The file settings.json
contains a UTF-8 BOM (U+FEFF) before the opening brace; remove the BOM so the
file starts with "{" to avoid issues with strict JSON parsers, then save the
file as UTF-8 without BOM (adjust your editor/IDE or use "Save with encoding" to
do this). Optionally add an .editorconfig or CI lint rule to enforce "utf-8"
without BOM for JSON files and re-commit the cleaned settings.json.
src/i18n/locales/ar/settings.json (1)

1-1: ⚡ Quick win

Remove BOM from all locale JSON files for consistency

All 11 settings.json files in src/i18n/locales/ have a UTF-8 BOM (U+FEFF) at the start. While Node.js 22.22.1 handles this fine, Vite's bundler and the i18n loader work correctly regardless, it's cleaner to strip it. There's already precedent in scripts/test-windows-native-cursor.mjs where BOM is explicitly stripped.

Affected files (all locale variants)
src/i18n/locales/ar/settings.json
src/i18n/locales/en/settings.json
src/i18n/locales/es/settings.json
src/i18n/locales/fr/settings.json
src/i18n/locales/ja-JP/settings.json
src/i18n/locales/ko-KR/settings.json
src/i18n/locales/ru/settings.json
src/i18n/locales/tr/settings.json
src/i18n/locales/vi/settings.json
src/i18n/locales/zh-CN/settings.json
src/i18n/locales/zh-TW/settings.json
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/i18n/locales/ar/settings.json` at line 1, Remove the UTF-8 BOM (U+FEFF)
at the start of each locale settings.json file listed (e.g.,
src/i18n/locales/ar/settings.json and the other 10 locale variants) so the files
begin with the normal JSON '{' character; open each settings.json, delete the
invisible BOM character at the very start, save the file as UTF-8 without BOM,
and re-run build/tests to confirm the i18n loader and Vite bundler behave the
same.
src/hooks/useScreenRecorder.ts (1)

776-811: 💤 Low value

nit: the video-only getUserMedia call is written twice.

low priority, but the audio: false, video: videoConstraints call shows up in both the systemAudio-catch branch and the else branch. a tiny helper would dedupe and make the control flow easier to read:

♻️ optional cleanup
+			const captureVideoOnly = () =>
+				navigator.mediaDevices.getUserMedia({
+					audio: false,
+					video: videoConstraints,
+				} as unknown as MediaStreamConstraints);
+
 			if (systemAudioEnabled) {
 				try {
 					screenMediaStream = await navigator.mediaDevices.getUserMedia({
 						audio: {
 							mandatory: {
 								chromeMediaSource: CHROME_MEDIA_SOURCE,
 								chromeMediaSourceId: selectedSource.id,
 							},
 						},
 						video: videoConstraints,
 					} as unknown as MediaStreamConstraints);
 				} catch (audioErr) {
 					console.warn("System audio capture failed, falling back to video-only:", audioErr);
 					toast.error(t("recording.systemAudioUnavailable"));
-					screenMediaStream = await navigator.mediaDevices.getUserMedia({
-						audio: false,
-						video: videoConstraints,
-					} as unknown as MediaStreamConstraints);
+					screenMediaStream = await captureVideoOnly();
 				}
 			} else {
-				screenMediaStream = await navigator.mediaDevices.getUserMedia({
-					audio: false,
-					video: videoConstraints,
-				} as unknown as MediaStreamConstraints);
+				screenMediaStream = await captureVideoOnly();
 			}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/hooks/useScreenRecorder.ts` around lines 776 - 811, Duplicate
getUserMedia calls for video-only should be extracted into a small helper to
reduce repetition: create a function (e.g., getVideoOnlyStream or
fetchVideoOnlyStream) that returns navigator.mediaDevices.getUserMedia({ audio:
false, video: videoConstraints } as MediaStreamConstraints) and replace both
occurrences where screenMediaStream is assigned in the systemAudioEnabled catch
block and the else branch; keep existing references to videoConstraints,
screenMediaStream, systemAudioEnabled, audioErr, toast and t unchanged so
behavior and error handling remain the same.
src/i18n/locales/ko-KR/editor.json (1)

1-1: ⚡ Quick win

BOM is present, but it's not actually a problem

Yeah, there's a UTF-8 BOM here (), but honestly this is a non-issue. 32 other translation files across all locales have the same thing, and Vite's JSON loader handles it gracefully at build time. If it was breaking JSON parsing, you'd already be seeing errors in like, half your locales.

If it bugs you, stripping BOM from all the translation files (dialogs.json, editor.json, settings.json across all locales) would be a nice cleanup—but that's optional polish, not a fix. Just keep it consistent with the rest of the codebase if you touch it.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/i18n/locales/ko-KR/editor.json` at line 1, The file contains a UTF-8 BOM
character at the start of src/i18n/locales/ko-KR/editor.json; remove the leading
BOM (the invisible U+FEFF character) from this file and, if you touch other
locale files, remove the BOM consistently in dialogs.json, editor.json, and
settings.json across locales so all translation JSON files start with a plain
'{' and remain consistent with the rest of the codebase and Vite's JSON loader.
src/components/video-editor/EditorEmptyState.tsx (2)

110-111: Dialog onOpenChange could cause race

line 110: onOpenChange={(open) => !open && setDropError(null)} works but kinda subtle. if the dialog is opened programmatically (not by user interaction), this callback fires immediately and could clear the error before the animation completes. the lastDropErrorRef pattern saves you here but it's a bit fragile.

nit: could be clearer as onOpenChange={(open) => { if (!open) setDropError(null); }} but current code works fine.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/video-editor/EditorEmptyState.tsx` around lines 110 - 111, The
onOpenChange handler on the Dialog (onOpenChange={(open) => !open &&
setDropError(null)}) can clear dropError prematurely when the dialog is toggled
programmatically; change the handler to explicitly check for a closing
transition and only clear after the dialog has fully closed (e.g., use
onOpenChange={(open) => { if (!open) setDropError(null); }} combined with
waiting for the Dialog's onClose or animation end callback instead of
immediately calling setDropError, and remove reliance on the fragile
lastDropErrorRef pattern; target the Dialog component and the setDropError usage
to implement this safer clear-on-close behavior.

64-75: ⚡ Quick win

file extension check needs case handling

line 64 checks f.name.endsWith(".openscreen") which is case-sensitive. someone could drop project.OpenScreen or PROJECT.OPENSCREEN and it'd be rejected. probably rare but easy fix.

case-insensitive check
-		const projectFile = files.find((f) => f.name.endsWith(".openscreen"));
+		const projectFile = files.find((f) => f.name.toLowerCase().endsWith(".openscreen"));
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/video-editor/EditorEmptyState.tsx` around lines 64 - 75, The
file extension check in EditorEmptyState.tsx uses files.find((f) =>
f.name.endsWith(".openscreen")) which is case-sensitive and will reject variants
like "project.OpenScreen"; change the predicate to perform a case-insensitive
comparison (e.g., compare f.name.toLowerCase() with ".openscreen") and guard
against missing/undefined names (use f?.name or String(f.name) before
lowercasing); keep the rest of the flow (setting projectFile, setDropError, and
calling window.electronAPI.getPathForFile) unchanged.
electron/electron-env.d.ts (1)

169-169: ⚡ Quick win

getPathForFile return type might be wrong

the type says (file: File) => string but webUtils.getPathForFile() can potentially throw or return empty string on failure. might wanna make this string | null or string | undefined for safety, or at least document that it can throw.

safer type
-		getPathForFile: (file: File) => string;
+		getPathForFile: (file: File) => string | null;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@electron/electron-env.d.ts` at line 169, The declaration for getPathForFile
is too narrow: change its return type from (file: File) => string to reflect
failures (e.g., (file: File) => string | null or string | undefined) and add a
brief JSDoc note that webUtils.getPathForFile may also throw; then update any
callers (or ensure callers already check for null/undefined and catch
exceptions) to handle the absence of a path or thrown errors. Reference symbols:
getPathForFile and webUtils.getPathForFile.
src/i18n/locales/en/editor.json (1)

1-1: ⚡ Quick win

BOM character at start of file

there's a UTF-8 BOM () at the beginning of this file. this can cause parsing issues with some JSON parsers or tools. most modern parsers handle it but it's cleaner without it.

nit: remove the BOM if it wasn't intentional. most editors can strip it on save.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/i18n/locales/en/editor.json` at line 1, The file begins with a UTF-8 BOM
character (U+FEFF) immediately before the opening '{' which can break some JSON
parsers; remove that BOM so the file starts with '{', re-save it as UTF-8
without BOM, and run the JSON validator/linters to confirm parsing succeeds;
also update your editor/save settings (or project pre-commit hook) to prevent
writing BOMs in the future.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@electron/ipc/handlers.ts`:
- Around line 1780-1783: Change the case-sensitive endsWith check in the
drop/project validation to compare the file extension case-insensitively: use
path.extname(filePath).toLowerCase() and compare it to ("." +
PROJECT_FILE_EXTENSION).toLowerCase() (or lowercased PROJECT_FILE_EXTENSION)
instead of filePath.endsWith(`.${PROJECT_FILE_EXTENSION}`) in the validation
block inside electron/ipc/handlers.ts so names like "MyProject.OPENSCREEN" are
accepted; ensure path is imported if not already and keep the same early-return
shape ({ success: false, message: ... }) used around this check.

In `@electron/preload.ts`:
- Line 127: Wrap the call to webUtils.getPathForFile(file) inside getPathForFile
in the preload API with a try/catch so any thrown errors (e.g., inaccessible or
synthetic files) are caught and the function returns null instead of
propagating; update the exposed signature in electron-env.d.ts to return string
| null so callers can handle the nullable path. Ensure you reference
getPathForFile (preload) and webUtils.getPathForFile in the change and log or
silently swallow the caught error per project convention.

In `@src/components/video-editor/EditorEmptyState.tsx`:
- Around line 70-75: Wrap the call to
window.electronAPI.getPathForFile(projectFile) in a try/catch inside
EditorEmptyState (the drop handler) so exceptions from getPathForFile are
caught; on catch call setDropError("load-failed") (and optionally log the error)
and return, and keep the existing !filePath check after the try to handle falsy
returns. This ensures projectFile conversion won't throw and crash the handler.

In `@src/components/video-editor/VideoEditor.tsx`:
- Around line 2179-2186: The quick-start click handler currently calls
window.electronAPI.startNewRecording() and ignores its promise, swallowing
failures; change the empty-state branch to reuse the existing
handleNewRecordingConfirm() flow (or call startNewRecording() and await its
result, then run the same success/error path as handleNewRecordingConfirm) so
errors and { success: false } outcomes are handled the same as when
setShowNewRecordingDialog(true) is used; update the onClick branch that checks
videoPath to invoke handleNewRecordingConfirm() (or await and check the returned
value) and propagate errors to the same UI/error handling logic.
- Around line 716-724: doNewProject only clears file/path state; add a full
per-project editor reset by extracting the media-path clears into a new
resetEditorState helper that also clears timeline/history, undo/redo stacks,
crop state, wallpaper, cursor/mode state, selection(s), and any project-scoped
transient UI flags, then call that helper from doNewProject (replace the
existing state clears with a call to resetEditorState) and reuse the same
resetEditorState from the import flow handler(s) (e.g. your
importVideo/handleImport functions) so importing into an empty project can't
carry over previous edits; reference the current symbols (doNewProject,
nativeBridgeClient.project.clearCurrentVideoPath, setVideoPath,
setVideoSourcePath, setWebcamVideoPath, setWebcamVideoSourcePath,
setCurrentProjectPath, setLastSavedSnapshot) when locating where to plug in the
new reset.

In `@src/hooks/useScreenRecorder.ts`:
- Around line 787-811: When getUserMedia with system audio constraints fails
inside the block that checks systemAudioEnabled, call the setter to disable the
system-audio toggle so UI state matches reality (similar to how the mic path
uses setMicrophoneEnabled(false)); specifically, in the catch handling audioErr
for the block that assigns screenMediaStream with
chromeMediaSource/chromeMediaSourceId, add setSystemAudioEnabled(false) before
showing the toast and falling back to the video-only getUserMedia so the
systemAudioEnabled flag is cleared and the toggle won’t remain on.

In `@src/i18n/locales/ja-JP/settings.json`:
- Line 1: The file begins with a UTF-8 BOM character before the opening brace
"{" which should be removed; open the locale file, delete the BOM at the very
start (so the first character is '{'), and re-save the file encoded as UTF-8
without BOM to ensure cleaner, portable JSON (no code changes required beyond
removing the BOM).

In `@src/i18n/locales/ru/settings.json`:
- Line 1: The JSON file ru/settings.json (and the other locale JSON files like
dialogs.json, editor.json, settings.json in each locale) currently contains a
UTF-8 BOM at the start; remove the leading BOM character from the beginning of
these files so the first byte is the opening brace '{' (perform a bulk cleanup
across all 33 files to strip BOMs), verify files are valid UTF-8 without BOM and
commit the cleaned files.

In `@src/i18n/locales/tr/settings.json`:
- Line 1: Remove the UTF-8 BOM (U+FEFF) present at the start of each locale
settings.json (e.g., settings.json in every locale folder) so the files begin
directly with '{'. Locate files that start with the invisible BOM character and
delete that first character (ensure the first byte is not 0xEF,0xBB,0xBF), then
save and run your JSON/lint checks to confirm parsing still succeeds. Keep all
file encoding as UTF-8 without BOM going forward.

In `@src/i18n/locales/vi/settings.json`:
- Line 1: The file src/i18n/locales/vi/settings.json contains a UTF-8 BOM at the
start which can break some JSON parsers; remove the BOM bytes so the file begins
directly with "{" (i.e., re-save the file without BOM/UTF-8 signature using your
editor or run a tool/command that strips BOM) and ensure the file is saved as
plain UTF-8 without BOM so json loaders consume it correctly.

---

Outside diff comments:
In `@electron/main.ts`:
- Around line 114-127: The current logic calls createEditorWindowWrapper()
(which force-closes mainWindow) whenever the focused window isn't an editor,
risking loss of unsaved work; change it to first search
BrowserWindow.getAllWindows() for an existing editor via isEditorWindow() and
route the action to that unfocused editor (set targetWindow to that window and
send the channel or wait for did-finish-load if necessary); only call
createEditorWindowWrapper() to create a new editor and assign mainWindow if no
existing editor window is found. Ensure you reference
BrowserWindow.getFocusedWindow(), BrowserWindow.getAllWindows(),
isEditorWindow(), createEditorWindowWrapper(), and mainWindow when making the
change so you avoid invoking the force-close path when an editor already exists.

---

Nitpick comments:
In `@electron/electron-env.d.ts`:
- Line 169: The declaration for getPathForFile is too narrow: change its return
type from (file: File) => string to reflect failures (e.g., (file: File) =>
string | null or string | undefined) and add a brief JSDoc note that
webUtils.getPathForFile may also throw; then update any callers (or ensure
callers already check for null/undefined and catch exceptions) to handle the
absence of a path or thrown errors. Reference symbols: getPathForFile and
webUtils.getPathForFile.

In `@src/components/video-editor/EditorEmptyState.tsx`:
- Around line 110-111: The onOpenChange handler on the Dialog
(onOpenChange={(open) => !open && setDropError(null)}) can clear dropError
prematurely when the dialog is toggled programmatically; change the handler to
explicitly check for a closing transition and only clear after the dialog has
fully closed (e.g., use onOpenChange={(open) => { if (!open) setDropError(null);
}} combined with waiting for the Dialog's onClose or animation end callback
instead of immediately calling setDropError, and remove reliance on the fragile
lastDropErrorRef pattern; target the Dialog component and the setDropError usage
to implement this safer clear-on-close behavior.
- Around line 64-75: The file extension check in EditorEmptyState.tsx uses
files.find((f) => f.name.endsWith(".openscreen")) which is case-sensitive and
will reject variants like "project.OpenScreen"; change the predicate to perform
a case-insensitive comparison (e.g., compare f.name.toLowerCase() with
".openscreen") and guard against missing/undefined names (use f?.name or
String(f.name) before lowercasing); keep the rest of the flow (setting
projectFile, setDropError, and calling window.electronAPI.getPathForFile)
unchanged.

In `@src/hooks/useScreenRecorder.ts`:
- Around line 776-811: Duplicate getUserMedia calls for video-only should be
extracted into a small helper to reduce repetition: create a function (e.g.,
getVideoOnlyStream or fetchVideoOnlyStream) that returns
navigator.mediaDevices.getUserMedia({ audio: false, video: videoConstraints } as
MediaStreamConstraints) and replace both occurrences where screenMediaStream is
assigned in the systemAudioEnabled catch block and the else branch; keep
existing references to videoConstraints, screenMediaStream, systemAudioEnabled,
audioErr, toast and t unchanged so behavior and error handling remain the same.

In `@src/i18n/locales/ar/settings.json`:
- Line 1: Remove the UTF-8 BOM (U+FEFF) at the start of each locale
settings.json file listed (e.g., src/i18n/locales/ar/settings.json and the other
10 locale variants) so the files begin with the normal JSON '{' character; open
each settings.json, delete the invisible BOM character at the very start, save
the file as UTF-8 without BOM, and re-run build/tests to confirm the i18n loader
and Vite bundler behave the same.

In `@src/i18n/locales/en/editor.json`:
- Line 1: The file begins with a UTF-8 BOM character (U+FEFF) immediately before
the opening '{' which can break some JSON parsers; remove that BOM so the file
starts with '{', re-save it as UTF-8 without BOM, and run the JSON
validator/linters to confirm parsing succeeds; also update your editor/save
settings (or project pre-commit hook) to prevent writing BOMs in the future.

In `@src/i18n/locales/ko-KR/editor.json`:
- Line 1: The file contains a UTF-8 BOM character at the start of
src/i18n/locales/ko-KR/editor.json; remove the leading BOM (the invisible U+FEFF
character) from this file and, if you touch other locale files, remove the BOM
consistently in dialogs.json, editor.json, and settings.json across locales so
all translation JSON files start with a plain '{' and remain consistent with the
rest of the codebase and Vite's JSON loader.

In `@src/i18n/locales/zh-CN/settings.json`:
- Line 1: The file settings.json contains a UTF-8 BOM (U+FEFF) before the
opening brace; remove the BOM so the file starts with "{" to avoid issues with
strict JSON parsers, then save the file as UTF-8 without BOM (adjust your
editor/IDE or use "Save with encoding" to do this). Optionally add an
.editorconfig or CI lint rule to enforce "utf-8" without BOM for JSON files and
re-commit the cleaned settings.json.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: ad976fb7-5aa5-486b-848e-bbd49c9a6da7

📥 Commits

Reviewing files that changed from the base of the PR and between b0293e7 and 370323c.

📒 Files selected for processing (51)
  • electron/electron-env.d.ts
  • electron/ipc/handlers.ts
  • electron/ipc/nativeBridge.ts
  • electron/main.ts
  • electron/native-bridge/services/projectService.ts
  • electron/preload.ts
  • electron/windows.ts
  • index.html
  • src/App.tsx
  • src/components/launch/LaunchWindow.tsx
  • src/components/video-editor/EditorEmptyState.tsx
  • src/components/video-editor/UnsavedChangesDialog.tsx
  • src/components/video-editor/VideoEditor.tsx
  • src/components/video-editor/VideoPlayback.tsx
  • src/hooks/useScreenRecorder.ts
  • src/i18n/locales/ar/dialogs.json
  • src/i18n/locales/ar/editor.json
  • src/i18n/locales/ar/settings.json
  • src/i18n/locales/en/dialogs.json
  • src/i18n/locales/en/editor.json
  • src/i18n/locales/en/launch.json
  • src/i18n/locales/en/settings.json
  • src/i18n/locales/es/dialogs.json
  • src/i18n/locales/es/editor.json
  • src/i18n/locales/es/settings.json
  • src/i18n/locales/fr/dialogs.json
  • src/i18n/locales/fr/editor.json
  • src/i18n/locales/fr/settings.json
  • src/i18n/locales/ja-JP/dialogs.json
  • src/i18n/locales/ja-JP/editor.json
  • src/i18n/locales/ja-JP/settings.json
  • src/i18n/locales/ko-KR/dialogs.json
  • src/i18n/locales/ko-KR/editor.json
  • src/i18n/locales/ko-KR/settings.json
  • src/i18n/locales/ru/dialogs.json
  • src/i18n/locales/ru/editor.json
  • src/i18n/locales/ru/settings.json
  • src/i18n/locales/tr/dialogs.json
  • src/i18n/locales/tr/editor.json
  • src/i18n/locales/tr/settings.json
  • src/i18n/locales/vi/dialogs.json
  • src/i18n/locales/vi/editor.json
  • src/i18n/locales/vi/settings.json
  • src/i18n/locales/zh-CN/dialogs.json
  • src/i18n/locales/zh-CN/editor.json
  • src/i18n/locales/zh-CN/settings.json
  • src/i18n/locales/zh-TW/dialogs.json
  • src/i18n/locales/zh-TW/editor.json
  • src/i18n/locales/zh-TW/settings.json
  • src/native/client.ts
  • src/native/contracts.ts

Comment thread electron/ipc/handlers.ts
Comment thread electron/preload.ts Outdated
Comment thread src/components/video-editor/EditorEmptyState.tsx
Comment thread src/components/video-editor/VideoEditor.tsx Outdated
Comment thread src/components/video-editor/VideoEditor.tsx
Comment thread src/hooks/useScreenRecorder.ts
Comment thread src/i18n/locales/ja-JP/settings.json Outdated
Comment thread src/i18n/locales/ru/settings.json Outdated
Comment thread src/i18n/locales/tr/settings.json Outdated
Comment thread src/i18n/locales/vi/settings.json Outdated
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 11, 2026

Caution

Failed to replace (edit) comment. This is likely due to insufficient permissions or the comment being deleted.

Error details
{}

makaradam added 8 commits May 11, 2026 19:31
doNewProject() was only clearing media/project pointers (videoPath,
webcamVideoPath, projectPath). All undoable editor state — zoom regions,
trim regions, speed regions, annotation regions, crop, wallpaper,
shadow, blur, aspect ratio, webcam layout, etc. — was left intact, so
importing a new video after discarding a project would silently inherit
all previous edits.

Fix by:
- Adding resetState() to useEditorHistory — resets present to
  INITIAL_EDITOR_STATE and clears the full undo/redo history stack
- Calling resetState() in doNewProject()
- Also resetting non-undoable selection state (selectedZoomId, etc.),
  playback state (currentTime, isPlaying), cursor preferences
  (showCursor, cursorSize, cursorSmoothing, cursorMotionBlur,
  cursorClickBounce), and region ID counters (nextZoomIdRef, etc.)

Files changed:
  src/hooks/useEditorHistory.ts              — add resetState()
  src/components/video-editor/VideoEditor.tsx — call resetState() + full reset in doNewProject
endsWith('.openscreen') silently rejected files with uppercase extensions
(e.g. MyProject.OPENSCREEN) — a real false negative on Windows/macOS where
the OS can preserve or change extension casing. Replaced with
path.extname(filePath).toLowerCase() for consistent behaviour.

Files changed:
  electron/ipc/handlers.ts — loadProjectFileFromPath extension validation
PowerShell's default UTF-8-with-BOM encoding introduced ef bb bf at the
start of every locale file when the files were batch-patched earlier.
The upstream files were BOM-free. Stripped with sed across all 33 locale
JSON files (dialogs.json, editor.json, settings.json × 11 locales).

Files changed:
  src/i18n/locales/*/dialogs.json  — BOM removed (11 files)
  src/i18n/locales/*/editor.json   — BOM removed (11 files)
  src/i18n/locales/*/settings.json — BOM removed (11 files)
webUtils.getPathForFile() can throw if the file object is a synthetic
blob or comes from a non-filesystem context. Without a guard the
exception would surface as an unhandled rejection in the renderer.
Wrap in try/catch and return an empty string on failure so callers
can rely on the existing falsy check.

Files changed:
  electron/preload.ts — getPathForFile wrapped in try/catch
The !filePath guard only catches a falsy return value — it won't catch
an exception thrown by webUtils.getPathForFile() (e.g. synthetic blob,
virtual file from certain drag sources). Wrap the call in try/catch so
any thrown error is caught and shown as a "load-failed" drop error
instead of crashing the drop handler.

Files changed:
  src/components/video-editor/EditorEmptyState.tsx — try/catch around getPathForFile
… button

The empty-state fast path (no video loaded) was calling
startNewRecording() with void, silently dropping any failure result.
Replace with handleNewRecordingConfirm() which checks result.success
and calls setError() on failure — consistent with the dialog path.

Files changed:
  src/components/video-editor/VideoEditor.tsx — fast-path uses handleNewRecordingConfirm
When getUserMedia with system audio constraints throws, the code fell
back to video-only and showed a toast, but systemAudioEnabled stayed
true. The toggle kept showing "on" despite audio not being captured,
and every subsequent recording start would hit the same failed branch
and re-show the same toast. Added setSystemAudioEnabled(false) in the
catch block, consistent with how the mic path calls
setMicrophoneEnabled(false) on failure.

Files changed:
  src/hooks/useScreenRecorder.ts — setSystemAudioEnabled(false) on audio capture failure
…egression

This fix has been reverted twice by parallel PRs touching VideoPlayback.tsx.
The comment explains why getZoomScale(region) must be used instead of
region.depth in the drag handler, and explicitly warns against switching
it back.

Files changed:
  src/components/video-editor/VideoPlayback.tsx — protective comment on clampFocusForRegion
@makaradam
Copy link
Copy Markdown
Contributor Author

Hey @siddharthvaddem, I've sent you an email about this Feature as this is kinda gigantic like it was a 5-6hr session, that explains some things plus there's an unlisted video there that explains.

Also I've added now with the last commit in a6e91e3, for some reason after figuring out the edge case for the custom zoom, maybe a later merge had it removed or something, now it's more clearer (I'm explaining the bug in the video sent via email)

Hope you like so far, I'll check the cursor later (in the current main windows cursor gets baked in either way when cursor button is toggled)

I really hope that I tested the most of the edge cases (more than 20-30 QAs ran before shipping)

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@electron/native-bridge/cursor/recording/windowsNativeRecordingSession.ts`:
- Around line 227-246: The current emission block caches
lastEmittedCursorType/lastEmittedAssetId even when currentAssetId is present but
its bitmap/nativeAsset is not yet available, which suppresses a later real asset
emission; change the logic in the emission branch so you first resolve
nativeAsset = currentAssetId ? this.assets.get(currentAssetId) : null, then: if
currentAssetId && !nativeAsset, call
this.options.onCursorTypeChange(currentType, null) but do NOT update
this.lastEmittedCursorType or this.lastEmittedAssetId (so the real bitmap can
later trigger); otherwise (nativeAsset exists OR currentAssetId is null meaning
no custom asset) update lastEmittedCursorType/lastEmittedAssetId and emit the
payload including the nativeAsset properties when present; this uses
this.assets, this.options.onCursorTypeChange, this.lastEmittedCursorType, and
this.lastEmittedAssetId to locate the code to change.

In `@electron/windows.ts`:
- Around line 274-280: The overlay window is using
screen.getPrimaryDisplay().bounds so it only covers the primary monitor; replace
that with virtual-desktop bounds computed from screen.getAllDisplays() (or, if
you want the overlay on the display with the cursor, use
screen.getCursorScreenPoint() + screen.getDisplayNearestPoint()) and use that
computed bounds when constructing the BrowserWindow (update where bounds is read
and the BrowserWindow options in the win creation). Ensure you compute combined
x/y/width/height from displays' bounds (union) and then pass those values into
new BrowserWindow instead of screen.getPrimaryDisplay().bounds.

In `@src/components/launch/CursorOverlay.tsx`:
- Around line 243-246: The onCursorTypeChange handler currently only calls
setLiveAsset when asset is truthy, which leaves the previous bitmap set when the
main process sends null; update the handler registered via
window.electronAPI.onCursorTypeChange to always call setLiveAsset(asset) (or
setLiveAsset(asset ?? null)) regardless of truthiness and keep
setCursorType(type) as-is so the state clears correctly and the fallback SVG can
render; refer to the onCursorTypeChange registration, setCursorType, and
setLiveAsset symbols in CursorOverlay.tsx when making the change.

In `@src/components/video-editor/SettingsPanel.tsx`:
- Around line 1399-1419: The UI strings in SettingsPanel.tsx are hardcoded
(e.g., "Cursor Style", "Native OS", "Custom cursor", "Size") — update these to
use the existing i18n helper (t(...)) like the rest of the panel: replace
literal labels inside the Select/SelectTrigger/SelectItem blocks and other
occurrences around the cursor controls (including the range 1429-1490) with
t('key') calls, using the same translation keys/style used elsewhere in this
component; ensure cursorDisplayMode and onCursorDisplayModeChange behavior is
unchanged and only the displayed strings are wrapped with t(...) so translations
render correctly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 2f2c8d81-b664-4712-92bf-a5600e33948d

📥 Commits

Reviewing files that changed from the base of the PR and between a6e91e3 and 52ddaf4.

📒 Files selected for processing (12)
  • electron/electron-env.d.ts
  • electron/ipc/handlers.ts
  • electron/native-bridge/cursor/recording/factory.ts
  • electron/native-bridge/cursor/recording/windowsNativeRecordingSession.ts
  • electron/native-bridge/cursor/recording/windowsNativeRecordingSession.types.ts
  • electron/preload.ts
  • electron/windows.ts
  • src/App.tsx
  • src/components/launch/CursorOverlay.tsx
  • src/components/video-editor/SettingsPanel.tsx
  • src/components/video-editor/VideoEditor.tsx
  • src/components/video-editor/VideoPlayback.tsx
🚧 Files skipped from review as they are similar to previous changes (3)
  • electron/electron-env.d.ts
  • src/App.tsx
  • src/components/video-editor/VideoEditor.tsx

Comment thread electron/native-bridge/cursor/recording/windowsNativeRecordingSession.ts Outdated
Comment thread electron/windows.ts Outdated
Comment thread src/components/launch/CursorOverlay.tsx Outdated
Comment thread src/components/video-editor/SettingsPanel.tsx Outdated
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.

[Feature]: Studio/Editor dashboard instead of shortcuts, New Project state, with localization support

1 participant