* Add opt-in browser tools for desktop tabs Adds the daemon opt-in, desktop tab routing, MCP tools, and real browser automation surfaces for Paseo desktop browser tabs. * Fix browser tools CI expectations * Address browser tools review findings * Restrict browser file automation paths * Fix browser upload test on Windows * Harden browser navigation inputs * Make browser tools create usable tabs * Update browser MCP empty-state test * Fail browser tab creation when registration times out * Fix browser screenshots for agents * Hide disabled browser tools from agents * Address browser tools architecture review * Replace browser tools review tests * Wrap browser tab registration errors * Mock Expo Router in app unit tests * Handle invalid browser automation requests * Return browser failure on desktop disconnect * Update browser disconnect websocket test * Relax browser timeout polling test * Handle invalid browser responses * Return browser failure when send fails * Remove local diagnostics and fixture paths * Fix dev service home fallback * Use worktree home for dev services * Use managed daemon in desktop dev * fix(browser): keep agent tabs addressable Track agent-active browser targets separately from human-focused tabs and keep resident webviews alive for automation. Browser tool visibility now comes from registration while the broker reports disabled execution. * refactor(browser): register tools through catalog Move browser tool registration onto the shared Paseo tool catalog so the MCP server remains only the transport adapter. * fix(settings): translate browser tools host error
137 lines
4.5 KiB
TypeScript
137 lines
4.5 KiB
TypeScript
import { webContents as allWebContents, type WebContents } from "electron";
|
|
import {
|
|
BROWSER_NEW_TAB_REQUEST_EVENT,
|
|
handleBrowserWindowOpenRequest,
|
|
isAllowedBrowserWebviewUrl,
|
|
} from "./window-open.js";
|
|
import { PaseoBrowserWebviewRegistry, type BrowserWorkspaceRegistration } from "./registry.js";
|
|
|
|
export { BROWSER_NEW_TAB_REQUEST_EVENT, handleBrowserWindowOpenRequest };
|
|
export type { BrowserWorkspaceRegistration };
|
|
|
|
const browserRegistry = new PaseoBrowserWebviewRegistry();
|
|
|
|
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;
|
|
}
|
|
|
|
export function readBrowserIdFromWebviewAttach(input: {
|
|
src?: string;
|
|
partition?: string;
|
|
}): string | null {
|
|
if (!isAllowedBrowserWebviewUrl(input.src)) {
|
|
return null;
|
|
}
|
|
return getBrowserIdFromWebviewPartition(input.partition);
|
|
}
|
|
|
|
export function listRegisteredPaseoBrowserIds(): string[] {
|
|
return browserRegistry
|
|
.listBrowserIds()
|
|
.filter((browserId) => getPaseoBrowserWebContents(browserId));
|
|
}
|
|
|
|
export function registerPaseoBrowserWebContents(contents: WebContents, browserId: string): void {
|
|
browserRegistry.registerWebContents({ webContentsId: contents.id, browserId });
|
|
contents.once("destroyed", () => {
|
|
browserRegistry.unregisterWebContents(contents.id);
|
|
});
|
|
}
|
|
|
|
export function getPaseoBrowserIdForWebContents(contents: WebContents | null): string | null {
|
|
if (!contents || contents.isDestroyed()) {
|
|
return null;
|
|
}
|
|
return browserRegistry.getBrowserIdForWebContents(contents.id);
|
|
}
|
|
|
|
export function registerPaseoBrowserWorkspace(input: BrowserWorkspaceRegistration): void {
|
|
browserRegistry.registerWorkspace(input);
|
|
}
|
|
|
|
export function getPaseoBrowserWorkspaceId(browserId: string): string | null {
|
|
return browserRegistry.getWorkspaceId(browserId);
|
|
}
|
|
|
|
export function listRegisteredPaseoBrowserIdsForWorkspace(workspaceId: string): string[] {
|
|
return browserRegistry
|
|
.listBrowserIdsForWorkspace(workspaceId)
|
|
.filter((browserId) => getPaseoBrowserWebContents(browserId));
|
|
}
|
|
|
|
export function setWorkspaceActivePaseoBrowserId(input: {
|
|
workspaceId: string;
|
|
browserId: string | null;
|
|
}): void {
|
|
browserRegistry.setWorkspaceActiveBrowser(input);
|
|
}
|
|
|
|
export function getWorkspaceActivePaseoBrowserId(workspaceId: string): string | null {
|
|
return browserRegistry.getWorkspaceActiveBrowserId(workspaceId);
|
|
}
|
|
|
|
export function setAgentActivePaseoBrowserId(input: {
|
|
agentId: string;
|
|
browserId: string | null;
|
|
}): void {
|
|
browserRegistry.setAgentActiveBrowser(input);
|
|
}
|
|
|
|
export function getAgentActivePaseoBrowserId(agentId: string): string | null {
|
|
return browserRegistry.getAgentActiveBrowserId(agentId);
|
|
}
|
|
|
|
export function getPaseoBrowserWebContents(browserId: string): WebContents | null {
|
|
const contentsId = browserRegistry.getWebContentsIdForBrowser(browserId);
|
|
if (contentsId === null) {
|
|
return null;
|
|
}
|
|
const contents = allWebContents.fromId(contentsId);
|
|
if (contents && !contents.isDestroyed()) {
|
|
return contents;
|
|
}
|
|
browserRegistry.unregisterWebContents(contentsId);
|
|
return null;
|
|
}
|
|
|
|
export function getWorkspaceActivePaseoBrowserWebContents(workspaceId: string): WebContents | null {
|
|
const activeBrowserId = getWorkspaceActivePaseoBrowserId(workspaceId);
|
|
return activeBrowserId ? getPaseoBrowserWebContents(activeBrowserId) : null;
|
|
}
|
|
|
|
export function getAgentActivePaseoBrowserWebContents(agentId: string): WebContents | null {
|
|
const activeBrowserId = getAgentActivePaseoBrowserId(agentId);
|
|
return activeBrowserId ? getPaseoBrowserWebContents(activeBrowserId) : null;
|
|
}
|
|
|
|
export function getMostRecentWorkspaceActivePaseoBrowserWebContents(): WebContents | null {
|
|
const browserId = browserRegistry.getMostRecentWorkspaceActiveBrowserId();
|
|
return browserId ? getPaseoBrowserWebContents(browserId) : null;
|
|
}
|
|
|
|
function preventUnsafeBrowserWebviewNavigation(
|
|
event: { preventDefault: () => void },
|
|
url: string | undefined,
|
|
): void {
|
|
if (!isAllowedBrowserWebviewUrl(url)) {
|
|
event.preventDefault();
|
|
}
|
|
}
|
|
|
|
export function registerBrowserWebviewNavigationGuards(contents: WebContents): void {
|
|
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);
|
|
});
|
|
}
|