Skip to content
Closed
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
907 changes: 456 additions & 451 deletions apps/cli/src/index.tsx

Large diffs are not rendered by default.

41 changes: 38 additions & 3 deletions packages/browser/src/browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,9 @@ export class Browser extends ServiceMap.Service<Browser>()("@browser/Browser", {

const contextOptions: Parameters<typeof browser.newContext>[0] = {
ignoreHTTPSErrors: true,
...(options.headed && { viewport: null }),
...(options.viewport
? { viewport: options.viewport }
: options.headed && { viewport: null }),
};
if (profileLocale) {
contextOptions.locale = profileLocale;
Expand Down Expand Up @@ -269,10 +271,43 @@ export class Browser extends ServiceMap.Service<Browser>()("@browser/Browser", {
url ?? "",
defaultBrowserContext.preferredProfile,
);
const formatted = cookies.map((cookie) => cookie.playwrightFormat);
yield* Effect.tryPromise({
try: () => context.addCookies(cookies.map((cookie) => cookie.playwrightFormat)),
try: () => context.addCookies(formatted),
catch: toBrowserLaunchError,
});
}).pipe(
Effect.catchTag("BrowserLaunchError", (batchError) =>
Effect.gen(function* () {
yield* Effect.logDebug(
"Batch addCookies failed, falling back to per-cookie injection",
{ error: batchError.message, totalCookies: formatted.length },
);
const results = yield* Effect.forEach(
formatted,
(cookie) =>
Effect.tryPromise({
try: () => context.addCookies([cookie]),
catch: toBrowserLaunchError,
}).pipe(
Effect.as(true),
Effect.catchTag("BrowserLaunchError", (perCookieError) =>
Effect.logDebug("Skipping invalid cookie", {
cookieName: cookie.name,
cookieDomain: cookie.domain,
error: perCookieError.message,
}).pipe(Effect.as(false)),
),
),
{ concurrency: 1 },
);
const injected = results.filter(Boolean).length;
yield* Effect.logInfo("Cookies injected with per-cookie fallback", {
injected,
skipped: formatted.length - injected,
});
}),
),
);
}

const page = yield* Effect.tryPromise({
Expand Down
3 changes: 3 additions & 0 deletions packages/browser/src/mcp/constants.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import * as path from "node:path";

export const EXPECT_COOKIE_BROWSERS_ENV_NAME = "EXPECT_COOKIE_BROWSERS";
export const EXPECT_COOKIES_ENV_NAME = "EXPECT_COOKIES";
export const EXPECT_CDP_URL_ENV_NAME = "EXPECT_CDP_URL";
export const EXPECT_BASE_URL_ENV_NAME = "EXPECT_BASE_URL";
export const EXPECT_HEADED_ENV_NAME = "EXPECT_HEADED";
export const EXPECT_PROFILE_ENV_NAME = "EXPECT_PROFILE";
export const EXPECT_BROWSER_ENV_NAME = "EXPECT_BROWSER";
export const EXPECT_VIEWPORT_ENV_NAME = "EXPECT_VIEWPORT";
export const DUPLICATE_REQUEST_WINDOW_MS = 500;
export const TMP_ARTIFACT_OUTPUT_DIRECTORY = "/tmp/expect-artifacts";
export const CLI_SESSION_FILE = "/tmp/expect-cli-session.json";
Expand Down
3 changes: 3 additions & 0 deletions packages/browser/src/mcp/index.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
export {
CLI_SESSION_FILE,
EXPECT_COOKIE_BROWSERS_ENV_NAME,
EXPECT_COOKIES_ENV_NAME,
EXPECT_CDP_URL_ENV_NAME,
EXPECT_BASE_URL_ENV_NAME,
EXPECT_HEADED_ENV_NAME,
EXPECT_PROFILE_ENV_NAME,
EXPECT_BROWSER_ENV_NAME,
EXPECT_VIEWPORT_ENV_NAME,
TMP_ARTIFACT_OUTPUT_DIRECTORY,
} from "./constants";
export { McpSession } from "./mcp-session";
Expand Down
91 changes: 25 additions & 66 deletions packages/browser/src/mcp/mcp-session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import {
AGENT_OVERLAY_CONTAINER_ID,
BROWSER_CLOSE_TIMEOUT_MS,
OVERLAY_REINJECT_TIMEOUT_MS,
VIDEO_PATH_TIMEOUT_MS,
} from "../constants";
import type {
AnnotatedScreenshotOptions,
Expand All @@ -24,10 +23,13 @@ import type {
} from "../types";
import {
EXPECT_COOKIE_BROWSERS_ENV_NAME,
EXPECT_COOKIES_ENV_NAME,
EXPECT_CDP_URL_ENV_NAME,
EXPECT_BASE_URL_ENV_NAME,
EXPECT_HEADED_ENV_NAME,
EXPECT_PROFILE_ENV_NAME,
EXPECT_BROWSER_ENV_NAME,
EXPECT_VIEWPORT_ENV_NAME,
TMP_ARTIFACT_OUTPUT_DIRECTORY,
} from "./constants";
import { McpSessionNotOpenError } from "./errors";
Expand Down Expand Up @@ -72,8 +74,6 @@ export interface OpenResult {
}

export interface CloseResult {
readonly videoPath: string | undefined;
readonly tmpVideoPath: string | undefined;
readonly screenshotPaths: readonly string[];
}

Expand All @@ -87,7 +87,6 @@ interface BrowserOpenAnalyticsProperties {
readonly cookie_count: number;
}

const PLAYWRIGHT_VIDEO_SUBDIRECTORY = "playwright";

const setupPageTracking = (page: Page, sessionData: BrowserSessionData) => {
if (sessionData.trackedPages.has(page)) return;
Expand Down Expand Up @@ -150,11 +149,29 @@ export class McpSession extends ServiceMap.Service<McpSession>()("@browser/McpSe
});
const profileConfig = yield* Config.option(Config.string(EXPECT_PROFILE_ENV_NAME));
const configuredProfileName = Option.getOrUndefined(profileConfig);
const cookiesConfig = yield* Config.option(Config.string(EXPECT_COOKIES_ENV_NAME));
const defaultCookiesEnabled = Option.match(cookiesConfig, {
onNone: () => false,
onSome: (value) => value !== "false",
});
const browserConfig = yield* Config.option(Config.string(EXPECT_BROWSER_ENV_NAME));
const defaultBrowserEngine = Option.getOrUndefined(browserConfig) as
| BrowserEngine
| undefined;
Comment thread
NisargIO marked this conversation as resolved.
const viewportConfig = yield* Config.option(Config.string(EXPECT_VIEWPORT_ENV_NAME));
const configuredViewport = Option.match(viewportConfig, {
onNone: () => undefined,
onSome: (value) => {
const match = /^(\d+)x(\d+)$/i.exec(value.trim());
if (!match) return undefined;
return { width: parseInt(match[1]!, 10), height: parseInt(match[2]!, 10) };
},
});
const cookieBrowserKeys = Option.match(cookieBrowsersConfig, {
onNone: (): string[] => [],
onSome: (value) => value.split(",").filter(Boolean),
});
const cookiesDisabled = cookieBrowserKeys.length === 0;
const cookiesDisabled = cookieBrowserKeys.length === 0 && !defaultCookiesEnabled;

const sessionRef = yield* Ref.make<BrowserSessionData | undefined>(undefined);
const preExtractedCookiesRef = yield* Ref.make<Cookie[] | undefined>(undefined);
Expand Down Expand Up @@ -290,23 +307,11 @@ export class McpSession extends ServiceMap.Service<McpSession>()("@browser/McpSe
yield* Effect.annotateCurrentSpan({ url });
yield* Ref.set(savedScreenshotPathsRef, []);

const cookiesOption = yield* resolveCookies(options.cookies);
const videoOutputDir = path.join(
TMP_ARTIFACT_OUTPUT_DIRECTORY,
PLAYWRIGHT_VIDEO_SUBDIRECTORY,
);

yield* fileSystem
.makeDirectory(videoOutputDir, { recursive: true })
.pipe(
Effect.catchCause((cause) =>
Effect.logDebug("Failed to create Playwright video directory", { cause }),
),
);
const cookiesOption = yield* resolveCookies(options.cookies ?? defaultCookiesEnabled);

const explicitCdpUrl = Option.orElse(options.cdpUrl ?? Option.none(), () => defaultCdpUrl);
const headed = options.headed ?? isHeadedDefault;
const engine = options.browserType ?? "chromium";
const engine = options.browserType ?? defaultBrowserEngine ?? "chromium";
yield* Ref.set(isHeadedRef, headed);

const useSystemChrome =
Expand Down Expand Up @@ -362,9 +367,9 @@ export class McpSession extends ServiceMap.Service<McpSession>()("@browser/McpSe
headed,
cookies: cookiesOption,
waitUntil: options.waitUntil,
videoOutputDir,
cdpUrl,
browserType: engine,
viewport: configuredViewport,
});

const sessionData: BrowserSessionData = {
Expand Down Expand Up @@ -493,9 +498,6 @@ export class McpSession extends ServiceMap.Service<McpSession>()("@browser/McpSe

yield* Ref.set(sessionRef, undefined);

const pageVideo = activeSession.page.video();
const artifactBaseName = `session-${Date.now()}`;

if (!activeSession.page.isClosed()) {
yield* evaluateRuntime(
activeSession.page,
Expand Down Expand Up @@ -532,50 +534,7 @@ export class McpSession extends ServiceMap.Service<McpSession>()("@browser/McpSe
),
);

let videoPath: string | undefined;
let tmpVideoPath: string | undefined;

if (pageVideo) {
videoPath = yield* Effect.tryPromise(() => pageVideo.path()).pipe(
Effect.timeoutOrElse({
duration: `${VIDEO_PATH_TIMEOUT_MS} millis`,
onTimeout: () => Effect.succeed(undefined),
}),
Effect.catchCause((cause) =>
Effect.logDebug("Failed to resolve Playwright video path", { cause }).pipe(
Effect.as(undefined),
),
),
);

if (videoPath) {
yield* fileSystem
.makeDirectory(TMP_ARTIFACT_OUTPUT_DIRECTORY, { recursive: true })
.pipe(
Effect.catchCause((cause) =>
Effect.logDebug("Failed to create /tmp artifact directory", { cause }),
),
);

const tmpVideoFilePath = path.join(
TMP_ARTIFACT_OUTPUT_DIRECTORY,
`${artifactBaseName}.webm`,
);

yield* fileSystem
.copyFile(videoPath, tmpVideoFilePath)
.pipe(
Effect.catchCause((cause) =>
Effect.logDebug("Failed to copy video to /tmp", { cause }),
),
);
tmpVideoPath = tmpVideoFilePath;
}
}

return {
videoPath,
tmpVideoPath,
screenshotPaths: yield* Ref.get(savedScreenshotPathsRef),
} satisfies CloseResult;
});
Expand Down
5 changes: 0 additions & 5 deletions packages/browser/src/mcp/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -689,11 +689,6 @@ export const createBrowserMcpServer = <E>(
),
);
const lines = ["Browser closed."];
if (result.tmpVideoPath) {
lines.push(`Playwright video: ${result.tmpVideoPath}`);
} else if (result.videoPath) {
lines.push(`Playwright video: ${result.videoPath}`);
}
for (const screenshotPath of result.screenshotPaths) {
lines.push(`Screenshot: ${screenshotPath}`);
}
Expand Down
1 change: 1 addition & 0 deletions packages/browser/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ export interface CreatePageOptions {
videoOutputDir?: string;
cdpUrl?: Option.Option<string>;
browserType?: BrowserEngine;
viewport?: { width: number; height: number };
}

export interface AnnotatedScreenshotOptions extends SnapshotOptions {
Expand Down
2 changes: 1 addition & 1 deletion packages/cookies/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ export class Cookie extends Schema.Class<Cookie>("@cookies/Cookie")({
expires: this.expires ?? SESSION_EXPIRES,
secure: this.secure,
httpOnly: this.httpOnly,
sameSite: this.sameSite,
...(this.sameSite !== undefined && { sameSite: this.sameSite }),
};
}
}
Expand Down
1 change: 1 addition & 0 deletions tmp/add-mcp
Submodule add-mcp added at a58f24
Loading