Skip to content

fix(mcp): browser navigate waitForPageAndFramesLoad #209

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Mar 18, 2025
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
2 changes: 1 addition & 1 deletion apps/omega/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
"package": "electron-forge package",
"build": "rimraf dist out && npm run typecheck && npm run build:reporter && electron-vite build && electron-forge make",
"test": "vitest run",
"publish:mac": "electron-vite build && electron-forge publish --arch=universal --platform=darwin",
"publish:mac": "npm run build:reporter && electron-vite build && electron-forge publish --arch=universal --platform=darwin",
"build:reporter": "vite build"
},
"peerDependencies": {
Expand Down
233 changes: 5 additions & 228 deletions packages/agent-infra/browser-use/src/browser/page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import {
type PageState,
} from './types';
import { createLogger, getBuildDomTreeScript } from '../utils';
import { waitForPageAndFramesLoad } from './utils';

const logger = createLogger('Page');

Expand Down Expand Up @@ -1038,235 +1039,11 @@ export default class Page {
return false;
}

async waitForPageLoadState(timeout?: number) {
const timeoutValue = timeout || 8000;
await this._puppeteerPage?.waitForNavigation({ timeout: timeoutValue });
}

private async _waitForStableNetwork() {
if (!this._puppeteerPage) {
throw new Error('Puppeteer page is not connected');
}

const RELEVANT_RESOURCE_TYPES = new Set([
'document',
'stylesheet',
'image',
'font',
'script',
'iframe',
]);

const RELEVANT_CONTENT_TYPES = new Set([
'text/html',
'text/css',
'application/javascript',
'image/',
'font/',
'application/json',
]);

const IGNORED_URL_PATTERNS = new Set([
// Analytics and tracking
'analytics',
'tracking',
'telemetry',
'beacon',
'metrics',
// Ad-related
'doubleclick',
'adsystem',
'adserver',
'advertising',
// Social media widgets
'facebook.com/plugins',
'platform.twitter',
'linkedin.com/embed',
// Live chat and support
'livechat',
'zendesk',
'intercom',
'crisp.chat',
'hotjar',
// Push notifications
'push-notifications',
'onesignal',
'pushwoosh',
// Background sync/heartbeat
'heartbeat',
'ping',
'alive',
// WebRTC and streaming
'webrtc',
'rtmp://',
'wss://',
// Common CDNs
'cloudfront.net',
'fastly.net',
]);

const pendingRequests = new Set();
let lastActivity = Date.now();

const onRequest = (request: HTTPRequest) => {
// Filter by resource type
const resourceType = request.resourceType();
if (!RELEVANT_RESOURCE_TYPES.has(resourceType)) {
return;
}

// Filter out streaming, websocket, and other real-time requests
if (
['websocket', 'media', 'eventsource', 'manifest', 'other'].includes(
resourceType,
)
) {
return;
}

// Filter out by URL patterns
const url = request.url().toLowerCase();
if (
Array.from(IGNORED_URL_PATTERNS).some((pattern) =>
url.includes(pattern),
)
) {
return;
}

// Filter out data URLs and blob URLs
if (url.startsWith('data:') || url.startsWith('blob:')) {
return;
}

// Filter out requests with certain headers
const headers = request.headers();
if (
// biome-ignore lint/complexity/useLiteralKeys: <explanation>
headers['purpose'] === 'prefetch' ||
headers['sec-fetch-dest'] === 'video' ||
headers['sec-fetch-dest'] === 'audio'
) {
return;
}

pendingRequests.add(request);
lastActivity = Date.now();
};

const onResponse = (response: HTTPResponse) => {
const request = response.request();
if (!pendingRequests.has(request)) {
return;
}

// Filter by content type
const contentType =
response.headers()['content-type']?.toLowerCase() || '';

// Skip streaming content
if (
[
'streaming',
'video',
'audio',
'webm',
'mp4',
'event-stream',
'websocket',
'protobuf',
].some((t) => contentType.includes(t))
) {
pendingRequests.delete(request);
return;
}

// Only process relevant content types
if (
!Array.from(RELEVANT_CONTENT_TYPES).some((ct) =>
contentType.includes(ct),
)
) {
pendingRequests.delete(request);
return;
}

// Skip large responses
const contentLength = response.headers()['content-length'];
if (contentLength && Number.parseInt(contentLength) > 5 * 1024 * 1024) {
// 5MB
pendingRequests.delete(request);
return;
}

pendingRequests.delete(request);
lastActivity = Date.now();
};

// Add event listeners
this._puppeteerPage.on('request', onRequest as any);
this._puppeteerPage.on('response', onResponse as any);

try {
const startTime = Date.now();

// eslint-disable-next-line no-constant-condition
while (true) {
await new Promise((resolve) => setTimeout(resolve, 100));

const now = Date.now();
const timeSinceLastActivity = (now - lastActivity) / 1000; // Convert to seconds

if (
pendingRequests.size === 0 &&
timeSinceLastActivity >= this._config.waitForNetworkIdlePageLoadTime
) {
break;
}

const elapsedTime = (now - startTime) / 1000; // Convert to seconds
if (elapsedTime > this._config.maximumWaitPageLoadTime) {
console.debug(
`Network timeout after ${this._config.maximumWaitPageLoadTime}s with ${pendingRequests.size} pending requests:`,
Array.from(pendingRequests).map((r) => (r as HTTPRequest).url()),
);
break;
}
}
} finally {
// Clean up event listeners
this._puppeteerPage.off('request', onRequest as any);
this._puppeteerPage.off('response', onResponse as any);
}
console.debug(
`Network stabilized for ${this._config.waitForNetworkIdlePageLoadTime} seconds`,
);
}

async waitForPageAndFramesLoad(timeoutOverwrite?: number): Promise<void> {
// Start timing
const startTime = Date.now();

// Wait for page load
try {
await this._waitForStableNetwork();
} catch (error) {
console.warn('Page load failed, continuing...');
}

// Calculate remaining time to meet minimum wait time
const elapsed = (Date.now() - startTime) / 1000; // Convert to seconds
const minWaitTime =
timeoutOverwrite || this._config.minimumWaitPageLoadTime;
const remaining = Math.max(minWaitTime - elapsed, 0);

console.debug(
`--Page loaded in ${elapsed.toFixed(2)} seconds, waiting for additional ${remaining.toFixed(2)} seconds`,
await waitForPageAndFramesLoad(
this._puppeteerPage,
timeoutOverwrite,
this._config,
);

// Sleep remaining time if needed
if (remaining > 0) {
await new Promise((resolve) => setTimeout(resolve, remaining * 1000)); // Convert seconds to milliseconds
}
}
}
3 changes: 3 additions & 0 deletions packages/agent-infra/browser-use/src/browser/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@
*/
import type { DOMState } from '../dom/views';

export type PartialWithRequired<T, K extends keyof T> = Required<Pick<T, K>> &
Partial<Omit<T, K>>;

export interface BrowserContextWindowSize {
width: number;
height: number;
Expand Down
Loading
Loading