Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,16 @@ const fakes = vi.hoisted(() => {
action: "allow" | "deny";
createWindow?: (options: Record<string, unknown>) => FakeWebContents;
};
type WindowOpenHandler = (details: { url: string }) => WindowOpenHandlerResponse;
type WindowOpenDetails = {
url: string;
referrer?: { url: string; policy: string };
postBody?: {
boundary?: string;
contentType: string;
data: Array<Record<string, unknown>>;
};
};
type WindowOpenHandler = (details: WindowOpenDetails) => WindowOpenHandlerResponse;
type BeforeSendHeadersHandler = (
details: { requestHeaders: Record<string, string | string[] | undefined> },
callback: (response: { requestHeaders: Record<string, string | string[] | undefined> }) => void,
Expand Down Expand Up @@ -77,10 +86,12 @@ const fakes = vi.hoisted(() => {
session: unknown = null;
audioMutedCalls: boolean[] = [];
userAgentCalls: string[] = [];
loadURLCalls: Array<{ url: string; options?: Record<string, unknown> }> = [];
currentUrl = "";
private listeners: Record<string, Array<(...args: unknown[]) => void>> = {};
private windowOpenHandler: WindowOpenHandler | null = null;
loadURL = async (url: string): Promise<void> => {
loadURL = async (url: string, options?: Record<string, unknown>): Promise<void> => {
this.loadURLCalls.push({ url, options });
const event = { preventDefault: vi.fn() };
this.emit("will-navigate", event, url);
if (event.preventDefault.mock.calls.length === 0) {
Expand Down Expand Up @@ -116,7 +127,7 @@ const fakes = vi.hoisted(() => {
setWindowOpenHandler = (handler: WindowOpenHandler): void => {
this.windowOpenHandler = handler;
};
openWindow = (url: string): WindowOpenHandlerResponse | null => this.windowOpenHandler?.({ url }) ?? null;
openWindow = (url: string, details: Partial<WindowOpenDetails> = {}): WindowOpenHandlerResponse | null => this.windowOpenHandler?.({ ...details, url }) ?? null;
on = (event: string, fn: (...args: unknown[]) => void): void => {
(this.listeners[event] ??= []).push(fn);
};
Expand Down Expand Up @@ -989,6 +1000,64 @@ describe("createBuiltInBrowserService — bounds and status dedupe", () => {
expect(fakes.openExternal).not.toHaveBeenCalled();
});

it("recovers crashed browser renderers to a blank tab with an error event", async () => {
const logger = {
debug: vi.fn(),
error: vi.fn(),
info: vi.fn(),
warn: vi.fn(),
};
let resolveRecovery: () => void = () => undefined;
const recovered = new Promise<void>((resolve) => {
resolveRecovery = resolve;
});
const service = createBuiltInBrowserService({
getLogger: () => logger,
onEvent: (event) => {
collector.onEvent(event);
if (
event.type === "error"
&& event.message.includes("renderer exited (crashed, exit code 133)")
&& event.message.includes("Recovered the tab to a blank page")
) {
resolveRecovery();
}
},
});
service.attachToWindow(fakeBrowserWindow() as unknown as Parameters<typeof service.attachToWindow>[0]);
await service.createTab({ url: "https://linear.app/integrations/agents?code=secret", activate: true });
collector.events.length = 0;

const wc = fakes.webContentsInstances[0];
expect(wc, "browser tab webContents exists").toBeTruthy();
const originalLoadURL = wc.loadURL;
wc.loadURL = vi.fn(async (url: string) => {
await originalLoadURL(url);
});

wc.emit("render-process-gone", {}, {
reason: "crashed",
exitCode: 133,
});
await recovered;

expect(service.getStatus().url).toBe("about:blank");
expect(service.getStatus().tabs[0]).toMatchObject({
url: "about:blank",
isLoading: false,
});
expect(collector.events.some((event) => (
event.type === "error"
&& event.message.includes("renderer exited (crashed, exit code 133)")
&& event.message.includes("Recovered the tab to a blank page")
))).toBe(true);
expect(logger.warn).toHaveBeenCalledWith("built_in_browser.render_process_gone", expect.objectContaining({
reason: "crashed",
exitCode: 133,
url: "https://linear.app",
}));
});

it("does not impersonate Chrome or rewrite browser request headers", async () => {
const service = createBuiltInBrowserService({ onEvent: collector.onEvent });
await service.createTab({ url: "https://example.test", activate: true });
Expand Down Expand Up @@ -1029,7 +1098,7 @@ describe("createBuiltInBrowserService — bounds and status dedupe", () => {
})).toBe(false);
});

it("opens popup requests as real ADE browser tabs", async () => {
it("intercepts popup requests as real ADE browser tabs", async () => {
const service = createBuiltInBrowserService({ onEvent: collector.onEvent });
await service.createTab({
url: "https://example.test",
Expand All @@ -1039,17 +1108,49 @@ describe("createBuiltInBrowserService — bounds and status dedupe", () => {
});
const firstTabId = service.getStatus().activeTabId;
const firstWc = fakes.webContentsInstances[0];
const postData = [{ bytes: Buffer.from("token=abc"), type: "rawData" }];

const response = firstWc?.openWindow("https://accounts.google.com/gsi/select");
const response = firstWc?.openWindow("https://accounts.google.com/gsi/select", {
referrer: { url: "https://example.test/sign-in", policy: "strict-origin-when-cross-origin" },
postBody: {
contentType: "application/x-www-form-urlencoded",
data: postData,
},
});

expect(response?.action).toBe("allow");
expect(response?.createWindow).toEqual(expect.any(Function));
const popupWc = response?.createWindow?.({
webPreferences: {
additionalArguments: ["--popup"],
javascript: false,
nodeIntegration: true,
partition: "persist:other",
webviewTag: true,
},
});
expect(popupWc).toBe(fakes.webContentsInstances.at(-1));
await popupWc?.loadURL("https://accounts.google.com/gsi/select");

expect(service.getStatus().tabs).toHaveLength(2);
expect(service.getStatus().activeTabId).not.toBe(firstTabId);
expect(response?.createWindow?.({})).toBe(fakes.webContentsInstances[1]);
expect(service.getStatus().tabs.at(-1)).toMatchObject({
url: "https://accounts.google.com/gsi/select",
ownerLaneId: "lane-1",
ownerChatSessionId: "chat-1",
});
const popupWebPreferences = fakes.webContentsViewInstances.at(-1)?.webPreferences as Record<string, unknown>;
expect(popupWebPreferences).toMatchObject({
partition: service.getStatus().partition,
nodeIntegration: false,
contextIsolation: true,
sandbox: true,
webSecurity: true,
backgroundThrottling: false,
});
expect(popupWebPreferences.additionalArguments).toBeUndefined();
expect(popupWebPreferences.javascript).toBeUndefined();
expect(popupWebPreferences.webviewTag).toBeUndefined();

const openEvent = collector.events.findLast((event) => event.type === "open-request");
expect(openEvent).toMatchObject({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -575,6 +575,7 @@ function createBuiltInBrowserWindowService(args: {
let browserSessionConfigured = false;
let lastEmittedStatusKey: string | null = null;
const configuredWebContents = new WeakSet<WebContents>();
const renderProcessRecoveryTabs = new Set<string>();
let configuredBrowserSession: ReturnType<typeof browserSessionForProfile> | null = null;

const logger = (): Logger | null => {
Expand Down Expand Up @@ -1057,6 +1058,62 @@ function createBuiltInBrowserWindowService(args: {
}
};

const recoverTabAfterRenderProcessGone = async (
tab: BrowserTabState,
details: Electron.RenderProcessGoneDetails,
): Promise<void> => {
const crashedWebContents = tab.webContents;
if (renderProcessRecoveryTabs.has(tab.id) || crashedWebContents.isDestroyed()) return;
const crashedUrl = emptyToNull(crashedWebContents.getURL()) ?? "about:blank";
const reason = details.reason || "unknown";
if (reason === "clean-exit") {
notifyTabActivity(tab);
emitStatus();
return;
}
renderProcessRecoveryTabs.add(tab.id);
const exitCode = Number.isFinite(details.exitCode) ? `, exit code ${details.exitCode}` : "";
const exitMessage = `ADE browser tab renderer exited (${reason}${exitCode}).`;
try {
tab.pendingNetworkRequests.clear();
pushNetworkDiagnostic(tab, {
url: crashedUrl,
method: null,
resourceType: "mainFrame",
statusCode: null,
error: exitMessage,
startedAt: null,
endedAt: new Date().toISOString(),
durationMs: null,
});
if (tab.id === activeTabId) {
clearSelectionInternal();
await stopInspectQuietly("built_in_browser.render_process_gone_stop_inspect_failed");
}
if (tab.webContents !== crashedWebContents) {
emitError(new Error(`${exitMessage} Recovery skipped because the tab target changed.`));
return;
}
if (crashedWebContents.isDestroyed()) {
emitError(new Error(`${exitMessage} Recovery skipped because the browser tab was destroyed.`));
return;
}
await crashedWebContents.loadURL("about:blank");
emitError(new Error(`${exitMessage} Recovered the tab to a blank page.`));
} catch (error) {
logger()?.warn("built_in_browser.render_process_recovery_failed", {
tabId: tab.id,
reason,
err: errorMessage(error),
});
emitError(new Error(`ADE browser tab renderer exited and recovery failed: ${errorMessage(error)}`));
} finally {
renderProcessRecoveryTabs.delete(tab.id);
notifyTabActivity(tab);
emitStatus();
}
};

const configureBrowserWebContents = (wc: WebContents): void => {
if (configuredWebContents.has(wc)) return;
configuredWebContents.add(wc);
Expand All @@ -1073,12 +1130,18 @@ function createBuiltInBrowserWindowService(args: {
timestamp: new Date().toISOString(),
});
});
wc.setWindowOpenHandler(({ url }) => {
const tab = createPopupTabState(url, tabForWebContents(wc) ?? activeTab());
if (!tab) return { action: "deny" };
wc.setWindowOpenHandler((details) => {
const opener = tabForWebContents(wc) ?? activeTab();
const popupUrl = popupUrlForOpen(details.url);
if (!popupUrl) return { action: "deny" };
return {
action: "allow",
createWindow: () => tab.webContents,
createWindow: () => {
const tab = createPopupTabStateFromView(popupUrl, opener, new WebContentsView({
webPreferences: browserWebPreferences(),
}));
return tab.webContents;
},
};
});
wc.on("will-navigate", (event, url) => {
Expand Down Expand Up @@ -1109,12 +1172,21 @@ function createBuiltInBrowserWindowService(args: {
emitStatus();
});
wc.on("render-process-gone", (_event, details) => {
const tab = tabForWebContents(wc);
logger()?.warn("built_in_browser.render_process_gone", {
reason: details.reason,
exitCode: details.exitCode,
tabId: tab?.id ?? null,
url: tab && !wc.isDestroyed() ? urlForBrowserLog(wc.getURL()) : null,
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.
if (!tab) {
emitStatus();
return;
}
void recoverTabAfterRenderProcessGone(tab, details).catch((error) => {
emitError(new Error(`ADE browser tab renderer recovery failed: ${errorMessage(error)}`));
emitStatus();
});
Comment thread
greptile-apps[bot] marked this conversation as resolved.
notifyTabActivity(tabForWebContents(wc));
emitStatus();
});
wc.on("did-fail-load", (_event, errorCode, errorDescription, validatedURL, isMainFrame) => {
if (!isMainFrame || errorCode === -3) return;
Expand Down Expand Up @@ -1142,19 +1214,16 @@ function createBuiltInBrowserWindowService(args: {
});
};

const createTabState = (): BrowserTabState => {
configureBrowserSession();
const browserWebPreferences = (): Electron.WebPreferences => ({
partition: args.profile.partition,
nodeIntegration: false,
contextIsolation: true,
sandbox: true,
webSecurity: true,
backgroundThrottling: false,
});

const nextView = new WebContentsView({
webPreferences: {
partition: args.profile.partition,
nodeIntegration: false,
contextIsolation: true,
sandbox: true,
webSecurity: true,
backgroundThrottling: false,
},
});
const createTabStateForView = (nextView: WebContentsView): BrowserTabState => {
nextView.setBackgroundColor("#111827");
nextView.setBounds(toElectronRect(bounds));
nextView.setVisible(false);
Expand All @@ -1179,7 +1248,14 @@ function createBuiltInBrowserWindowService(args: {
};
};

const createPopupTabState = (url: string, opener: BrowserTabState | null = activeTab()): BrowserTabState | null => {
const createTabState = (): BrowserTabState => {
configureBrowserSession();
return createTabStateForView(new WebContentsView({
webPreferences: browserWebPreferences(),
}));
};

const popupUrlForOpen = (url: string): string | null => {
const popupUrl = stringOrNull(url) ?? "about:blank";
if (!isAllowedNavigationUrl(popupUrl)) {
emitError(new Error(`Blocked unsupported browser popup protocol: ${url}`));
Expand All @@ -1189,7 +1265,16 @@ function createBuiltInBrowserWindowService(args: {
emitError(new Error(`ADE browser is limited to ${MAX_BROWSER_TABS} tabs. Close a tab before opening another.`));
return null;
}
const tab = createTabState();
return popupUrl;
};

const createPopupTabStateFromView = (
popupUrl: string,
opener: BrowserTabState | null,
nextView: WebContentsView,
): BrowserTabState => {
configureBrowserSession();
const tab = createTabStateForView(nextView);
copyTabOwner(opener, tab);
tabs = [...tabs, tab];
activeTabId = tab.id;
Expand Down Expand Up @@ -2824,6 +2909,19 @@ function emptyToNull(value: string): string | null {
return trimmed.length ? trimmed : null;
}

function urlForBrowserLog(value: string): string | null {
const url = emptyToNull(value);
if (!url) return null;
try {
const parsed = new URL(url);
if (parsed.protocol === "http:" || parsed.protocol === "https:") return parsed.origin;
if (parsed.protocol === "about:") return parsed.href === "about:blank" ? parsed.href : "about:";
return parsed.protocol;
} catch {
return null;
}
}

function tabStatus(tab: BrowserTabState): BuiltInBrowserTab {
const wc = tab.webContents;
return {
Expand Down
Loading