Add browser pane element picker (#670)
* feat: add browser pane element picker Port the browser pane from PR #198 and route picked elements through workspace attachments so the context follows every agent composer in a workspace. Co-authored-by: Jason Kneen <jason.kneen@bouncingfish.com> * docs: document electron platform overlays * fix: harden browser pane navigation * fix: polish browser tab interactions * fix: route browser pane focus shortcuts --------- Co-authored-by: Jason Kneen <jason.kneen@bouncingfish.com>
This commit is contained in:
co-authored by
Jason Kneen
parent
34b95e7f01
commit
906205f97a
@@ -0,0 +1,17 @@
|
||||
import type { WebContents } from "electron";
|
||||
|
||||
const browserIdsByWebContentsId = new Map<number, string>();
|
||||
|
||||
export function registerPaseoBrowserWebContents(contents: WebContents, browserId: string): void {
|
||||
browserIdsByWebContentsId.set(contents.id, browserId);
|
||||
contents.once("destroyed", () => {
|
||||
browserIdsByWebContentsId.delete(contents.id);
|
||||
});
|
||||
}
|
||||
|
||||
export function getPaseoBrowserIdForWebContents(contents: WebContents | null): string | null {
|
||||
if (!contents || contents.isDestroyed()) {
|
||||
return null;
|
||||
}
|
||||
return browserIdsByWebContentsId.get(contents.id) ?? null;
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import { app, Menu, BrowserWindow, ipcMain } from "electron";
|
||||
import { app, Menu, BrowserWindow, ipcMain, webContents } from "electron";
|
||||
import { getPaseoBrowserIdForWebContents } from "./browser-webviews.js";
|
||||
|
||||
interface ShowContextMenuInput {
|
||||
kind?: "terminal";
|
||||
@@ -14,6 +15,33 @@ function withBrowserWindow(
|
||||
};
|
||||
}
|
||||
|
||||
function getFocusedPaseoBrowserWebContents(): Electron.WebContents | null {
|
||||
const focusedContents = webContents.getFocusedWebContents();
|
||||
return getPaseoBrowserIdForWebContents(focusedContents) ? focusedContents : null;
|
||||
}
|
||||
|
||||
function reloadFocusedContentsOrWindow(win: BrowserWindow, options?: { ignoreCache?: boolean }) {
|
||||
const browserContents = getFocusedPaseoBrowserWebContents();
|
||||
if (browserContents) {
|
||||
if (options?.ignoreCache) {
|
||||
browserContents.reloadIgnoringCache();
|
||||
return;
|
||||
}
|
||||
if (browserContents.isLoadingMainFrame()) {
|
||||
browserContents.stop();
|
||||
return;
|
||||
}
|
||||
browserContents.reload();
|
||||
return;
|
||||
}
|
||||
|
||||
if (options?.ignoreCache) {
|
||||
win.webContents.reloadIgnoringCache();
|
||||
return;
|
||||
}
|
||||
win.webContents.reload();
|
||||
}
|
||||
|
||||
export function setupApplicationMenu(): void {
|
||||
const isMac = process.platform === "darwin";
|
||||
|
||||
@@ -73,8 +101,20 @@ export function setupApplicationMenu(): void {
|
||||
}),
|
||||
},
|
||||
{ type: "separator" },
|
||||
{ role: "reload" },
|
||||
{ role: "forceReload" },
|
||||
{
|
||||
label: "Reload",
|
||||
accelerator: "CmdOrCtrl+R",
|
||||
click: withBrowserWindow((win) => {
|
||||
reloadFocusedContentsOrWindow(win);
|
||||
}),
|
||||
},
|
||||
{
|
||||
label: "Force Reload",
|
||||
accelerator: "CmdOrCtrl+Shift+R",
|
||||
click: withBrowserWindow((win) => {
|
||||
reloadFocusedContentsOrWindow(win, { ignoreCache: true });
|
||||
}),
|
||||
},
|
||||
{ role: "toggleDevTools" },
|
||||
{ type: "separator" },
|
||||
{ role: "togglefullscreen" },
|
||||
|
||||
@@ -33,6 +33,10 @@ import {
|
||||
} from "./features/notifications.js";
|
||||
import { registerOpenerHandlers } from "./features/opener.js";
|
||||
import { setupApplicationMenu } from "./features/menu.js";
|
||||
import {
|
||||
getPaseoBrowserIdForWebContents,
|
||||
registerPaseoBrowserWebContents,
|
||||
} from "./features/browser-webviews.js";
|
||||
import { parseOpenProjectPathFromArgv } from "./open-project-routing.js";
|
||||
import { getDesktopSettingsStore } from "./settings/desktop-settings-electron.js";
|
||||
import {
|
||||
@@ -47,11 +51,60 @@ import { autoUpdateSkillsIfInstalled } from "./integrations/integrations-manager
|
||||
|
||||
const DEV_SERVER_URL = process.env.EXPO_DEV_URL ?? "http://localhost:8081";
|
||||
const APP_SCHEME = "paseo";
|
||||
|
||||
function isAllowedBrowserWebviewUrl(value: string | undefined): boolean {
|
||||
if (!value) {
|
||||
return true;
|
||||
}
|
||||
try {
|
||||
const parsed = new URL(value);
|
||||
return (
|
||||
parsed.protocol === "http:" || parsed.protocol === "https:" || parsed.href === "about:blank"
|
||||
);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function preventUnsafeBrowserWebviewNavigation(
|
||||
event: Electron.Event,
|
||||
url: string | undefined,
|
||||
): void {
|
||||
if (!isAllowedBrowserWebviewUrl(url)) {
|
||||
event.preventDefault();
|
||||
}
|
||||
}
|
||||
const OPEN_PROJECT_EVENT = "paseo:event:open-project";
|
||||
const BROWSER_SHORTCUT_EVENT = "paseo:event:browser-shortcut";
|
||||
const DESKTOP_SMOKE_ENV = "PASEO_DESKTOP_SMOKE";
|
||||
const DESKTOP_SMOKE_STOP_REQUEST = "paseo-smoke-stop";
|
||||
app.setName("Paseo");
|
||||
|
||||
function getBrowserIdFromWebviewPartition(partition: string | undefined): string | null {
|
||||
const prefix = "persist:paseo-browser-";
|
||||
if (!partition?.startsWith(prefix)) {
|
||||
return null;
|
||||
}
|
||||
const browserId = partition.slice(prefix.length).trim();
|
||||
return browserId.length > 0 ? browserId : null;
|
||||
}
|
||||
|
||||
const pendingBrowserWebviewIds: string[] = [];
|
||||
|
||||
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";
|
||||
}
|
||||
|
||||
// In dev mode, detect git worktrees and isolate each instance so multiple
|
||||
// Electron windows can run side-by-side (separate userData = separate lock).
|
||||
let devWorktreeName: string | null = null;
|
||||
@@ -206,6 +259,7 @@ async function createMainWindow(): Promise<void> {
|
||||
preload: getPreloadPath(),
|
||||
contextIsolation: true,
|
||||
nodeIntegration: false,
|
||||
webviewTag: true,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -217,6 +271,71 @@ async function createMainWindow(): Promise<void> {
|
||||
setupWindowResizeEvents(mainWindow);
|
||||
setupDefaultContextMenu(mainWindow);
|
||||
setupDragDropPrevention(mainWindow);
|
||||
mainWindow.webContents.on("will-attach-webview", (event, webPreferences, params) => {
|
||||
if (!isAllowedBrowserWebviewUrl(params.src)) {
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
const browserId = getBrowserIdFromWebviewPartition(params.partition);
|
||||
if (!browserId) {
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
pendingBrowserWebviewIds.push(browserId);
|
||||
webPreferences.nodeIntegration = false;
|
||||
webPreferences.nodeIntegrationInSubFrames = false;
|
||||
webPreferences.nodeIntegrationInWorker = false;
|
||||
webPreferences.contextIsolation = true;
|
||||
webPreferences.sandbox = true;
|
||||
webPreferences.webSecurity = true;
|
||||
webPreferences.webviewTag = false;
|
||||
webPreferences.allowRunningInsecureContent = false;
|
||||
delete webPreferences.preload;
|
||||
delete params.preload;
|
||||
delete (webPreferences as { preloadURL?: string }).preloadURL;
|
||||
delete (params as { preloadURL?: string }).preloadURL;
|
||||
});
|
||||
mainWindow.webContents.on("did-attach-webview", (_event, contents) => {
|
||||
const browserId = pendingBrowserWebviewIds.shift() ?? null;
|
||||
if (browserId) {
|
||||
registerPaseoBrowserWebContents(contents, browserId);
|
||||
}
|
||||
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 } : {}),
|
||||
});
|
||||
}
|
||||
});
|
||||
contents.setWindowOpenHandler(({ url }) => {
|
||||
if (!isAllowedBrowserWebviewUrl(url)) {
|
||||
return { action: "deny" };
|
||||
}
|
||||
contents.loadURL(url).catch(() => undefined);
|
||||
return { action: "deny" };
|
||||
});
|
||||
contents.on("will-navigate", (event) => {
|
||||
preventUnsafeBrowserWebviewNavigation(event, event.url);
|
||||
});
|
||||
contents.on("will-frame-navigate", (event) => {
|
||||
preventUnsafeBrowserWebviewNavigation(event, event.url);
|
||||
});
|
||||
contents.on("will-redirect", (event) => {
|
||||
preventUnsafeBrowserWebviewNavigation(event, event.url);
|
||||
});
|
||||
});
|
||||
|
||||
mainWindow.once("ready-to-show", () => {
|
||||
mainWindow.show();
|
||||
|
||||
Reference in New Issue
Block a user