Implement --save-session functionality with session logging

Co-authored-by: pavelfeldman <883973+pavelfeldman@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot]
2025-07-21 00:56:15 +00:00
parent 173637e1d2
commit f00f78491a
5 changed files with 146 additions and 1 deletions

View File

@@ -44,6 +44,7 @@ export type CLIOptions = {
proxyBypass?: string;
proxyServer?: string;
saveTrace?: boolean;
saveSession?: boolean;
storageState?: string;
userAgent?: string;
userDataDir?: string;
@@ -191,6 +192,7 @@ export function configFromCLIOptions(cliOptions: CLIOptions): Config {
blockedOrigins: cliOptions.blockedOrigins,
},
saveTrace: cliOptions.saveTrace,
saveSession: cliOptions.saveSession,
outputDir: cliOptions.outputDir,
imageResponses: cliOptions.imageResponses,
};
@@ -221,6 +223,7 @@ function configFromEnv(): Config {
options.proxyBypass = envToString(process.env.PLAYWRIGHT_MCP_PROXY_BYPASS);
options.proxyServer = envToString(process.env.PLAYWRIGHT_MCP_PROXY_SERVER);
options.saveTrace = envToBoolean(process.env.PLAYWRIGHT_MCP_SAVE_TRACE);
options.saveSession = envToBoolean(process.env.PLAYWRIGHT_MCP_SAVE_SESSION);
options.storageState = envToString(process.env.PLAYWRIGHT_MCP_STORAGE_STATE);
options.userAgent = envToString(process.env.PLAYWRIGHT_MCP_USER_AGENT);
options.userDataDir = envToString(process.env.PLAYWRIGHT_MCP_USER_DATA_DIR);

View File

@@ -16,6 +16,8 @@
import debug from 'debug';
import * as playwright from 'playwright';
import * as fs from 'fs';
import * as path from 'path';
import { callOnPageNoTrace, waitForCompletion } from './tools/utils.js';
import { ManualPromise } from './manualPromise.js';
@@ -42,6 +44,7 @@ export class Context {
private _modalStates: (ModalState & { tab: Tab })[] = [];
private _pendingAction: PendingAction | undefined;
private _downloads: { download: playwright.Download, finished: boolean, outputFile: string }[] = [];
private _sessionFile: string | undefined;
clientVersion: { name: string; version: string; } | undefined;
constructor(tools: Tool[], config: FullConfig, browserContextFactory: BrowserContextFactory) {
@@ -49,6 +52,9 @@ export class Context {
this.config = config;
this._browserContextFactory = browserContextFactory;
testDebug('create context');
if (this.config.saveSession) {
void this._initializeSessionFile();
}
}
clientSupportsImages(): boolean {
@@ -129,6 +135,43 @@ export class Context {
return await this.listTabsMarkdown();
}
private async _initializeSessionFile() {
if (!this.config.saveSession)
return;
const timestamp = new Date().toISOString();
const fileName = `session${timestamp}.yml`;
this._sessionFile = await outputFile(this.config, fileName);
// Initialize empty session file
await fs.promises.writeFile(this._sessionFile, '# Session log started at ' + timestamp + '\n', 'utf8');
}
private async _logSessionEntry(toolName: string, params: Record<string, unknown>, snapshotFile?: string) {
if (!this.config.saveSession || !this._sessionFile)
return;
const entry = [
`- ${toolName}:`,
' params:',
];
// Add parameters with proper YAML indentation
for (const [key, value] of Object.entries(params)) {
const yamlValue = typeof value === 'string' ? value : JSON.stringify(value);
entry.push(` ${key}: ${yamlValue}`);
}
// Add snapshot reference if provided
if (snapshotFile) {
entry.push(` snapshot: ${path.basename(snapshotFile)}`);
}
entry.push(''); // Empty line for readability
await fs.promises.appendFile(this._sessionFile, entry.join('\n') + '\n', 'utf8');
}
async run(tool: Tool, params: Record<string, unknown> | undefined) {
// Tab management is done outside of the action() call.
const toolResult = await tool.handle(this, tool.schema.inputSchema.parse(params || {}));
@@ -147,6 +190,8 @@ export class Context {
}
const tab = this.currentTabOrDie();
let snapshotFile: string | undefined;
// TODO: race against modal dialogs to resolve clicks.
const actionResult = await this._raceAgainstModalDialogs(async () => {
try {
@@ -155,11 +200,24 @@ export class Context {
else
return await action?.() ?? undefined;
} finally {
if (captureSnapshot && !this._javaScriptBlocked())
if (captureSnapshot && !this._javaScriptBlocked()) {
await tab.captureSnapshot();
// Save snapshot to file if session logging is enabled
if (this.config.saveSession && tab.hasSnapshot()) {
const timestamp = new Date().toISOString();
const snapshotFileName = `${timestamp}.snapshot.yaml`;
snapshotFile = await outputFile(this.config, snapshotFileName);
await fs.promises.writeFile(snapshotFile, tab.snapshotOrDie().text(), 'utf8');
}
}
}
});
// Log session entry if enabled
if (this.config.saveSession) {
await this._logSessionEntry(tool.schema.name, params || {}, snapshotFile);
}
const result: string[] = [];
result.push(`### Ran Playwright code
\`\`\`js

View File

@@ -47,6 +47,7 @@ program
.option('--proxy-bypass <bypass>', 'comma-separated domains to bypass proxy, for example ".com,chromium.org,.domain.com"')
.option('--proxy-server <proxy>', 'specify proxy server, for example "http://myproxy:3128" or "socks5://myproxy:8080"')
.option('--save-trace', 'Whether to save the Playwright Trace of the session into the output directory.')
.option('--save-session', 'Whether to save the session log with tool calls and snapshots into the output directory.')
.option('--storage-state <path>', 'path to the storage state file for isolated sessions.')
.option('--user-agent <ua string>', 'specify user agent string')
.option('--user-data-dir <path>', 'path to the user data directory. If not specified, a temporary directory will be created.')