Skip to content

fix(browser-tab): right-click context menu inside embedded webview tabs#1066

Open
pedramamini wants to merge 3 commits into
rcfrom
fix/1065-browser-tab-context-menu
Open

fix(browser-tab): right-click context menu inside embedded webview tabs#1066
pedramamini wants to merge 3 commits into
rcfrom
fix/1065-browser-tab-context-menu

Conversation

@pedramamini

@pedramamini pedramamini commented Jun 1, 2026

Copy link
Copy Markdown
Collaborator

Closes #1065

Problem

Right-clicking inside an embedded Maestro browser tab did nothing. Electron renders no default context menu for guest <webview> content, and the only context-menu handler in window-manager.ts was registered on mainWindow.webContents (the app window). The guest webContents never had one, so right-click was dead inside browser-tab pages.

Fix

Wire a context-menu handler onto the guest webContents in the existing did-attach-webview block. It builds a native menu whose actions operate on the guest webContents (edit roles would target the host window, so each item calls the guest's edit/navigation methods directly):

  • Editable fields: Cut / Copy / Paste / Select All, plus spellcheck suggestions and Add to Dictionary when Chromium flags a misspelled word. This is the path that matters most for login/form workflows (and helps the paste case from Cannot paste clipboard text into embedded browser tab form fields #1063).
  • Selected text: Copy
  • Links: Copy Link, and Open Link in Browser (restricted to http/https/mailto so a malicious page can't smuggle a dangerous URL into shell.openExternal).
  • Images: Copy Image, Copy Image Address
  • Page background: Back / Forward / Reload (navigation state read from navigationHistory, the non-deprecated API in Electron 41).

Navigation actions are always offered, so right-clicking blank background still produces a useful menu.

Notes

  • The base branch is rc, not main: the browser-tab webview feature (Port browser tabs into the unified rc tab strip #785) currently lives only on rc, so the fix has to land there.
  • Item actions go through the guest's own edit/navigation methods and clipboard.writeText; existing will-navigate/will-redirect hardening still gates any navigation the guest performs.

Tests

Added 11 tests in window-manager.test.ts covering handler registration, editable-field menu + editFlags enable/disable, spellcheck suggestions, link copy/open + scheme guard, image copy, selection copy, navigation state, and separator hygiene. Full file (48 tests) passes; window-manager.ts type-checks and lints clean.

Acceptance criteria

  • Right-clicking browser-tab content opens a native context menu
  • Editable fields expose Cut / Copy / Paste / Select All
  • Selection, links, images, and page background get sensible actions where Electron exposes the params
  • Actions operate on the guest webContents, not the app window
  • Targeted integration tests for the webview context-menu path

Summary by CodeRabbit

  • New Features
    • Embedded browser tabs now show a native right-click menu with editing actions (cut/copy/paste/select all), spellcheck suggestions and “Add to Dictionary”, link actions with protocol restrictions (copy/open), image copy/address options, and navigation controls (back/forward/reload).
  • Tests
    • Added comprehensive tests covering browser-tab context menu behaviors and menu item wiring.

…w tabs

Electron renders no default context menu for guest <webview> content, so
right-clicking inside a browser tab produced nothing. The only context-menu
handler lived on mainWindow.webContents (the app window) and never reached the
guest.

Wire a context-menu handler onto the guest webContents in did-attach-webview
that builds a native menu whose actions target the guest:

- editable fields: Cut / Copy / Paste / Select All (+ spellcheck suggestions)
- selected text: Copy
- links: Copy Link, Open Link in Browser (http/https/mailto only)
- images: Copy Image, Copy Image Address
- page background: Back / Forward / Reload

Actions call guest webContents methods directly (edit roles would target the
host window), and shell.openExternal is restricted to safe schemes so a
malicious page can't smuggle a dangerous URL through the menu.

closes #1065
@coderabbitai

coderabbitai Bot commented Jun 1, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 111e17e6-b14a-4df9-8c0a-9e560c48ed02

📥 Commits

Reviewing files that changed from the base of the PR and between 1efa9e6 and b5882ae.

📒 Files selected for processing (2)
  • src/__tests__/main/app-lifecycle/window-manager.test.ts
  • src/main/app-lifecycle/window-manager.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/main/app-lifecycle/window-manager.ts
  • src/tests/main/app-lifecycle/window-manager.test.ts

📝 Walkthrough

Walkthrough

This PR wires a guest-targeted context menu for embedded browser-tab webviews, adding a context-menu template builder with editable, non-editable, image, link, navigation, and spellcheck actions (with a protocol allowlist for external opens) and comprehensive tests/mocks covering those scenarios.

Changes

Browser-tab context menu implementation and testing

Layer / File(s) Summary
Context menu implementation for browser tabs
src/main/app-lifecycle/window-manager.ts
Adds clipboard import, defines protocol whitelist (http:, https:, mailto:), implements a context-menu template builder that assembles editable actions (cut/copy/paste/select all, spellcheck suggestions/Add to Dictionary), non-editable actions (copy link, optionally Open Link in Browser when protocol allowed, copy image/copy image address, copy selection), and navigation controls (back/forward/reload); attaches the handler during did-attach-webview.
Test mocks for context menu
src/__tests__/main/app-lifecycle/window-manager.test.ts
Extends guest webContents mock with navigation-history (canGoBack/canGoForward/goBack/goForward/reload) and editing/spellcheck/image/session/dictionary APIs; extends electron mock with Menu.buildFromTemplate, shell.openExternal, and clipboard.writeText spies.
Browser-tab context menu test suite
src/__tests__/main/app-lifecycle/window-manager.test.ts
Adds tests that attach a guest webview, capture the guest context-menu handler, and validate menu template construction, click wiring for edit actions, disabled-state logic, spellcheck suggestions/Add-to-Dictionary, copying/opening links with scheme allowlist, image copy/address copy, copying selected non-editable text, and Back/Forward/Reload item rendering and separators.
sequenceDiagram
  participant WebView as Browser Tab Webview
  participant Guest as Guest webContents
  participant Main as Main Process (window-manager)
  participant Menu as Electron Menu
  WebView->>Guest: user right-clicks in page
  Guest->>Main: emits 'context-menu' with params
  Main->>Main: build template from params and guest navigation/session state
  Main->>Menu: Menu.buildFromTemplate(template)
  Menu->>Guest: popup({window: guest})
  Guest->>Main: menu item click executes guest or main actions (editing, navigation, clipboard, shell)
Loading

🎯 4 (Complex) | ⏱️ ~45 minutes

🐰 A right-click in a tab so true,
Brings cut, paste, suggestions too,
Links and images copy with care,
Webviews now respond and share 🥕

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 55.56% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'fix(browser-tab): right-click context menu inside embedded webview tabs' accurately and specifically describes the main change: adding right-click context menu support to embedded browser-tab webviews.
Linked Issues check ✅ Passed The PR implements all core objectives from issue #1065: registers context-menu handler on guest webContents, provides Cut/Copy/Paste/Select All for editable fields, Copy for selections, Copy/Open Link with protocol whitelist, Copy Image/Address, and Back/Forward/Reload actions with state validation.
Out of Scope Changes check ✅ Passed All changes are directly scoped to the linked issue: test expansions validate the new context-menu handler, imports add only clipboard (needed for menu actions), and logic focuses exclusively on guest webContents context-menu behavior during did-attach-webview.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/1065-browser-tab-context-menu

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
src/main/app-lifecycle/window-manager.ts (1)

195-204: 💤 Low value

Consider adding error handling for shell.openExternal.

The void shell.openExternal(linkURL) pattern ignores promise rejections. If opening the URL fails (e.g., no default browser configured, system error), the failure is silently lost. Per coding guidelines, consider at least logging the error.

Proposed fix
 		if (isExternallyOpenableLink(linkURL)) {
 			linkSection.push({
 				label: 'Open Link in Browser',
 				click: () => {
-					void shell.openExternal(linkURL);
+					shell.openExternal(linkURL).catch((err) => {
+						logger.warn(`Failed to open external link: ${err.message}`, 'Window', { url: linkURL });
+					});
 				},
 			});
 		}

As per coding guidelines: "Do not silently swallow errors. Handle expected/recoverable errors explicitly."

🤖 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/main/app-lifecycle/window-manager.ts` around lines 195 - 204, The click
handler for the "Open Link in Browser" menu item currently calls void
shell.openExternal(linkURL) which swallows promise rejections; update the click
in the linkSection (inside window-manager.ts) to handle the returned promise and
log failures (e.g., shell.openExternal(linkURL).catch(err =>
processLogger.error(...) or console.error(...))) so any error opening the URL is
not silently lost; ensure the log includes linkURL and the error object for
debugging.
🤖 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.

Nitpick comments:
In `@src/main/app-lifecycle/window-manager.ts`:
- Around line 195-204: The click handler for the "Open Link in Browser" menu
item currently calls void shell.openExternal(linkURL) which swallows promise
rejections; update the click in the linkSection (inside window-manager.ts) to
handle the returned promise and log failures (e.g.,
shell.openExternal(linkURL).catch(err => processLogger.error(...) or
console.error(...))) so any error opening the URL is not silently lost; ensure
the log includes linkURL and the error object for debugging.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 604aa6c8-32eb-4a3c-879f-b564c312be42

📥 Commits

Reviewing files that changed from the base of the PR and between d67c140 and 9674e2e.

📒 Files selected for processing (2)
  • src/__tests__/main/app-lifecycle/window-manager.test.ts
  • src/main/app-lifecycle/window-manager.ts

@greptile-apps

greptile-apps Bot commented Jun 1, 2026

Copy link
Copy Markdown

Greptile Summary

This PR wires a native right-click context menu onto the guest <webview> webContents inside Maestro browser tabs, fixing the completely dead right-click behaviour reported in #1065. All menu actions (cut/copy/paste/select-all, spellcheck, link copy/open, image copy, back/forward/reload) operate directly on the guest webContents to avoid the host-window targeting problem with Electron menu roles.

  • buildBrowserTabContextMenuTemplate assembles a per-context template with an http:/https:/mailto: allowlist guarding shell.openExternal, and separators are emitted only between non-empty sections to avoid leading or doubled dividers.
  • attachBrowserTabContextMenu is wired in the existing did-attach-webview block alongside the existing security hardening, and 11 new tests cover all menu branches including scheme-guard enforcement and navigation-state reflection.

Confidence Score: 4/5

Safe to merge; the scheme allowlist and direct guest-webContents targeting are correctly implemented, and the 11 new tests cover the critical branches.

The feature is well-scoped and the security-sensitive path (scheme validation before shell.openExternal) is correctly implemented and tested. Two minor issues were found: an unreachable guard after template construction, and a fire-and-forget shell.openExternal call that drops rejections without logging them. Neither affects correctness of the happy path.

src/main/app-lifecycle/window-manager.ts — the void shell.openExternal call at the Open Link in Browser click handler.

Important Files Changed

Filename Overview
src/main/app-lifecycle/window-manager.ts Adds EXTERNAL_LINK_PROTOCOLS allowlist, isExternallyOpenableLink guard, buildBrowserTabContextMenuTemplate, and attachBrowserTabContextMenu; wires context-menu onto guest webContents in did-attach-webview. Implementation is sound; minor issues: the template.length === 0 guard is dead code, and void shell.openExternal silently drops rejections.
src/tests/main/app-lifecycle/window-manager.test.ts Adds 11 focused tests covering handler registration, editable menus, spellcheck, link/image/selection copy, navigation state, and separator hygiene; mocks are well-scoped and the test surface matches the implementation.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[context-menu event on guest webContents] --> B{params.isEditable?}
    B -- Yes --> C[Editable path]
    C --> D{misspelledWord?}
    D -- Yes --> E[Spellcheck suggestions + Add to Dictionary + separator]
    D -- No --> F[Cut / Copy / Paste + separator + Select All]
    E --> F
    F --> Z[Menu.buildFromTemplate → popup]
    B -- No --> G[Non-editable path: collect sections]
    G --> H{linkURL?}
    H -- Yes --> I[Copy Link + isExternallyOpenableLink? Open Link in Browser]
    H -- No --> J{mediaType === image && srcURL?}
    I --> J
    J -- Yes --> K[Copy Image / Copy Image Address]
    J -- No --> L{selectionText.trim?}
    K --> L
    L -- Yes --> M[Copy]
    L -- No --> N[Always: Back / Forward / Reload]
    M --> N
    N --> O[Join sections with separators]
    O --> Z
Loading

Reviews (1): Last reviewed commit: "fix(browser-tab): add right-click contex..." | Re-trigger Greptile

Comment on lines +198 to +201
click: () => {
void shell.openExternal(linkURL);
},
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Unhandled rejection silently dropped on shell.openExternal

void discards the promise, so if shell.openExternal rejects (e.g. the OS has no default handler for mailto: or the call is rate-limited), the error disappears with no log entry or user feedback. Per the project's error-handling guideline, failures should surface to Sentry via the logger rather than vanishing. Adding a .catch that logs with logger.warn would make failures visible.

Comment on lines +243 to +245
const template = buildBrowserTabContextMenuTemplate(guest, params);
if (template.length === 0) return;
Menu.buildFromTemplate(template).popup({ window: mainWindow });

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Unreachable guard — template is never empty

The navigation section (Back / Forward / Reload) is pushed unconditionally at the end of the non-editable path, and the editable path always returns at least four items (Cut / Copy / Paste / Select All). So template.length === 0 can never be true and this early-return is dead code. Removing it avoids misleading future readers into thinking there is a scenario where no menu should be shown.

Suggested change
const template = buildBrowserTabContextMenuTemplate(guest, params);
if (template.length === 0) return;
Menu.buildFromTemplate(template).popup({ window: mainWindow });
const template = buildBrowserTabContextMenuTemplate(guest, params);
Menu.buildFromTemplate(template).popup({ window: mainWindow });

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

@chr1syy chr1syy left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Nice focused fix — the security thinking (protocol allowlist before shell.openExternal, guest-targeted actions instead of edit roles, non-deprecated navigationHistory API) and the 11 tests are solid. Confirmed rc is the correct base since did-attach-webview doesn't exist on main yet (browser-tab webview feature still lives only on rc).

One blocking issue and one optional nit, both already surfaced by the bots — I'm raising the first because it conflicts with the codebase's own pattern + CLAUDE.md's error-handling guidance.

Blocking: silent rejection from shell.openExternal

src/main/app-lifecycle/window-manager.ts:198-200:

click: () => {
    void shell.openExternal(linkURL);
},

void drops the promise on the floor. If shell.openExternal rejects (no default browser, Launch Services failure, etc.) the user clicks "Open Link in Browser" and gets nothing — no toast, no log, no Sentry. CLAUDE.md's Error Handling & Sentry section is explicit: "DO NOT silently swallow errors."

The codebase already has the right pattern for this exact API in src/main/ipc/handlers/system.ts:230-240:

try {
    await shell.openExternal(url);
} catch (err) {
    const message = err instanceof Error ? err.message : String(err);
    if (message.includes('Launch Services') || message.includes('No application')) {
        logger.warn(`No application found to open "${url}"`, 'Shell', { error: message });
        return;
    }
    throw err;
}

A minimal fix here would be:

click: () => {
    shell.openExternal(linkURL).catch((err) => {
        logger.warn(`Failed to open external link: ${err instanceof Error ? err.message : String(err)}`, 'Window', { url: linkURL });
    });
},

Worth a one-line test ("logs a warning when shell.openExternal rejects") so future refactors don't quietly regress this back to void.

Optional nit: dead-code guard

src/main/app-lifecycle/window-manager.ts:244:

if (template.length === 0) return;

Greptile flagged this as unreachable, and it is — the non-editable path unconditionally pushes the navigation section (Back/Forward/Reload), and the editable path always returns ≥5 items. I'd lean toward keeping it as a defensive belt-and-suspenders at the popup boundary (cheap insurance if the section logic ever changes), but happy either way.

Out of scope / observations

  • Author's note that this also helps the paste case from #1063 is correct — paste lands in the editable path, so #1063 should be fixed transitively. Worth mentioning in the closing comment when this merges.
  • Targeting rc is correct; this can't land on main until the browser-tab webview feature does.

Otherwise LGTM once the .catch() lands.

Address review feedback on #1066:
- shell.openExternal rejections were silently dropped; now logged via logger.warn
- document the template.length === 0 popup guard as intentional defense
…ntext-menu

# Conflicts:
#	src/__tests__/main/app-lifecycle/window-manager.test.ts
#	src/main/app-lifecycle/window-manager.ts
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