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
This commit is contained in:
@@ -51,15 +51,17 @@ import {
|
||||
BROWSER_NEW_TAB_REQUEST_EVENT,
|
||||
decideBrowserWindowOpenRequest,
|
||||
getPaseoBrowserIdForWebContents,
|
||||
getPaseoBrowserWebContents,
|
||||
getPaseoBrowserWebContentsForHostWindow,
|
||||
getPaseoBrowserWebviewRegistry,
|
||||
listRegisteredPaseoBrowserIds,
|
||||
isPaseoBrowserWebviewAttach,
|
||||
preparePaseoBrowserWebContents,
|
||||
PendingBrowserWindowOpenRequests,
|
||||
registerBrowserWebviewNavigationGuards,
|
||||
unregisterPaseoBrowser,
|
||||
unregisterPaseoBrowserFromHost,
|
||||
registerAttachedPaseoBrowser,
|
||||
setWorkspaceActivePaseoBrowserId,
|
||||
unregisterPaseoBrowserHost,
|
||||
} from "./features/browser-webviews/index.js";
|
||||
import {
|
||||
clearPaseoBrowserProfile,
|
||||
@@ -85,6 +87,7 @@ import {
|
||||
import { runDesktopStartup } from "./desktop-startup.js";
|
||||
import { autoUpdateInstalledSkills } from "./integrations/skills/index.js";
|
||||
import { registerBrowserAutomationIpc } from "./features/browser-automation/ipc.js";
|
||||
import { BrowserKeyboard } from "./features/browser-keyboard/index.js";
|
||||
|
||||
const DEV_SERVER_URL = process.env.EXPO_DEV_URL ?? "http://localhost:8081";
|
||||
const APP_SCHEME = "paseo";
|
||||
@@ -93,35 +96,6 @@ const DISABLE_SINGLE_INSTANCE_LOCK = process.env.PASEO_DISABLE_SINGLE_INSTANCE_L
|
||||
const APP_NAME = process.env.PASEO_TEST_APP_NAME?.trim() || "Paseo";
|
||||
const pendingBrowserWindowOpenRequests = new PendingBrowserWindowOpenRequests();
|
||||
|
||||
const BROWSER_SHORTCUT_EVENT = "paseo:event:browser-shortcut";
|
||||
const BROWSER_FORWARDED_KEY_EVENT = "paseo:event:browser-forwarded-key";
|
||||
|
||||
const FORWARDED_PASEO_SHORTCUT_KEYS = new Set([
|
||||
"b",
|
||||
"e",
|
||||
"w",
|
||||
"t",
|
||||
"k",
|
||||
"o",
|
||||
"/",
|
||||
"\\",
|
||||
",",
|
||||
".",
|
||||
"1",
|
||||
"2",
|
||||
"3",
|
||||
"4",
|
||||
"5",
|
||||
"6",
|
||||
"7",
|
||||
"8",
|
||||
"9",
|
||||
"enter",
|
||||
"arrowleft",
|
||||
"arrowright",
|
||||
"arrowup",
|
||||
"arrowdown",
|
||||
]);
|
||||
app.setName(APP_NAME);
|
||||
|
||||
interface AttachedBrowserInput {
|
||||
@@ -169,29 +143,8 @@ function readActiveBrowserInput(
|
||||
return { workspaceId: record.workspaceId.trim(), browserId: browserId || null };
|
||||
}
|
||||
|
||||
function isBrowserRefreshInput(input: Electron.Input): boolean {
|
||||
if (input.type !== "keyDown" || input.alt || input.shift) {
|
||||
return false;
|
||||
}
|
||||
return (input.meta || input.control) && input.key.toLowerCase() === "r";
|
||||
}
|
||||
|
||||
function isBrowserLocationInput(input: Electron.Input): boolean {
|
||||
if (input.type !== "keyDown" || input.alt || input.shift) {
|
||||
return false;
|
||||
}
|
||||
return (input.meta || input.control) && input.key.toLowerCase() === "l";
|
||||
}
|
||||
|
||||
function isForwardablePaseoShortcutInput(input: Electron.Input): boolean {
|
||||
if (input.type !== "keyDown") {
|
||||
return false;
|
||||
}
|
||||
if (!input.meta && !input.control) {
|
||||
return false;
|
||||
}
|
||||
return FORWARDED_PASEO_SHORTCUT_KEYS.has(input.key.toLowerCase());
|
||||
}
|
||||
const browserKeyboard = new BrowserKeyboard(getPaseoBrowserWebviewRegistry());
|
||||
browserKeyboard.registerIpc();
|
||||
|
||||
function showBrowserWebviewContextMenu(
|
||||
win: BrowserWindow,
|
||||
@@ -432,6 +385,11 @@ ipcMain.handle("paseo:browser:register-attached", (event, rawInput: unknown) =>
|
||||
if (!registered) {
|
||||
throw new Error("Attached browser registration was rejected");
|
||||
}
|
||||
const guest = webContents.fromId(input.webContentsId);
|
||||
if (!guest) {
|
||||
throw new Error("Attached browser guest disappeared after registration");
|
||||
}
|
||||
browserKeyboard.attach({ contents: guest, hostContents: event.sender });
|
||||
log.info("[browser-webview] registered", {
|
||||
browserId: input.browserId,
|
||||
webContentsId: input.webContentsId,
|
||||
@@ -445,12 +403,18 @@ ipcMain.handle("paseo:browser:register-attached", (event, rawInput: unknown) =>
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle("paseo:browser:unregister-workspace-browser", async (_event, browserId: unknown) => {
|
||||
ipcMain.handle("paseo:browser:unregister-workspace-browser", async (event, browserId: unknown) => {
|
||||
if (typeof browserId === "string" && browserId.trim().length > 0) {
|
||||
const normalizedBrowserId = browserId.trim();
|
||||
unregisterPaseoBrowser(normalizedBrowserId);
|
||||
const hasOtherHost = getPaseoBrowserWebviewRegistry().hasBrowserInOtherHostWindow(
|
||||
event.sender.id,
|
||||
normalizedBrowserId,
|
||||
);
|
||||
unregisterPaseoBrowserFromHost(event.sender.id, normalizedBrowserId);
|
||||
// COMPAT(browserProfile): added in v0.1.108; remove after 2027-01-15.
|
||||
const legacyProfile = getLegacyPaseoBrowserProfileSession(session, normalizedBrowserId);
|
||||
const legacyProfile = hasOtherHost
|
||||
? null
|
||||
: getLegacyPaseoBrowserProfileSession(session, normalizedBrowserId);
|
||||
if (legacyProfile) {
|
||||
try {
|
||||
await clearPaseoBrowserProfile({
|
||||
@@ -468,14 +432,14 @@ ipcMain.handle("paseo:browser:unregister-workspace-browser", async (_event, brow
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle("paseo:browser:set-workspace-active-browser", (_event, rawInput: unknown) => {
|
||||
ipcMain.handle("paseo:browser:set-workspace-active-browser", (event, rawInput: unknown) => {
|
||||
const input = readActiveBrowserInput(rawInput);
|
||||
if (input) {
|
||||
setWorkspaceActivePaseoBrowserId(input);
|
||||
setWorkspaceActivePaseoBrowserId({ ...input, hostWebContentsId: event.sender.id });
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle("paseo:browser:open-devtools", (_event, browserId: unknown) => {
|
||||
ipcMain.handle("paseo:browser:open-devtools", (event, browserId: unknown) => {
|
||||
if (typeof browserId !== "string" || browserId.trim().length === 0) {
|
||||
const result = {
|
||||
ok: false,
|
||||
@@ -486,7 +450,7 @@ ipcMain.handle("paseo:browser:open-devtools", (_event, browserId: unknown) => {
|
||||
log.warn("[browser-devtools] open-devtools.invalid", result);
|
||||
return result;
|
||||
}
|
||||
const contents = getPaseoBrowserWebContents(browserId);
|
||||
const contents = getPaseoBrowserWebContentsForHostWindow(browserId, event.sender.id);
|
||||
if (!contents) {
|
||||
const result = {
|
||||
ok: false,
|
||||
@@ -537,11 +501,11 @@ ipcMain.handle("paseo:browser:clear-profile", async (_event, rawLegacyBrowserIds
|
||||
|
||||
ipcMain.handle(
|
||||
"paseo:browser:capture-element",
|
||||
async (_event, browserId: unknown, rect: unknown) => {
|
||||
async (event, browserId: unknown, rect: unknown) => {
|
||||
if (typeof browserId !== "string" || browserId.trim().length === 0) {
|
||||
return null;
|
||||
}
|
||||
const contents = getPaseoBrowserWebContents(browserId);
|
||||
const contents = getPaseoBrowserWebContentsForHostWindow(browserId, event.sender.id);
|
||||
if (!contents || contents.isDestroyed()) {
|
||||
return null;
|
||||
}
|
||||
@@ -622,6 +586,10 @@ function getPreloadPath(): string {
|
||||
return path.join(__dirname, "preload.js");
|
||||
}
|
||||
|
||||
function getBrowserKeyboardPreloadPath(): string {
|
||||
return path.join(__dirname, "features", "browser-keyboard", "guest-preload.js");
|
||||
}
|
||||
|
||||
function getAppDistDir(): string {
|
||||
if (app.isPackaged) {
|
||||
return path.join(process.resourcesPath, "app-dist");
|
||||
@@ -725,6 +693,8 @@ async function createWindow(
|
||||
pendingOpenProjectStore.set(webContentsId, options.pendingOpenProjectPath);
|
||||
mainWindow.on("closed", () => {
|
||||
pendingOpenProjectStore.delete(webContentsId);
|
||||
unregisterPaseoBrowserHost(webContentsId);
|
||||
browserKeyboard.detachHost(webContentsId);
|
||||
});
|
||||
|
||||
if (devWorktreeName) {
|
||||
@@ -748,7 +718,9 @@ async function createWindow(
|
||||
return;
|
||||
}
|
||||
webPreferences.nodeIntegration = false;
|
||||
webPreferences.nodeIntegrationInSubFrames = false;
|
||||
// The sandboxed keyboard preload must run in every frame so focused iframes keep
|
||||
// the same page-first shortcut boundary. Node integration remains disabled.
|
||||
webPreferences.nodeIntegrationInSubFrames = true;
|
||||
webPreferences.nodeIntegrationInWorker = false;
|
||||
webPreferences.contextIsolation = true;
|
||||
webPreferences.sandbox = true;
|
||||
@@ -759,43 +731,13 @@ async function createWindow(
|
||||
delete params.preload;
|
||||
delete (webPreferences as { preloadURL?: string }).preloadURL;
|
||||
delete (params as { preloadURL?: string }).preloadURL;
|
||||
webPreferences.preload = getBrowserKeyboardPreloadPath();
|
||||
});
|
||||
mainWindow.webContents.on("did-attach-webview", (_event, contents) => {
|
||||
preparePaseoBrowserWebContents(contents);
|
||||
contents.once("destroyed", () => {
|
||||
pendingBrowserWindowOpenRequests.delete(contents.id);
|
||||
});
|
||||
contents.on("before-input-event", (event, input) => {
|
||||
if (isBrowserRefreshInput(input)) {
|
||||
event.preventDefault();
|
||||
if (contents.isLoadingMainFrame()) {
|
||||
contents.stop();
|
||||
} else {
|
||||
contents.reload();
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (isBrowserLocationInput(input)) {
|
||||
event.preventDefault();
|
||||
const focusedBrowserId = getPaseoBrowserIdForWebContents(contents);
|
||||
mainWindow.webContents.send(BROWSER_SHORTCUT_EVENT, {
|
||||
action: "focus-url",
|
||||
...(focusedBrowserId ? { browserId: focusedBrowserId } : {}),
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (isForwardablePaseoShortcutInput(input)) {
|
||||
event.preventDefault();
|
||||
mainWindow.webContents.send(BROWSER_FORWARDED_KEY_EVENT, {
|
||||
key: input.key,
|
||||
code: input.code,
|
||||
meta: input.meta,
|
||||
control: input.control,
|
||||
shift: input.shift,
|
||||
alt: input.alt,
|
||||
});
|
||||
}
|
||||
});
|
||||
installBrowserWindowOpenHandler({
|
||||
contents,
|
||||
sourceContents: contents,
|
||||
|
||||
Reference in New Issue
Block a user