Skip to content

feat(otter): Chrome extensions in the browser preview - #15

Merged
ckafrouni merged 3 commits into
mainfrom
otter/browser-extensions
Oct 3, 2026
Merged

ckafrouni merged 3 commits into
mainfrom
otter/browser-extensions

Conversation

@ckafrouni

@ckafrouni ckafrouni commented Oct 3, 2026 •

Copy link
Copy Markdown
Collaborator

The browser preview couldn't run Chrome extensions, so password managers like 1Password weren't available when signing in to sites in a thread's browser. This brings over Otter Mail's extension support and gives the preview's toolbar Otter Mail's look, keeping everything the preview already does (agent control, annotate, screenshots and recording, picture-in-picture, profiles, cookie import).

How it works

  • Installing. The Chrome Web Store's "Add to" button installs extensions, through electron-chrome-web-store (MIT), behind a native "Add …?" confirmation that says what the extension can read. Extensions update on their own.
  • What Electron lacks. Electron runs an extension's worker, pages and content scripts, but has none of Chrome's toolbar, tab strip or windows. A session preload (preview-extensions-preload.ts) adds them to the extension's chrome and browser objects: chrome.action, the rest of chrome.tabs, chrome.windows, contextMenus, notifications, webNavigation, commands, permissions, and simple downloads and privacy. preview/extensions/PreviewExtensions.ts answers those calls in the main process; it's a port of Otter Mail's MIT layer.
  • Profiles. Every persistent preview partition (one per environment and browser profile) loads the same extensions and keeps its own extension data, as Chrome profiles do. One partition checks for updates and shares them with the rest. Incognito partitions get no extensions.
  • No context isolation. Preview pages run with contextIsolation=false for the element picker, and there contextBridge throws. The preload puts the Web Store's API on the page's window instead, and installs its own APIs directly on extension pages that load in a tab.
  • Toolbar. Back, forward and reload sit in one pill. The address is a centered pill showing the host and path, and the full URL while editing. Pinned extensions and the Extensions (puzzle) menu come next, then the tools pill (annotate, screenshot, picture-in-picture), then ⋯. The menu runs, pins and manages extensions. A popup opens in a borderless window under its button, sized by its page (Chrome's 25×25 to 800×600), and closes on blur.
  • Settings › Browser Extensions (desktop only): a card per extension to turn it on or off, see its site access and permissions, pin it, open its options, or remove it. It also has search, a developer mode that loads an unpacked extension, and a switch to hide the Extensions button. From Settings, the Chrome Web Store opens as a tab beside the last open thread.
  • Pages extensions open. Web pages go to a preview tab beside the thread showing. An extension's own pages (options, windows it asks for) open in a window of their own. Extensions' context menu items join the page's right-click menu.
  • Google passkeys. Given a User-Agent that names Electron, Google serves a lite sign-in page (flowName=WebLiteSignIn) that never asks for passkeys until you click "Try another way". Requests to accounts.google.com now drop the Electron/x token, so Google serves Chrome's page (GlifWebSignIn). That page asks for passkeys as it loads (navigator.credentials.get, mediation: "conditional"), and 1Password offers "Use passkey" there. Only those requests change: rewriting the session's whole User-Agent still breaks Cloudflare Turnstile with error 600010 ([Bug]: Cloudflare Turnstile challenge loops indefinitely in the built-in browser preview pingdotgg/t3code#5002). I re-checked this on Electron 44: the stripped session failed on dash.cloudflare.com, while the Google-only rewrite passed it and still got Google's full page with the passkey request.
  • Not supported: native messaging, so 1Password can't link to the 1Password app (no Touch ID unlock).

Upstream files change only where the feature hooks in: getPreviewConfig enables a partition's extensions, DesktopWindow adds the menu items, preload.ts exposes the bridge, PreviewChromeRow is restyled, the overlay carries the tab's webContents id, and Settings gets the new page.

Screenshots

Before: the current release's row of ghost buttons with a borderless address field.

After: the toolbar's right end, with 1Password pinned, the Extensions menu, and the tools pill
Toolbar

After: Settings › Browser Extensions
Settings

Verified

  • In a dev build: 1Password installed from the Chrome Web Store (in the Web Store window from Settings) and pinned, and it runs in the preview's toolbar.

  • tsc for desktop, web and contracts. Lint on every changed file. Tests: 83 web files (preview, settings, sidebar, preview state) and 12 desktop files (IPC, windows, browser session) pass. The third-party license check passes with the new notice.

  • User agent, in a throwaway Electron 44 with each setup side by side:

    • Native: Google served WebLiteSignIn and made no passkey request.
    • Whole session stripped: GlifWebSignIn and a conditional passkey request, but Turnstile error 600010 on dash.cloudflare.com.
    • Google-only (this PR): GlifWebSignIn, the conditional passkey request, no Turnstile error. An email went on to Google's normal next step, with no "browser may not be secure".

Not verified yet: "Add to Otter Code" from a Web Store page inside a preview tab, which relies on the context-isolation shim.

Work by Claude Opus 5.5 in Claude Code.

🤖 Generated with Claude Code

ckafrouni and others added 3 commits October 4, 2026 00:04
The preview's tabs can now run Chrome extensions from the Chrome Web Store,
such as 1Password, as Otter Mail's browser does. Electron runs an extension's
worker, pages and content scripts; this adds what it lacks: the Web Store's
install button (electron-chrome-web-store, MIT), the toolbar's buttons and
popups, and chrome.action, the rest of chrome.tabs, chrome.windows,
contextMenus, notifications, webNavigation and friends, answered by the main
process through a session preload.

Every persistent preview partition (one per environment and browser
profile) loads the same extensions and keeps its own extension data, as
Chrome profiles do. Incognito tabs get none.

The preview's toolbar takes Otter Mail's look: pills for navigation, the
address, the extensions and the tools. Settings › Browser Extensions lists,
switches, inspects and removes them.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Given a User-Agent that names Electron, Google serves a lite sign-in page
(flowName=WebLiteSignIn) that never asks for passkeys, such as 1Password's,
until you go looking for them. Requests to accounts.google.com now drop the
Electron token, so Google serves Chrome's page (GlifWebSignIn), which offers
passkeys as it loads.

Only those requests change: rewriting the session's whole User-Agent,
Electron token removed, still makes Cloudflare Turnstile fail with 600010
(pingdotgg#5002), checked again on Electron 44.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@ckafrouni
ckafrouni merged commit 014292d into main Oct 3, 2026
18 of 19 checks passed
@ckafrouni
ckafrouni deleted the otter/browser-extensions branch October 3, 2026 22:44
Laurin-Notemann added a commit that referenced this pull request Oct 4, 2026
* feat(otter): Chrome extensions in the browser preview (#15)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Laurin-Notemann added a commit that referenced this pull request Oct 5, 2026
* feat(otter): Chrome extensions in the browser preview (#15)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Laurin-Notemann added a commit that referenced this pull request Oct 5, 2026
* feat(otter): Chrome extensions in the browser preview (#15)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Laurin-Notemann added a commit that referenced this pull request Oct 5, 2026
* feat(otter): Chrome extensions in the browser preview (#15)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Laurin-Notemann added a commit that referenced this pull request Oct 6, 2026
* feat(otter): Chrome extensions in the browser preview (#15)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Laurin-Notemann added a commit that referenced this pull request Oct 7, 2026
* feat(otter): Chrome extensions in the browser preview (#15)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Laurin-Notemann added a commit that referenced this pull request Oct 8, 2026
* feat(otter): Chrome extensions in the browser preview (#15)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.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.

1 participant