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:
@@ -1,4 +1,8 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
vi.hoisted(() => {
|
||||
Object.defineProperty(globalThis, "__DEV__", { value: false, configurable: true });
|
||||
});
|
||||
import type {
|
||||
DaemonClient,
|
||||
ConnectionState,
|
||||
@@ -16,6 +20,10 @@ import {
|
||||
type HostRuntimeStorage,
|
||||
} from "./host-runtime";
|
||||
|
||||
vi.mock("@/browser-automation/handler", () => ({
|
||||
mountBrowserAutomationDaemonClientHandler: vi.fn(() => () => undefined),
|
||||
}));
|
||||
|
||||
class FakeDaemonClient {
|
||||
private state: ConnectionState = { status: "idle" };
|
||||
private listeners = new Set<(status: ConnectionState) => void>();
|
||||
@@ -342,6 +350,18 @@ function onceHostListMatches(store: HostRuntimeStore, predicate: () => boolean):
|
||||
});
|
||||
}
|
||||
|
||||
class BrowserClientLifecycle {
|
||||
public active: Array<{ serverId: string; connectionId: string }> = [];
|
||||
|
||||
mount(input: { host: HostProfile; connection: HostConnection }): () => void {
|
||||
const entry = { serverId: input.host.serverId, connectionId: input.connection.id };
|
||||
this.active.push(entry);
|
||||
return () => {
|
||||
this.active = this.active.filter((current) => current !== entry);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
describe("HostRuntimeController", () => {
|
||||
it("replaces the active relay client when re-pairing changes the daemon public key", async () => {
|
||||
const oldRelay: HostConnection = {
|
||||
@@ -458,6 +478,39 @@ describe("HostRuntimeController", () => {
|
||||
expect(controller.getSnapshot().connectionStatus).toBe("online");
|
||||
});
|
||||
|
||||
it("keeps browser client lifecycle tied to the active host runtime client", async () => {
|
||||
const host = makeHost({ preferredConnectionId: "direct:lan:6767" });
|
||||
const fakeClient = makeConnectedProbeClient(12);
|
||||
const lifecycle = new BrowserClientLifecycle();
|
||||
const controller = new HostRuntimeController({
|
||||
host,
|
||||
deps: {
|
||||
createClient: () => new FakeDaemonClient() as unknown as DaemonClient,
|
||||
connectToDaemon: async ({ host: hostProfile, connection }) => ({
|
||||
client: makeConnectedProbeClient(10) as unknown as DaemonClient,
|
||||
serverId: hostProfile.serverId,
|
||||
hostname: connection.id,
|
||||
}),
|
||||
getClientId: async () => "cid_runtime_stable",
|
||||
mountClientHandlers: (input) => lifecycle.mount(input),
|
||||
},
|
||||
});
|
||||
|
||||
await controller.start({
|
||||
autoProbe: false,
|
||||
initialConnection: {
|
||||
connectionId: "direct:lan:6767",
|
||||
existingClient: fakeClient as unknown as DaemonClient,
|
||||
},
|
||||
});
|
||||
|
||||
expect(lifecycle.active).toEqual([{ serverId: "srv_test", connectionId: "direct:lan:6767" }]);
|
||||
|
||||
await controller.stop();
|
||||
|
||||
expect(lifecycle.active).toEqual([]);
|
||||
});
|
||||
|
||||
it("adopts the first successful probe on startup", async () => {
|
||||
const host = makeHost({ preferredConnectionId: "direct:lan:6767" });
|
||||
const clients: FakeDaemonClient[] = [];
|
||||
@@ -1431,7 +1484,7 @@ describe("HostRuntimeStore", () => {
|
||||
entries: [
|
||||
makeFetchAgentsEntry({
|
||||
id: "agent-recent",
|
||||
cwd: "/Users/moboudra/dev/paseo",
|
||||
cwd: "/workspaces/paseo",
|
||||
updatedAt: "2026-03-04T12:00:00.000Z",
|
||||
title: "Recent agent",
|
||||
}),
|
||||
@@ -1444,7 +1497,7 @@ describe("HostRuntimeStore", () => {
|
||||
entries: [
|
||||
makeFetchAgentsEntry({
|
||||
id: "agent-stale-attention",
|
||||
cwd: "/Users/moboudra/dev/paseo-pr67-review",
|
||||
cwd: "/workspaces/paseo-pr67-review",
|
||||
updatedAt: "2026-02-20T08:00:00.000Z",
|
||||
title: "Needs triage",
|
||||
requiresAttention: true,
|
||||
@@ -1612,7 +1665,7 @@ describe("HostRuntimeStore", () => {
|
||||
useSessionStore.getState().setAgents(host.serverId, () => {
|
||||
const stale = makeFetchAgentsEntry({
|
||||
id: "agent-archived",
|
||||
cwd: "/Users/moboudra/dev/paseo",
|
||||
cwd: "/workspaces/paseo",
|
||||
updatedAt: "2026-03-30T15:29:00.000Z",
|
||||
archivedAt: null,
|
||||
title: "Stale active copy",
|
||||
|
||||
Reference in New Issue
Block a user