Skip to content

Custom tab dialogs overlay#232

Open
iamEvanYT wants to merge 4 commits intomainfrom
cursor/custom-tab-dialogs-overlay-44aa
Open

Custom tab dialogs overlay#232
iamEvanYT wants to merge 4 commits intomainfrom
cursor/custom-tab-dialogs-overlay-44aa

Conversation

@iamEvanYT
Copy link
Member

Implement custom, tab-scoped JavaScript dialogs (alert, confirm, prompt) to replace inconsistent, window-blocking native dialogs.

The initial design for intercepting synchronous JavaScript dialogs via Chromium DevTools Protocol (CDP) proved unviable at runtime. This PR pivots to a robust solution using a preload main-world override combined with a synchronous custom protocol (flow-dialog://) handler in the main process. This approach ensures dialogs remain synchronous from the page's perspective while allowing the main process to manage the custom overlay lifecycle and tab-specific geometry.


Open in Web Open in Cursor 

Co-authored-by: Evan <iamEvanYT@users.noreply.github.com>
@cursor
Copy link

cursor bot commented Mar 7, 2026

Cursor Agent can help with this pull request. Just @cursor in comments and I'll start working on changes in this branch.
Learn more about Cursor Agents

@github-actions
Copy link
Contributor

github-actions bot commented Mar 7, 2026

Build artifacts for all platforms are ready! 🚀

Download the artifacts for:

One-line installer (Unstable):
bunx flow-debug-build --open 22789508418

(execution 22789508418 / attempt 1)

@greptile-apps
Copy link

greptile-apps bot commented Mar 7, 2026

Greptile Summary

This PR implements custom, tab-scoped JavaScript dialog overlays (alert, confirm, prompt) to replace Chromium's native blocking dialogs. The approach is architecturally sound: a preload script overrides the global dialog functions using a synchronous XHR to a custom flow-dialog:// protocol handler in the main process, keeping dialogs synchronous from the page's perspective while allowing the browser UI renderer to show a fully styled, tab-bounded overlay.

Key changes:

  • Preload (src/preload/index.ts): Patches window.alert/confirm/prompt in the main world via contextBridge.executeInMainWorld, sending a synchronous XHR to flow-dialog://dialog/request with a registered clientId.
  • Main process (tab-dialogs-controller.ts, ipc/browser/tab-dialogs.ts): TabDialogsController manages client registration keyed to WebContents, holds pending dialog promises, and broadcasts state changes to the correct browser window's renderer.
  • Renderer (javascript-dialogs.tsx, tab-overlay-portal.tsx, portal-bounds.tsx): A React overlay subscribes to dialog state and renders JavaScriptDialogCard components into per-tab portals whose position and size are driven by main-process-owned TabBoundsController geometry.
  • Tab bounds plumbing (bounds.ts, tabs-provider.tsx, tabs.ts): TabData now carries a bounds field populated by the spring-physics TabBoundsController, enabling the renderer to position overlays precisely over the active tab view.

Issues found:

  • In cleanupClient, all pending dialogs are resolved with accept: false, while handleProtocolRequest correctly returns accept: true for missing-client alerts — this creates an inconsistency (though currently harmless).
  • The exit animation declared on JavaScriptDialogCard's motion.div will never play because AnimatePresence is not used around the dialogs.map(...) in JavaScriptDialogsOverlay — dialogs will disappear instantly on dismissal.

Confidence Score: 4/5

  • Safe to merge with the two identified fixes applied; the architecture is well-designed with no security concerns.
  • The implementation is architecturally sound and the synchronous-XHR + protocol-handler approach works correctly. The clientId auth scheme is secure (IDs cannot be extracted from JS closures), and cleanup paths are properly handled. The two remaining issues are: (1) a visible UX regression where dialog exit animations won't play without AnimatePresence, and (2) a minor inconsistency in how alert dialogs are resolved during client cleanup (harmless today but worth aligning). Neither affects correctness or security.
  • src/renderer/src/components/browser-ui/tab-overlays/javascript-dialogs.tsx requires AnimatePresence to enable exit animations; src/main/controllers/tabs-controller/tab-dialogs-controller.ts should align alert cleanup behavior with handleProtocolRequest logic.

Sequence Diagram

sequenceDiagram
    participant Page as Web Page (Renderer)
    participant Preload as Preload Script
    participant Protocol as flow-dialog:// Handler (Main)
    participant Controller as TabDialogsController (Main)
    participant BrowserUI as Browser UI Renderer
    participant User as User

    Note over Preload: On load: installTabDialogsOverride()
    Preload->>Protocol: ipcRenderer.sendSync("tab-dialogs:register-client")
    Protocol-->>Preload: clientId

    Note over Preload: Patches window.alert/confirm/prompt

    Page->>Preload: window.alert("Hello!") [blocks]
    Preload->>Protocol: XHR POST flow-dialog://dialog/request (sync, blocks page)
    Protocol->>Controller: handleProtocolRequest(payload)
    Controller->>Controller: Create PendingDialogEntry + dialogId
    Controller->>BrowserUI: IPC "tab-dialogs:on-state-changed" [dialogs array]
    BrowserUI->>User: Render JavaScriptDialogCard overlay

    User->>BrowserUI: Click OK / press Enter
    BrowserUI->>Controller: IPC "tab-dialogs:respond"(dialogId, response)
    Controller->>Controller: Resolve pending Promise → HTTP Response
    Controller->>BrowserUI: IPC "tab-dialogs:on-state-changed" [empty]
    BrowserUI->>User: Dialog dismissed

    Protocol-->>Preload: XHR response {accept: true, promptText: ""}
    Preload-->>Page: alert() returns [unblocks]
Loading

Comments Outside Diff (1)

  1. src/renderer/src/components/browser-ui/tab-overlogs/javascript-dialogs.tsx, line 188-200 (link)

    JavaScriptDialogCard declares an exit transition on its motion.div (opacity: 0, y: 6, scale: 0.98), but Framer Motion only plays exit animations when the animated element is a direct child of AnimatePresence. Without this wrapper, dismissed dialogs vanish instantly instead of animating out.

    Wrap the dialogs.map() block in AnimatePresence (imported from motion/react) to enable the exit transition.

Last reviewed commit: e724371

for (const [dialogId, entry] of this.pendingDialogs) {
if (entry.clientId !== clientId) continue;

entry.resolve(this.createResolvedResponse(false, ""));
Copy link

Choose a reason for hiding this comment

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

In handleProtocolRequest, when no valid client exists the code correctly returns accept: true for alert dialogs:

return this.createResolvedResponse(payload.dialogType === "alert", "");

However, in cleanupClient, all pending dialogs — regardless of type — are resolved with accept: false:

entry.resolve(this.createResolvedResponse(false, ""));

For alert, this is functionally harmless today (the preload ignores the return value), but it creates an inconsistency. The cleanup should mirror the handling in handleProtocolRequest by resolving alert dialogs with accept: true:

Suggested change
entry.resolve(this.createResolvedResponse(false, ""));
entry.resolve(this.createResolvedResponse(entry.state.type === "alert", ""));

Comment on lines +188 to +200
return (
<>
{dialogs.map((dialog) => {
const tab = tabsById.get(dialog.tabId);
const originLabel = getDialogOriginLabel(tab?.url ?? "");

return (
<TabOverlayPortal key={dialog.id} tabId={dialog.tabId} autoFocus className="pointer-events-auto">
<JavaScriptDialogCard dialog={dialog} originLabel={originLabel} onRespond={handleRespond} />
</TabOverlayPortal>
);
})}
</>
Copy link

Choose a reason for hiding this comment

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

JavaScriptDialogCard declares an exit transition on its motion.div (opacity: 0, y: 6, scale: 0.98), but Framer Motion only plays exit animations when the animated element is a direct child of AnimatePresence. Without this wrapper, dismissed dialogs vanish instantly instead of animating out.

Wrap the dialogs.map() block in AnimatePresence (imported from motion/react) to enable the exit transition.

cursoragent and others added 3 commits March 7, 2026 00:46
Co-authored-by: Evan <iamEvanYT@users.noreply.github.com>
Co-authored-by: Evan <iamEvanYT@users.noreply.github.com>
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.

2 participants