Skip to content

feat: move plugin loading to dedicated hidden BrowserWindow (Phase 1)#9889

Open
jackkav wants to merge 30 commits intoKong:developfrom
jackkav:chore/move-plugins-to-hidden-window
Open

feat: move plugin loading to dedicated hidden BrowserWindow (Phase 1)#9889
jackkav wants to merge 30 commits intoKong:developfrom
jackkav:chore/move-plugins-to-hidden-window

Conversation

@jackkav
Copy link
Copy Markdown
Contributor

@jackkav jackkav commented May 4, 2026

Summary

Moves all plugin loading and execution out of the UI renderer into a dedicated hidden `BrowserWindow` with `nodeIntegration: true`. The renderer communicates with the plugin window exclusively over IPC.

What changed

  • New hidden plugin window (`src/plugin-window.html` + `src/entry.plugin-window.ts`) — loads and executes all plugin code with Node.js access
  • Preload for plugin window (`src/entry.plugin-window-preload.ts`) — provides `window.app.getPath` via `ipcRenderer.sendSync` so plugins can resolve `userData`
  • Main process manager (`src/main/plugin-window.ts`) — creates the window; routes IPC via a pending-request map (UUID correlation, 10s ready timeout, sender validation, crash drain on close)
  • IPC database proxy (`src/main/database.plugin-window.ts`) — plugin window reads from the main process NeDB connection via IPC instead of opening a second connection; services are initialized before the window signals readiness
  • Serializable type boundary (`src/plugins/bridge-types.ts`) — all data crossing IPC is serializable; context modules run in the plugin window, not the renderer
  • Renderer bridge (`window.main.plugins.*`) — all plugin execution replaced with bridge calls
  • Build — two new esbuild entry points added; dev `buildCount` threshold updated from 3 → 6

Architecture

Renderer
  window.main.plugins.executeAction({ type, pluginName, label, projectId, domainData })
    └─ ipcRenderer.invoke('plugins.executeAction', args)
         └─ ipcMain.handle → invokeInPluginWindow('executeAction', args)
              └─ pluginWindow.webContents.send('plugin-invoke', { id, method, args })
                   └─ entry.plugin-window.ts: finds action, builds context, calls action(ctx, domainData)
                        └─ ipcRenderer.send('plugin-invoke-result', { id, result })
                             └─ main pendingRequests.get(id).resolve(result)
                                  └─ renderer Promise resolves

What's bridged (Phase 1 complete)

Feature Status
Theme listing
Plugin listing / reload
Bundle plugin listing (`getBundlePlugins`)
Request / RequestGroup / Workspace / Document actions (list + execute)
Template tag listing (`getTemplateTags`)
Template tag action execution (`runTemplateTagAction`)
Elevated plugin main actions (`executePluginMainAction`)
Request hooks / Response hooks
Sandbox hardening ⏳ Phase 2 — `contextIsolation: true` once API surface is stable

Hook bridging design

Request and response hooks mutate their context objects by reference, which only works within a single process. To cross the IPC boundary the plugin window receives a serialized copy of the request/response, runs all registered hooks against it, and returns the fully mutated object — matching the same pattern used by pre-request scripts.

The built-in default-headers hook runs in the renderer without any IPC (it has no plugin dependency). A `hasRequestHooks`/`hasResponseHooks` result is cached in the main process after the first check and cleared on `reloadPlugins`, so requests incur zero plugin-window round-trips when no user plugins have hooks registered.

Non-renderer process handling

`_applyRequestPluginHooks` and `_applyResponsePluginHooks` guard on `process.type !== 'renderer'` before using the IPC bridge. In the Electron main process (OAuth token exchange) and in the inso CLI (Node.js), hooks are applied directly via `plugins.getRequestHooks()` / `plugins.getResponseHooks()` without any IPC.

Intentionally remaining in renderer

  • `applyColorScheme` / `getColorScheme` — CSS/DOM manipulation, not plugin execution
  • `createPlugin` — filesystem plugin scaffolding, not plugin execution

Security fixes (from review)

  • `plugin-window-ready` and `plugin-invoke-result` validate `event.sender === pluginWindow.webContents`
  • Persistent `ipcMain.on` listener (registered once) replaces `ipcMain.once` — plugin window re-arms correctly after reload
  • `waitForReady()` has a 10s timeout and listens to `did-fail-load`
  • Request IDs use `crypto.randomUUID()` instead of a guessable counter
  • Non-Error plugin hook throws are wrapped into `Error` instances before attaching `.plugin` metadata

Playwright fixture

The plugin window is created after the main window's `did-finish-load` event, so Playwright's `firstWindow()` always returns the main app window. No fixture fallback or polling is needed.

Test plan

  • All 1900 unit tests pass (`npm test -w packages/insomnia`)
  • TypeScript: no errors (`tsc --noEmit`)
  • E2E: `plugin-bridge.test.ts` — writes a test plugin, reloads via bridge, verifies the request action appears in the dropdown (16.7s, passes)
  • Manual: themes settings panel — themes should populate
  • Manual: reload plugins via keyboard shortcut — should complete without error
  • Manual: plugin settings tab — installed plugins should list
  • Manual: right-click a request — plugin actions should appear and execute
  • Manual: cloud credentials settings — Azure auth flow should work

🤖 Generated with Claude Code

closes INS-2466

@jackkav jackkav changed the title Chore:move plugins to hidden window feat: move plugin loading to dedicated hidden BrowserWindow (Phase 1) May 4, 2026
@jackkav jackkav force-pushed the chore/move-plugins-to-hidden-window branch from 2223c13 to 6efa211 Compare May 4, 2026 07:25
@jackkav jackkav marked this pull request as ready for review May 4, 2026 07:25
Copilot AI review requested due to automatic review settings May 4, 2026 07:25
Comment thread packages/insomnia/src/main/plugin-window.ts
Comment thread packages/insomnia/src/main/plugin-window.ts
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR moves plugin enumeration/loading for the UI renderer behind a dedicated hidden Electron BrowserWindow (with Node integration) and routes renderer-side plugin interactions through a typed IPC bridge, as the first phase toward renderer hardening.

Changes:

  • Added a hidden “plugin window” (HTML + entry + preload) and a main-process manager that proxies plugin API calls over IPC.
  • Updated UI codepaths (themes, plugin reload, settings plugins list) to call window.main.plugins.* instead of importing the plugin runtime directly.
  • Introduced serializable bridge types plus new unit tests covering plugin behaviors and plugin hook handling; updated build/esbuild entrypoints to ship the new window scripts.

Reviewed changes

Copilot reviewed 24 out of 24 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
packages/insomnia/src/ui/renderer-listeners.ts Route plugin reload through window.main.plugins.reloadPlugins()
packages/insomnia/src/ui/hooks/use-global-keyboard-shortcuts.ts Route plugin reload shortcut through the plugins bridge
packages/insomnia/src/ui/hooks/theme.ts Fetch themes via plugins bridge instead of direct plugin API call
packages/insomnia/src/ui/components/settings/plugins.tsx Use SerializablePlugin and fetch plugin list via bridge
packages/insomnia/src/root.tsx Reload plugins via bridge during theme install/apply flow
packages/insomnia/src/plugins/index.ts Add test helper to control in-memory plugin list
packages/insomnia/src/plugins/bridge-types.ts Define serializable plugin/theme/action metadata boundary + bridge API
packages/insomnia/src/plugins/tests/themes.test.ts Add baseline tests for built-in theme list + plugin theme merge behavior
packages/insomnia/src/plugins/tests/index.test.ts Add broad unit tests for plugin exports (hooks/actions/tags/themes) and errors
packages/insomnia/src/plugin-window.html Add HTML host for hidden plugin window script
packages/insomnia/src/network/network.ts Export internal hook-application helpers for unit testing
packages/insomnia/src/network/tests/plugin-hooks.test.ts Add unit tests around request/response plugin hook behavior
packages/insomnia/src/main/window-utils.ts Create plugin window during window initialization; export destroy helper
packages/insomnia/src/main/plugin-window.ts Implement plugin window lifecycle + IPC proxying + pending request map
packages/insomnia/src/main/ipc/main.ts Add plugins bridge type to main IPC surface; register plugin IPC handlers
packages/insomnia/src/main/ipc/electron.ts Extend IPC channel type unions for new plugin channels
packages/insomnia/src/entry.preload.ts Expose window.main.plugins.* bridge methods in renderer preload
packages/insomnia/src/entry.plugin-window.ts Implement plugin-window-side dispatcher and result serialization
packages/insomnia/src/entry.plugin-window-preload.ts Provide minimal window.app shim to support plugin path resolution
packages/insomnia/scripts/build.ts Copy plugin-window HTML into production build output
packages/insomnia/esbuild.entrypoints.ts Add esbuild entrypoints/watchers for plugin window + preload bundles
packages/insomnia/PLUGIN_SYSTEM_POC.md Add architecture/plan documentation for the plugin system POC
packages/insomnia-smoke-test/playwright.config.ts Change local reporter configuration
AGENTS.md Add guidance on minimizing command output and cx navigation

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread packages/insomnia/src/main/plugin-window.ts Outdated
Comment thread packages/insomnia/src/main/plugin-window.ts Outdated
Comment thread packages/insomnia/src/main/plugin-window.ts
Comment thread packages/insomnia/src/network/__tests__/plugin-hooks.test.ts Outdated
Comment thread packages/insomnia/PLUGIN_SYSTEM_POC.md Outdated
Comment thread packages/insomnia/src/main/plugin-window.ts Outdated
Comment thread packages/insomnia/src/main/plugin-window.ts
@jackkav jackkav requested review from a team and ZxBing0066 May 4, 2026 19:11
@jackkav jackkav force-pushed the chore/move-plugins-to-hidden-window branch from d01ff18 to 2835bce Compare May 4, 2026 19:11
@jackkav jackkav requested a review from gatzjames May 4, 2026 19:59
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.

clean 👍🏻

Comment thread packages/insomnia/src/main/ipc/electron.ts Outdated
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.

I'd prefer to keep this in Confluence

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Its easier for the llm to reference here. I'll get rid of it once the work is done though and can add it to confluence for posterity.

ryan-willis
ryan-willis previously approved these changes May 5, 2026
@ZxBing0066
Copy link
Copy Markdown
Member

ZxBing0066 commented May 6, 2026

Some plugin APIs that rely on the main window (e.g., showAlert, showWrapper) will trigger errors following this change. These APIs may require bridging to the main window to maintain functionality.

And if the previous plugins depend on some of the window APIs, like height and width, they also break. As well as if the plugins are trying to append DOMs. Is this acceptable?

@jackkav
Copy link
Copy Markdown
Contributor Author

jackkav commented May 6, 2026

feedback concern:
chome web apis that show UI will need bridges, or be excluded from supported features.
await window.showOpenFilePicker();

templating now runs in main, which means templating has access to nodejs again, temporarily.

injecting custom UI, in to the DOM won't work anymore, because it was using ambiant renderer libraries like React. We could add these to the plugin context and build bridges but this would be securable.

jackkav and others added 12 commits May 6, 2026 12:13
- Set npm loglevel=warn to suppress install/run progress noise
- Switch Playwright local reporter from list to dot (less output per test, CI unchanged)
- Add scripts/setup.sh for one-time local git config (compact log, short status)
- Document setup script in AGENTS.md

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
- Revert loglevel=warn from .npmrc — too broad, suppresses CI output
- Remove shell alias suggestions from setup.sh — out of scope for a repo script

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
Removes setup.sh in favour of explicit quiet-command guidance that
benefits all agents (Claude, Copilot, Codex) without requiring a
one-time setup step.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
cx gives agents a cost ladder (overview → symbols → definition → read)
that reduces file reads for all agents that read AGENTS.md — complementary
to CodeGraph which is Claude Code-specific.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
All plugin API calls (getThemes, getPlugins, getActivePlugins, reloadPlugins,
getRequestActions, getRequestGroupActions, getWorkspaceActions, getDocumentActions)
are now routed through a dedicated hidden BrowserWindow with nodeIntegration:true
instead of running directly in the renderer.

IPC relay: renderer → ipcMain.handle → plugin window webContents → ipcRenderer.send
back to main → resolve renderer promise via pending-request map with 30s timeout.

Renderer-side callers updated to use window.main.plugins.* bridge.
Two new esbuild entry points added (plugin-window, plugin-window-preload).
Dev build threshold updated from 3 to 6 to account for all contexts.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
Add executeAction IPC method so all four plugin action dropdowns (request,
requestGroup, workspace, document) dispatch through the plugin window
instead of running context modules directly in the renderer.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
jackkav and others added 18 commits May 6, 2026 12:13
…bridge

Bridge getTemplateTags() and runTemplateTagAction() so code-editor,
one-line-editor, and tag-editor no longer import from plugins/index
or plugins/context/store in the renderer.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
…rowserWindow

Bridge template tags (getTemplateTags, runTemplateTagAction), bundle plugin
listing (getBundlePlugins), and elevated plugin actions (executePluginMainAction)
so no renderer code calls plugin index or context modules directly for execution.

Remaining renderer plugin imports are intentional: applyColorScheme/getColorScheme
(DOM utilities) and createPlugin (filesystem scaffolding), neither of which is
plugin execution.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
- Create database.plugin-window.ts IPC proxy so the plugin window
  reads from the main process NeDB connection instead of opening a
  second one
- Initialize database + services in entry.plugin-window.ts before
  sending plugin-window-ready, fixing the silent "Service not
  initialized" crash that was masked by unawaited promises
- Add isMainWindow fallback to the page fixture so firstWindow()
  racing to return the hidden plugin window doesn't break other tests
- Add plugin-bridge.test.ts: E2E test that writes a requestAction
  plugin, reloads via the bridge, and verifies the action appears in
  the request dropdown

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
Moves requestHooks and responseHooks execution into the hidden plugin
window via the IPC bridge. The default-headers built-in runs in the
renderer (no IPC). A cached hasRequestHooks/hasResponseHooks check in
the main process avoids any plugin window round-trip per request when no
user plugins have hooks registered.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
_applyRequestPluginHooks and _applyResponsePluginHooks now use
window.main.plugins.* IPC only in the Electron renderer. In the main
process (OAuth2 token exchange via get-token.ts) and Node.js CLI
(insomnia-inso), they fall back to loading plugins directly via
plugins.getRequestHooks/getResponseHooks. This fixes:

- inso CLI: "window is not defined" in all run collection/test commands
- Electron: OAuth2 token exchange failing with "no access token provided"

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
Playwright's firstWindow() was racing with the plugin window and
sometimes returning it instead of the main app window. By deferring
createPluginWindow() to did-finish-load on the main window, the plugin
window is guaranteed to not exist yet when firstWindow() resolves.

Removes the findMainWindow polling fallback from the test fixture.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
…gin window to main renderer

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
@jackkav jackkav force-pushed the chore/move-plugins-to-hidden-window branch from f985e6b to d472861 Compare May 6, 2026 10:13
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.

4 participants