feat: replace native OS close dialog with custom in-app dialog#521
feat: replace native OS close dialog with custom in-app dialog#521makaradam wants to merge 5 commits intosiddharthvaddem:mainsiddharthvaddem/openscreen:mainfrom makaradam:feature/save-dialog-redesignmakaradam/openscreen:feature/save-dialog-redesignCopy head branch name to clipboard
Conversation
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (1)
✅ Files skipped from review due to trivial changes (1)
📝 WalkthroughWalkthroughMain process close-confirm flow moved from a synchronous native dialog to an async, renderer-driven IPC flow: main prevents close, sends ChangesClose Dialog Conversion
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Review rate limit: 6/8 reviews remaining, refill in 11 minutes and 57 seconds.Comment |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: b3469c469b
ℹ️ 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: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@electron/main.ts`:
- Around line 293-310: The close-confirm handshake can be re-entered causing
multiple pending ipcMain.once handlers; add a per-window "close confirmation in
flight" guard (e.g., a WeakMap<BrowserWindow, boolean> or a property on the
window) checked at the top of the close flow and set to true just before sending
windowToClose.webContents.send("request-close-confirm"), then clear the flag in
every terminal branch of the ipc handlers (on "close-confirm-response" for
cancel/discard, and inside the "save-before-close-done" handler whether
shouldClose is true or false) so subsequent close events are ignored until the
confirmation completes; keep references to the same event names
("close-confirm-response", "save-before-close-done") and call
forceCloseEditorWindow(windowToClose) only once after clearing the flag
appropriately.
In `@src/components/video-editor/UnsavedChangesDialog.tsx`:
- Around line 22-76: UnsavedChangesDialog is rendering a hand-rolled modal that
lacks the shared Dialog semantics (focus trap, Escape handling, accessible
names)—replace the outer fragments and fixed divs with the app's Dialog
primitive used in VideoEditor.tsx, wiring Dialog's open/onOpenChange (or
onClose) to the existing onCancel handler so Escape and backdrop clicks call
onCancel, move the title/description into Dialog.Title/Description (or the
primitive's equivalents) using td("unsavedChanges.title") and
td("unsavedChanges.message")/td("unsavedChanges.detail"), ensure the icon-only
close button (X) has an accessible name (aria-label or use Dialog.Close) and
keep the existing action buttons calling onSaveAndClose and onDiscardAndClose;
preserve styling/classes but remove the custom focus/escape logic in favor of
the Dialog primitive for proper a11y and focus trapping.
🪄 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: ccb4cf52-1a68-4d61-a627-f656c6b1f15f
📒 Files selected for processing (5)
electron/electron-env.d.tselectron/main.tselectron/preload.tssrc/components/video-editor/UnsavedChangesDialog.tsxsrc/components/video-editor/VideoEditor.tsx
There was a problem hiding this comment.
Actionable comments posted: 1
♻️ Duplicate comments (1)
electron/main.ts (1)
298-307:⚠️ Potential issue | 🟠 Major | ⚡ Quick winKeep
isCloseConfirmInFlightset until the save flow actually finishesOn Line 299, the guard is reset before
save-before-close-donereturns, so a second close click can reopen the confirm flow and queue duplicate save/close work. lowkey risky for double-dispatch.Suggested patch
- ipcMain.once("close-confirm-response", (_, choice: "save" | "discard" | "cancel") => { - isCloseConfirmInFlight = false; + ipcMain.once("close-confirm-response", (_, choice: "save" | "discard" | "cancel") => { if (!windowToClose || windowToClose.isDestroyed()) return; if (choice === "save") { // Tell renderer to save the project, then close when done windowToClose.webContents.send("request-save-before-close"); ipcMain.once("save-before-close-done", (_, shouldClose: boolean) => { + isCloseConfirmInFlight = false; if (!shouldClose) return; forceCloseEditorWindow(windowToClose); }); } else if (choice === "discard") { + isCloseConfirmInFlight = false; forceCloseEditorWindow(windowToClose); + } else { + isCloseConfirmInFlight = false; } // "cancel": flag reset, window stays open });#!/bin/bash # Verify guard lifecycle around close-confirm/save-before-close flow rg -n -C4 'isCloseConfirmInFlight|close-confirm-response|save-before-close-done' electron/main.ts🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@electron/main.ts` around lines 298 - 307, The guard isCloseConfirmInFlight is currently cleared immediately in the close-confirm-response handler allowing a second close to re-enter while the save flow is in-flight; move or defer clearing that flag so it remains true until the entire save/close flow completes: remove the early isCloseConfirmInFlight = false at the top of the ipcMain.once("close-confirm-response", ...) handler and instead clear isCloseConfirmInFlight only inside each terminal branch of the flow (after forceCloseEditorWindow when choice === "save" and after handling discard/cancel), and also clear it inside the ipcMain.once("save-before-close-done", ...) callback after handling shouldClose to ensure the flag covers the full save-before-close lifetime; reference: isCloseConfirmInFlight, ipcMain.once("close-confirm-response"), ipcMain.once("save-before-close-done"), windowToClose, forceCloseEditorWindow.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@electron/main.ts`:
- Around line 298-305: The IPC handlers for "close-confirm-response" and
"save-before-close-done" are currently using ipcMain.once and will consume the
first matching message from any renderer, so scope them to the originating
window by switching to named listeners that check event.sender.id against
windowToClose.webContents.id and only act when they match; implement
ipcMain.on("close-confirm-response", handler) and
ipcMain.on("save-before-close-done", handler) where each handler does if
(!windowToClose || windowToClose.isDestroyed()) { cleanup/remove listener } else
if (event.sender.id !== windowToClose.webContents.id) return; else perform the
existing logic (set isCloseConfirmInFlight, send "request-save-before-close",
handle shouldClose) and remove the listener after handling to avoid leaks.
Ensure you reference the existing variables windowToClose and
isCloseConfirmInFlight and remove the listener if the window is closed or after
a successful match.
---
Duplicate comments:
In `@electron/main.ts`:
- Around line 298-307: The guard isCloseConfirmInFlight is currently cleared
immediately in the close-confirm-response handler allowing a second close to
re-enter while the save flow is in-flight; move or defer clearing that flag so
it remains true until the entire save/close flow completes: remove the early
isCloseConfirmInFlight = false at the top of the
ipcMain.once("close-confirm-response", ...) handler and instead clear
isCloseConfirmInFlight only inside each terminal branch of the flow (after
forceCloseEditorWindow when choice === "save" and after handling
discard/cancel), and also clear it inside the
ipcMain.once("save-before-close-done", ...) callback after handling shouldClose
to ensure the flag covers the full save-before-close lifetime; reference:
isCloseConfirmInFlight, ipcMain.once("close-confirm-response"),
ipcMain.once("save-before-close-done"), windowToClose, forceCloseEditorWindow.
🪄 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: 2963b14c-9402-415e-b91b-a7c567b37d7c
📒 Files selected for processing (2)
electron/main.tssrc/components/video-editor/UnsavedChangesDialog.tsx
./openscreen.png resolves correctly both in dev (Vite serves public/) and in production (loadFile sets base to dist/, where public assets land inside the asar). getAssetPath points to extraResources, which is the wrong location for bundled dist assets.
Both ipcMain.once handlers now check event.sender.id against windowToClose.webContents.id and ignore messages from any other renderer, preventing cross-window response mix-ups if multiple editor windows are ever open simultaneously.
Closes #520
Replaces dialog.showMessageBoxSync in the main process with a custom React dialog rendered inside the Electron window.
What changed
electron/main.ts — close event now sends request-close-confirm IPC to the renderer instead of calling the native dialog
electron/preload.ts + electron/electron-env.d.ts — two new bridge functions: onRequestCloseConfirm and sendCloseConfirmResponse
UnsavedChangesDialog.tsx — new component matching the app's dark theme, with the app logo and the same three actions
VideoEditor.tsx — registers the IPC listener and wires up the dialog state
What didn't change
The three-action behavior is identical — Save & Close still triggers the existing request-save-before-close / save-before-close-done flow, Discard still force-closes, Cancel keeps the window open. No functional changes.
Tested on Windows — all three actions verified including the save flow.
Summary by CodeRabbit
New Features
Bug Fixes