Skip to content
Open
6 changes: 3 additions & 3 deletions apps/desktop/src/preview/DesktopBrowserHost.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -947,7 +947,7 @@ describe("DesktopBrowserHost", () => {
}).pipe(Effect.scoped),
);

it.effect("tells a download the person clicked from one the agent's input started", () =>
it.effect("tells a person's action from one the agent's input started", () =>
Effect.gen(function* () {
const host = yield* DesktopBrowserHost.make;
const debuggee = makeDebuggee();
Expand All @@ -960,7 +960,7 @@ describe("DesktopBrowserHost", () => {
message: encodeJson({ id: 1, method: "DOM.getDocument", params: {} }),
}),
);
expect(host.humanStartedDownload(debuggee.tab.webContents)).toBe(true);
expect(host.agentActedRecently(debuggee.tab.webContents)).toBe(false);
yield* host.handleCommandLine(
encodeJson({
type: "cdp",
Expand All @@ -972,7 +972,7 @@ describe("DesktopBrowserHost", () => {
}),
}),
);
expect(host.humanStartedDownload(debuggee.tab.webContents)).toBe(false);
expect(host.agentActedRecently(debuggee.tab.webContents)).toBe(true);
}),
);

Expand Down
10 changes: 5 additions & 5 deletions apps/desktop/src/preview/DesktopBrowserHost.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ const AGENT_INPUT_COMMAND =
/"method":"(?:Input\.|Page\.navigate"|Runtime\.(?:evaluate|callFunctionOn)")/;
/** The same commands as above, by method, for the relay's sends. */
const AGENT_INPUT_METHOD = /^(?:Input\.|Page\.navigate$|Runtime\.(?:evaluate|callFunctionOn)$)/;
const AGENT_DOWNLOAD_WINDOW_MS = 5_000;
const AGENT_INPUT_WINDOW_MS = 5_000;
const FOCUS_IDLE = "5 seconds";

export interface DesktopBrowserTabKey {
Expand Down Expand Up @@ -87,7 +87,7 @@ export class DesktopBrowserHost extends Context.Service<
readonly handleCommandLine: (line: string) => Effect.Effect<void>;
readonly attach: (key: DesktopBrowserTabKey, debuggee: DesktopBrowserTabDebugger) => void;
readonly detach: (key: DesktopBrowserTabKey) => void;
readonly humanStartedDownload: (source: Electron.WebContents) => boolean;
readonly agentActedRecently: (source: Electron.WebContents) => boolean;
readonly placeDownload: (source: Electron.WebContents, item: Electron.DownloadItem) => boolean;
readonly pointers: Stream.Stream<{
readonly key: DesktopBrowserTabKey;
Expand Down Expand Up @@ -482,9 +482,9 @@ export const make = Effect.gen(function* () {
relayFor(tab).receive(command.value.message);
});

const humanStartedDownload = (source: Electron.WebContents) => {
const agentActedRecently = (source: Electron.WebContents) => {
const tab = [...tabs.values()].find((candidate) => candidate.debuggee.webContents === source);
return tab !== undefined && performance.now() - tab.agentInputAt > AGENT_DOWNLOAD_WINDOW_MS;
return tab !== undefined && performance.now() - tab.agentInputAt <= AGENT_INPUT_WINDOW_MS;
};

const announceAll = Effect.suspend(() => {
Expand Down Expand Up @@ -515,7 +515,7 @@ export const make = Effect.gen(function* () {
attach,
detach,
placeDownload,
humanStartedDownload,
agentActedRecently,
});
});

Expand Down
66 changes: 66 additions & 0 deletions apps/desktop/src/preview/Manager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1812,6 +1812,72 @@ describe("PreviewManager", () => {
}).pipe(Effect.provide(managerLayer()), Effect.scoped),
);

effectIt.effect("opens a new-tab link the agent clicked in the background", () =>
Effect.gen(function* () {
const host = yield* DesktopBrowserHost.DesktopBrowserHost;
const serverTab = { threadId: "thread-1", tabId: "server-tab-1" };
const contents = Object.assign(
makeTestPreviewWebContents(async () => {
throw new Error("unused");
}, 42),
{ isDevToolsOpened: () => false, getUserAgent: () => "Electron", focusedFrame: {} },
);
fromId.mockReturnValue(contents);
const manager = yield* PreviewManager.PreviewManager;
const openLinks = yield* Queue.unbounded<{ url: string; background: boolean }>();
yield* manager.subscribeOpenLinks(({ url, background }) =>
Queue.offer(openLinks, { url, background }).pipe(Effect.asVoid),
);
const attached = yield* Deferred.make<void>();
yield* host.events.pipe(
Stream.runForEach((line) =>
new TextDecoder().decode(line).includes('"type":"attached"')
? Deferred.succeed(attached, undefined)
: Effect.void,
),
Effect.forkScoped,
);
yield* manager.createTab("tab_1", { serverTab });
yield* manager.registerWebview("tab_1", 42);
yield* Deferred.await(attached);
const openWindow = vi.mocked(contents.setWindowOpenHandler).mock.calls[0]![0];
const clickBlankLink = (url: string) =>
openWindow({ url, disposition: "foreground-tab" } as Electron.HandlerDetails);

clickBlankLink("https://example.com/person");
expect(yield* Queue.take(openLinks)).toEqual({
url: "https://example.com/person",
background: false,
});

Object.assign(contents, { focusedFrame: null });
yield* host.handleCommandLine(
JSON.stringify({
type: "cdp",
...serverTab,
message: JSON.stringify({
id: 1,
method: "Input.dispatchMouseEvent",
params: { type: "mousePressed", x: 10, y: 10, button: "left", clickCount: 1 },
sessionId: "t3-preview-page",
}),
}),
);
clickBlankLink("https://example.com/agent");
expect(yield* Queue.take(openLinks)).toEqual({
url: "https://example.com/agent",
background: true,
});

Object.assign(contents, { focusedFrame: {} });
clickBlankLink("https://example.com/person-after-agent");
expect(yield* Queue.take(openLinks)).toEqual({
url: "https://example.com/person-after-agent",
background: false,
});
}).pipe(Effect.provide(managerLayer()), Effect.scoped),
);

effectIt.effect("discards a screenshot that resolves after its guest is replaced", () =>
withManager((manager) =>
Effect.gen(function* () {
Expand Down
9 changes: 6 additions & 3 deletions apps/desktop/src/preview/Manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1548,7 +1548,10 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function
emitOpenLink({
tabId,
url: details.url,
background: details.disposition === "background-tab",
// A person's click focuses a guest frame; GuestInput hands focus back after the agent's press.
background:
details.disposition === "background-tab" ||
(browserHost.agentActedRecently(wc) && wc.focusedFrame === null),
}),
);
return { action: "deny" };
Expand Down Expand Up @@ -3598,8 +3601,8 @@ export const make = Effect.gen(function* PreviewManagerMake() {
downloadSessions.add(session);
session.on("will-download", (_event, item, source) => {
// Decided before the server's path is set: placing it consumes the guid.
const human = browserHost.humanStartedDownload(source);
if (!browserHost.placeDownload(source, item) || !human) return;
const agentStarted = browserHost.agentActedRecently(source);
if (!browserHost.placeDownload(source, item) || agentStarted) return;
item.once("done", (_doneEvent, state) => {
if (state !== "completed") return;
runForkDownload(
Expand Down
Loading