Add opt-in browser tools for desktop tabs

* 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
This commit is contained in:
Mohamed Boudra
2026-06-30 22:33:18 +02:00
committed by GitHub
parent db03b1f3fd
commit 20385bdb50
71 changed files with 10908 additions and 115 deletions
+33
View File
@@ -37,6 +37,8 @@ import {
buildLocalDaemonTransportUrl,
createDesktopLocalDaemonTransportFactory,
} from "@/desktop/daemon/desktop-daemon-transport";
import { getDesktopHost } from "@/desktop/host";
import { CLIENT_CAPS } from "@getpaseo/protocol/client-capabilities";
import { replaceFetchedAgentDirectory } from "@/utils/agent-directory-sync";
import { useSessionStore } from "@/stores/session-store";
import {
@@ -45,6 +47,7 @@ import {
} from "@/workspace/legacy-daemon-workspaces";
import { invalidateCheckoutGitQueriesForServer } from "@/git/query-keys";
import { queryClient } from "@/query/query-client";
import { mountBrowserAutomationDaemonClientHandler } from "@/browser-automation/handler";
export type HostRuntimeConnectionStatus = "idle" | "connecting" | "online" | "offline" | "error";
export type HostRegistryStatus = "loading" | "ready";
@@ -138,6 +141,11 @@ export interface HostRuntimeControllerDeps {
}>;
getClientId: () => Promise<string>;
readInitialConnectionHint?: () => InitialDaemonConnectionHint | null;
mountClientHandlers?: (input: {
client: DaemonClient;
host: HostProfile;
connection: HostConnection;
}) => () => void;
}
export interface HostRuntimeStorage {
@@ -514,6 +522,12 @@ function probeIntervalForConnection(
}
function createDefaultDeps(): HostRuntimeControllerDeps {
const desktopBrowserAutomationAvailable =
typeof getDesktopHost()?.browser?.executeAutomationCommand === "function";
const browserAutomationCapabilities = desktopBrowserAutomationAvailable
? { [CLIENT_CAPS.desktopBrowserAutomation]: true }
: undefined;
return {
createClient: ({ host, connection, clientId, runtimeGeneration }) => {
const localTransportFactory = createDesktopLocalDaemonTransportFactory();
@@ -523,6 +537,7 @@ function createDefaultDeps(): HostRuntimeControllerDeps {
clientType: "mobile" as const,
appVersion: resolveAppVersion() ?? undefined,
runtimeGeneration,
...(browserAutomationCapabilities ? { capabilities: browserAutomationCapabilities } : {}),
};
if (connection.type === "directSocket" || connection.type === "directPipe") {
return new DaemonClient({
@@ -560,8 +575,15 @@ function createDefaultDeps(): HostRuntimeControllerDeps {
connectToDaemon(connection, {
...(host.serverId ? { serverId: host.serverId } : {}),
...(timeoutMs !== undefined ? { timeoutMs } : {}),
...(browserAutomationCapabilities ? { capabilities: browserAutomationCapabilities } : {}),
}),
getClientId: () => getOrCreateClientId(),
mountClientHandlers: ({ client, host }) => {
if (!browserAutomationCapabilities) {
return () => {};
}
return mountBrowserAutomationDaemonClientHandler(client, { serverId: host.serverId });
},
};
}
@@ -574,6 +596,7 @@ export class HostRuntimeController {
private listeners = new Set<() => void>();
private activeClient: DaemonClient | null = null;
private unsubscribeClientStatus: (() => void) | null = null;
private unsubscribeClientHandlers: (() => void) | null = null;
private probeIntervalHandle: ReturnType<typeof setInterval> | null = null;
private started = false;
private connectionFirstSeenAt = new Map<string, number>();
@@ -656,6 +679,10 @@ export class HostRuntimeController {
this.unsubscribeClientStatus();
this.unsubscribeClientStatus = null;
}
if (this.unsubscribeClientHandlers) {
this.unsubscribeClientHandlers();
this.unsubscribeClientHandlers = null;
}
if (this.activeClient) {
const prev = this.activeClient;
this.activeClient = null;
@@ -1133,6 +1160,10 @@ export class HostRuntimeController {
this.unsubscribeClientStatus();
this.unsubscribeClientStatus = null;
}
if (this.unsubscribeClientHandlers) {
this.unsubscribeClientHandlers();
this.unsubscribeClientHandlers = null;
}
if (this.activeClient) {
const previousClient = this.activeClient;
this.activeClient = null;
@@ -1206,6 +1237,8 @@ export class HostRuntimeController {
}
this.activeClient = client;
this.unsubscribeClientHandlers =
this.deps.mountClientHandlers?.({ client, host: this.host, connection }) ?? null;
this.applyConnectionEvent({
type: "select_connection",
connectionId: connection.id,