Files
paseo/packages/desktop/src/preload.ts
T
Mohamed BoudraandGitHub 04d1ebdce0 Keep browser input from submitting Paseo prompts (#1982)
* fix(desktop): keep browser input out of the composer

Unhandled webview keys could be redispatched into the active host window, allowing agent Enter to submit a draft prompt. Give pages first refusal for ordinary shortcuts and contain automation at the guest boundary.

* fix(desktop): tighten browser shortcut validation

* fix(desktop): respect browser shortcut ownership

* fix(desktop): retain browser keys across windows

* fix(desktop): scope browser webviews by host

* Reshape browser shortcut ownership

Use the browser webview registry as the single guest identity authority, scope browser operations to their host window, publish chord continuations only while pending, and restore focus to the originating browser after command center dismissal.

* Fix host-scoped browser shortcut follow-ups

* Fix browser keyboard review follow-ups

* Fix browser keyboard lifecycle regressions

* Fix browser shortcut review regressions

* Fix desktop browser review regressions

* Fix browser shortcut frame regressions

* Fix rebase integration regressions

* Preserve browser-native shortcut ownership

* Isolate browser shortcuts and automation by context
2026-07-17 14:29:19 +08:00

121 lines
5.4 KiB
TypeScript

import { contextBridge, ipcRenderer, webUtils } from "electron";
import type { BrowserKeyboardPolicy } from "./features/browser-keyboard/index.js";
// This preload runs in Electron's sandbox and is tsc-compiled (not bundled), so it MUST
// NOT emit any runtime module load other than "electron" — a require() of a local or
// third-party module throws and aborts the preload before exposeInMainWorld runs, leaving
// window.paseoDesktop undefined (the 0.1.108 regression, #2103). Keep this literal in sync
// with PASEO_BROWSER_PROFILE_PARTITION in features/browser-profile.ts; preload-sandbox.test.ts
// guards both the no-local-import rule and this drift. Type-only imports are fine (erased at emit).
const PASEO_BROWSER_PROFILE_PARTITION = "persist:paseo-browser";
type EventHandler = (payload: unknown) => void;
interface AttachedBrowserRegistration {
browserId: string;
workspaceId: string;
webContentsId: number;
}
contextBridge.exposeInMainWorld("paseoDesktop", {
platform: process.platform,
invoke: (command: string, args?: Record<string, unknown>) =>
ipcRenderer.invoke("paseo:invoke", command, args),
getPendingOpenProject: () =>
ipcRenderer.invoke("paseo:get-pending-open-project") as Promise<string | null>,
events: {
on: (event: string, handler: EventHandler): Promise<() => void> => {
const listener = (_ipcEvent: Electron.IpcRendererEvent, payload: unknown) => {
handler(payload);
};
ipcRenderer.on(`paseo:event:${event}`, listener);
return Promise.resolve(() => {
ipcRenderer.removeListener(`paseo:event:${event}`, listener);
});
},
},
window: {
openNew: (options?: { pendingOpenProjectPath?: string | null }) =>
ipcRenderer.invoke("paseo:window:openNew", options),
getCurrentWindow: () => ({
toggleMaximize: () => ipcRenderer.invoke("paseo:window:toggleMaximize"),
setFullscreen: (fullscreen: boolean) =>
ipcRenderer.invoke("paseo:window:setFullscreen", fullscreen),
isFullscreen: () => ipcRenderer.invoke("paseo:window:isFullscreen"),
updateWindowControls: (update: {
height?: number;
backgroundColor?: string;
foregroundColor?: string;
trafficLightOffsetY?: number;
}) => ipcRenderer.invoke("paseo:window:updateWindowControls", update),
onResized: (handler: EventHandler): (() => void) => {
const listener = (_ipcEvent: Electron.IpcRendererEvent, payload: unknown) => {
handler(payload);
};
ipcRenderer.on("paseo:window:resized", listener);
return () => {
ipcRenderer.removeListener("paseo:window:resized", listener);
};
},
setBadgeCount: (count?: number) => ipcRenderer.invoke("paseo:window:setBadgeCount", count),
}),
},
dialog: {
ask: (message: string, options?: Record<string, unknown>) =>
ipcRenderer.invoke("paseo:dialog:ask", message, options),
askWithCheckbox: (message: string, options: Record<string, unknown>) =>
ipcRenderer.invoke("paseo:dialog:askWithCheckbox", message, options),
open: (options?: Record<string, unknown>) => ipcRenderer.invoke("paseo:dialog:open", options),
},
notification: {
isSupported: () => ipcRenderer.invoke("paseo:notification:isSupported"),
sendNotification: (payload: { title: string; body?: string; data?: Record<string, unknown> }) =>
ipcRenderer.invoke("paseo:notification:send", payload),
},
opener: {
openUrl: (url: string) => ipcRenderer.invoke("paseo:opener:openUrl", url),
},
editor: {
listTargets: () => ipcRenderer.invoke("paseo:editor:listTargets"),
openTarget: (input: {
editorId: string;
workspacePath: string;
filePath?: string;
line?: number;
column?: number;
}) => ipcRenderer.invoke("paseo:editor:openTarget", input),
},
webUtils: {
getPathForFile: (file: File) => webUtils.getPathForFile(file),
},
menu: {
showContextMenu: (input?: Record<string, unknown>) =>
ipcRenderer.invoke("paseo:menu:showContextMenu", input),
setCapturingShortcut: (capturing: boolean) =>
ipcRenderer.invoke("paseo:menu:set-capturing-shortcut", capturing),
},
browser: {
setShortcutPolicy: (input: BrowserKeyboardPolicy) =>
ipcRenderer.invoke("paseo:browser:set-shortcut-policy", input),
profilePartition: PASEO_BROWSER_PROFILE_PARTITION,
registerAttachedBrowser: (input: AttachedBrowserRegistration) =>
ipcRenderer.invoke("paseo:browser:register-attached", input),
unregisterWorkspaceBrowser: (browserId: string) =>
ipcRenderer.invoke("paseo:browser:unregister-workspace-browser", browserId),
setWorkspaceActiveBrowser: (input: { workspaceId: string; browserId: string | null }) =>
ipcRenderer.invoke("paseo:browser:set-workspace-active-browser", input),
openDevTools: (browserId: string) =>
ipcRenderer.invoke("paseo:browser:open-devtools", browserId),
clearProfile: (legacyBrowserIds: string[]) =>
ipcRenderer.invoke("paseo:browser:clear-profile", legacyBrowserIds),
executeAutomationCommand: (request: Record<string, unknown>) =>
ipcRenderer.invoke("paseo:browser:execute-automation-command", request),
captureElement: (
browserId: string,
rect: { x: number; y: number; width: number; height: number },
) => ipcRenderer.invoke("paseo:browser:capture-element", browserId, rect),
copyElement: (payload: { text?: string; imageDataUrl?: string }) =>
ipcRenderer.invoke("paseo:browser:copy-element", payload),
},
});