chore: serialize session entries for tool calls and user actions (#803)

This commit is contained in:
Pavel Feldman
2025-07-31 15:16:56 -07:00
committed by GitHub
parent 2a86ac74e3
commit 3787439fc1
6 changed files with 351 additions and 197 deletions

View File

@@ -24,13 +24,14 @@ import type { Tool } from './tools/tool.js';
import type { FullConfig } from './config.js';
import type { BrowserContextFactory } from './browserContextFactory.js';
import type * as actions from './actions.js';
import type { Action, SessionLog } from './sessionLog.js';
import type { SessionLog } from './sessionLog.js';
const testDebug = debug('pw:mcp:test');
export class Context {
readonly tools: Tool[];
readonly config: FullConfig;
readonly sessionLog: SessionLog | undefined;
private _browserContextPromise: Promise<{ browserContext: playwright.BrowserContext, close: () => Promise<void> }> | undefined;
private _browserContextFactory: BrowserContextFactory;
private _tabs: Tab[] = [];
@@ -40,14 +41,13 @@ export class Context {
private static _allContexts: Set<Context> = new Set();
private _closeBrowserContextPromise: Promise<void> | undefined;
private _inputRecorder: InputRecorder | undefined;
private _sessionLog: SessionLog | undefined;
private _isRunningTool: boolean = false;
constructor(tools: Tool[], config: FullConfig, browserContextFactory: BrowserContextFactory, sessionLog: SessionLog | undefined) {
this.tools = tools;
this.config = config;
this._browserContextFactory = browserContextFactory;
this._sessionLog = sessionLog;
this.sessionLog = sessionLog;
testDebug('create context');
Context._allContexts.add(this);
}
@@ -93,29 +93,6 @@ export class Context {
return this._currentTab!;
}
async listTabsMarkdown(force: boolean = false): Promise<string[]> {
if (this._tabs.length === 1 && !force)
return [];
if (!this._tabs.length) {
return [
'### Open tabs',
'No open tabs. Use the "browser_navigate" tool to navigate to a page first.',
'',
];
}
const lines: string[] = ['### Open tabs'];
for (let i = 0; i < this._tabs.length; i++) {
const tab = this._tabs[i];
const title = await tab.title();
const url = tab.page.url();
const current = tab === this._currentTab ? ' (current)' : '';
lines.push(`- ${i}:${current} [${title}] (${url})`);
}
lines.push('');
return lines;
}
async closeTab(index: number | undefined): Promise<string> {
const tab = index === undefined ? this._currentTab : this._tabs[index];
@@ -152,8 +129,12 @@ export class Context {
this._closeBrowserContextPromise = undefined;
}
async setInputRecorderEnabled(enabled: boolean) {
await this._inputRecorder?.setEnabled(enabled);
isRunningTool() {
return this._isRunningTool;
}
setRunningTool(isRunningTool: boolean) {
this._isRunningTool = isRunningTool;
}
private async _closeBrowserContextImpl() {
@@ -208,8 +189,8 @@ export class Context {
const result = await this._browserContextFactory.createContext(this.clientVersion!);
const { browserContext } = result;
await this._setupRequestInterception(browserContext);
if (this._sessionLog)
this._inputRecorder = await InputRecorder.create(this._sessionLog, browserContext);
if (this.sessionLog)
await InputRecorder.create(this, browserContext);
for (const page of browserContext.pages())
this._onPageCreated(page);
browserContext.on('page', page => this._onPageCreated(page));
@@ -226,87 +207,54 @@ export class Context {
}
export class InputRecorder {
private _actions: Action[] = [];
private _enabled = false;
private _sessionLog: SessionLog;
private _context: Context;
private _browserContext: playwright.BrowserContext;
private _flushTimer: NodeJS.Timeout | undefined;
private constructor(sessionLog: SessionLog, browserContext: playwright.BrowserContext) {
this._sessionLog = sessionLog;
private constructor(context: Context, browserContext: playwright.BrowserContext) {
this._context = context;
this._browserContext = browserContext;
}
static async create(sessionLog: SessionLog, browserContext: playwright.BrowserContext) {
const recorder = new InputRecorder(sessionLog, browserContext);
static async create(context: Context, browserContext: playwright.BrowserContext) {
const recorder = new InputRecorder(context, browserContext);
await recorder._initialize();
await recorder.setEnabled(true);
return recorder;
}
private async _initialize() {
const sessionLog = this._context.sessionLog!;
await (this._browserContext as any)._enableRecorder({
mode: 'recording',
recorderMode: 'api',
}, {
actionAdded: (page: playwright.Page, data: actions.ActionInContext, code: string) => {
if (!this._enabled)
if (this._context.isRunningTool())
return;
const tab = Tab.forPage(page);
this._actions.push({ ...data, tab, code: code.trim(), timestamp: performance.now() });
this._scheduleFlush();
if (tab)
sessionLog.logUserAction(data.action, tab, code, false);
},
actionUpdated: (page: playwright.Page, data: actions.ActionInContext, code: string) => {
if (!this._enabled)
if (this._context.isRunningTool())
return;
const tab = Tab.forPage(page);
this._actions[this._actions.length - 1] = { ...data, tab, code: code.trim(), timestamp: performance.now() };
this._scheduleFlush();
if (tab)
sessionLog.logUserAction(data.action, tab, code, true);
},
signalAdded: (page: playwright.Page, data: actions.SignalInContext) => {
if (this._context.isRunningTool())
return;
if (data.signal.name !== 'navigation')
return;
const tab = Tab.forPage(page);
this._actions.push({
frame: data.frame,
action: {
name: 'navigate',
url: data.signal.url,
signals: [],
},
startTime: data.timestamp,
endTime: data.timestamp,
tab,
code: `await page.goto('${data.signal.url}');`,
timestamp: performance.now(),
});
this._scheduleFlush();
const navigateAction: actions.Action = {
name: 'navigate',
url: data.signal.url,
signals: [],
};
if (tab)
sessionLog.logUserAction(navigateAction, tab, `await page.goto('${data.signal.url}');`, false);
},
});
}
async setEnabled(enabled: boolean) {
this._enabled = enabled;
if (!enabled)
await this._flush();
}
private _clearTimer() {
if (this._flushTimer) {
clearTimeout(this._flushTimer);
this._flushTimer = undefined;
}
}
private _scheduleFlush() {
this._clearTimer();
this._flushTimer = setTimeout(() => this._flush(), 1000);
}
private async _flush() {
this._clearTimer();
const actions = this._actions;
this._actions = [];
await this._sessionLog.logActions(actions);
}
}