chore: serialize session entries for tool calls and user actions (#803)
This commit is contained in:
118
src/context.ts
118
src/context.ts
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user