chore: make tab snapshot structured to mimic it in recorder (#799)
This commit is contained in:
@@ -14,6 +14,10 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { renderModalStates } from './tab.js';
|
||||||
|
|
||||||
|
import type { TabSnapshot } from './tab.js';
|
||||||
|
import type { ModalState } from './tools/tool.js';
|
||||||
import type { ImageContent, TextContent } from '@modelcontextprotocol/sdk/types.js';
|
import type { ImageContent, TextContent } from '@modelcontextprotocol/sdk/types.js';
|
||||||
import type { Context } from './context.js';
|
import type { Context } from './context.js';
|
||||||
|
|
||||||
@@ -24,7 +28,7 @@ export class Response {
|
|||||||
private _context: Context;
|
private _context: Context;
|
||||||
private _includeSnapshot = false;
|
private _includeSnapshot = false;
|
||||||
private _includeTabs = false;
|
private _includeTabs = false;
|
||||||
private _snapshot: string | undefined;
|
private _snapshot: { tabSnapshot?: TabSnapshot, modalState?: ModalState } | undefined;
|
||||||
|
|
||||||
readonly toolName: string;
|
readonly toolName: string;
|
||||||
readonly toolArgs: Record<string, any>;
|
readonly toolArgs: Record<string, any>;
|
||||||
@@ -77,13 +81,13 @@ export class Response {
|
|||||||
this._includeTabs = true;
|
this._includeTabs = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
async snapshot(): Promise<string> {
|
async snapshot(): Promise<{ tabSnapshot?: TabSnapshot, modalState?: ModalState }> {
|
||||||
if (this._snapshot !== undefined)
|
if (this._snapshot)
|
||||||
return this._snapshot;
|
return this._snapshot;
|
||||||
if (this._includeSnapshot && this._context.currentTab())
|
if (this._includeSnapshot && this._context.currentTab())
|
||||||
this._snapshot = await this._context.currentTabOrDie().captureSnapshot();
|
this._snapshot = await this._context.currentTabOrDie().captureSnapshot();
|
||||||
else
|
else
|
||||||
this._snapshot = '';
|
this._snapshot = {};
|
||||||
return this._snapshot;
|
return this._snapshot;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -112,8 +116,14 @@ ${this._code.join('\n')}
|
|||||||
|
|
||||||
// Add snapshot if provided.
|
// Add snapshot if provided.
|
||||||
const snapshot = await this.snapshot();
|
const snapshot = await this.snapshot();
|
||||||
if (snapshot)
|
if (snapshot?.modalState) {
|
||||||
response.push(snapshot, '');
|
response.push(...renderModalStates(this._context, [snapshot.modalState]));
|
||||||
|
response.push('');
|
||||||
|
}
|
||||||
|
if (snapshot?.tabSnapshot) {
|
||||||
|
response.push(renderTabSnapshot(snapshot.tabSnapshot));
|
||||||
|
response.push('');
|
||||||
|
}
|
||||||
|
|
||||||
// Main response part
|
// Main response part
|
||||||
const content: (TextContent | ImageContent)[] = [
|
const content: (TextContent | ImageContent)[] = [
|
||||||
@@ -129,3 +139,41 @@ ${this._code.join('\n')}
|
|||||||
return { content, isError: this._isError };
|
return { content, isError: this._isError };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function renderTabSnapshot(tabSnapshot: TabSnapshot): string {
|
||||||
|
const lines: string[] = [];
|
||||||
|
|
||||||
|
if (tabSnapshot.consoleMessages.length) {
|
||||||
|
lines.push(`### New console messages`);
|
||||||
|
for (const message of tabSnapshot.consoleMessages)
|
||||||
|
lines.push(`- ${trim(message.toString(), 100)}`);
|
||||||
|
lines.push('');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tabSnapshot.downloads.length) {
|
||||||
|
lines.push(`### Downloads`);
|
||||||
|
for (const entry of tabSnapshot.downloads) {
|
||||||
|
if (entry.finished)
|
||||||
|
lines.push(`- Downloaded file ${entry.download.suggestedFilename()} to ${entry.outputFile}`);
|
||||||
|
else
|
||||||
|
lines.push(`- Downloading file ${entry.download.suggestedFilename()} ...`);
|
||||||
|
}
|
||||||
|
lines.push('');
|
||||||
|
}
|
||||||
|
|
||||||
|
lines.push(`### Page state`);
|
||||||
|
lines.push(`- Page URL: ${tabSnapshot.url}`);
|
||||||
|
lines.push(`- Page Title: ${tabSnapshot.title}`);
|
||||||
|
lines.push(`- Page Snapshot:`);
|
||||||
|
lines.push('```yaml');
|
||||||
|
lines.push(tabSnapshot.ariaSnapshot);
|
||||||
|
lines.push('```');
|
||||||
|
|
||||||
|
return lines.join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
function trim(text: string, maxLength: number) {
|
||||||
|
if (text.length <= maxLength)
|
||||||
|
return text;
|
||||||
|
return text.slice(0, maxLength) + '...';
|
||||||
|
}
|
||||||
|
|||||||
@@ -75,9 +75,9 @@ export class SessionLog {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const snapshot = await response.snapshot();
|
const snapshot = await response.snapshot();
|
||||||
if (snapshot) {
|
if (snapshot?.tabSnapshot) {
|
||||||
const fileName = `${prefix}.snapshot.yml`;
|
const fileName = `${prefix}.snapshot.yml`;
|
||||||
await fs.promises.writeFile(path.join(this._folder, fileName), snapshot);
|
await fs.promises.writeFile(path.join(this._folder, fileName), snapshot.tabSnapshot?.ariaSnapshot);
|
||||||
lines.push(`- Snapshot: ${fileName}`);
|
lines.push(`- Snapshot: ${fileName}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
93
src/tab.ts
93
src/tab.ts
@@ -36,6 +36,15 @@ export type TabEventsInterface = {
|
|||||||
[TabEvents.modalState]: [modalState: ModalState];
|
[TabEvents.modalState]: [modalState: ModalState];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type TabSnapshot = {
|
||||||
|
url: string;
|
||||||
|
title: string;
|
||||||
|
ariaSnapshot: string;
|
||||||
|
modalStates: ModalState[];
|
||||||
|
consoleMessages: ConsoleMessage[];
|
||||||
|
downloads: { download: playwright.Download, finished: boolean, outputFile: string }[];
|
||||||
|
};
|
||||||
|
|
||||||
export class Tab extends EventEmitter<TabEventsInterface> {
|
export class Tab extends EventEmitter<TabEventsInterface> {
|
||||||
readonly context: Context;
|
readonly context: Context;
|
||||||
readonly page: playwright.Page;
|
readonly page: playwright.Page;
|
||||||
@@ -90,14 +99,7 @@ export class Tab extends EventEmitter<TabEventsInterface> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
modalStatesMarkdown(): string[] {
|
modalStatesMarkdown(): string[] {
|
||||||
const result: string[] = ['### Modal state'];
|
return renderModalStates(this.context, this.modalStates());
|
||||||
if (this._modalStates.length === 0)
|
|
||||||
result.push('- There is no modal state present');
|
|
||||||
for (const state of this._modalStates) {
|
|
||||||
const tool = this.context.tools.filter(tool => 'clearsModalState' in tool).find(tool => tool.clearsModalState === state.type);
|
|
||||||
result.push(`- [${state.description}]: can be handled by the "${tool?.schema.name}" tool`);
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private _dialogShown(dialog: playwright.Dialog) {
|
private _dialogShown(dialog: playwright.Dialog) {
|
||||||
@@ -180,53 +182,25 @@ export class Tab extends EventEmitter<TabEventsInterface> {
|
|||||||
return this._requests;
|
return this._requests;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _takeRecentConsoleMarkdown(): string[] {
|
async captureSnapshot(): Promise<{ tabSnapshot?: TabSnapshot, modalState?: ModalState }> {
|
||||||
if (!this._recentConsoleMessages.length)
|
let tabSnapshot: TabSnapshot | undefined;
|
||||||
return [];
|
const modalState = await this._raceAgainstModalStates(async () => {
|
||||||
const result = this._recentConsoleMessages.map(message => {
|
|
||||||
return `- ${trim(message.toString(), 100)}`;
|
|
||||||
});
|
|
||||||
return [`### New console messages`, ...result, ''];
|
|
||||||
}
|
|
||||||
|
|
||||||
private _listDownloadsMarkdown(): string[] {
|
|
||||||
if (!this._downloads.length)
|
|
||||||
return [];
|
|
||||||
|
|
||||||
const result: string[] = ['### Downloads'];
|
|
||||||
for (const entry of this._downloads) {
|
|
||||||
if (entry.finished)
|
|
||||||
result.push(`- Downloaded file ${entry.download.suggestedFilename()} to ${entry.outputFile}`);
|
|
||||||
else
|
|
||||||
result.push(`- Downloading file ${entry.download.suggestedFilename()} ...`);
|
|
||||||
}
|
|
||||||
result.push('');
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
async captureSnapshot(): Promise<string> {
|
|
||||||
const result: string[] = [];
|
|
||||||
if (this.modalStates().length) {
|
|
||||||
result.push(...this.modalStatesMarkdown());
|
|
||||||
return result.join('\n');
|
|
||||||
}
|
|
||||||
|
|
||||||
result.push(...this._takeRecentConsoleMarkdown());
|
|
||||||
result.push(...this._listDownloadsMarkdown());
|
|
||||||
|
|
||||||
await this._raceAgainstModalStates(async () => {
|
|
||||||
const snapshot = await (this.page as PageEx)._snapshotForAI();
|
const snapshot = await (this.page as PageEx)._snapshotForAI();
|
||||||
result.push(
|
tabSnapshot = {
|
||||||
`### Page state`,
|
url: this.page.url(),
|
||||||
`- Page URL: ${this.page.url()}`,
|
title: await this.page.title(),
|
||||||
`- Page Title: ${await this.page.title()}`,
|
ariaSnapshot: snapshot,
|
||||||
`- Page Snapshot:`,
|
modalStates: this.modalStates(),
|
||||||
'```yaml',
|
consoleMessages: [],
|
||||||
snapshot,
|
downloads: this._downloads,
|
||||||
'```',
|
};
|
||||||
);
|
|
||||||
});
|
});
|
||||||
return result.join('\n');
|
if (tabSnapshot) {
|
||||||
|
// Assign console message late so that we did not lose any to modal state.
|
||||||
|
tabSnapshot.consoleMessages = this._recentConsoleMessages;
|
||||||
|
this._recentConsoleMessages = [];
|
||||||
|
}
|
||||||
|
return { tabSnapshot, modalState };
|
||||||
}
|
}
|
||||||
|
|
||||||
private _javaScriptBlocked(): boolean {
|
private _javaScriptBlocked(): boolean {
|
||||||
@@ -308,10 +282,15 @@ function pageErrorToConsoleMessage(errorOrValue: Error | any): ConsoleMessage {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function trim(text: string, maxLength: number) {
|
export function renderModalStates(context: Context, modalStates: ModalState[]): string[] {
|
||||||
if (text.length <= maxLength)
|
const result: string[] = ['### Modal state'];
|
||||||
return text;
|
if (modalStates.length === 0)
|
||||||
return text.slice(0, maxLength) + '...';
|
result.push('- There is no modal state present');
|
||||||
|
for (const state of modalStates) {
|
||||||
|
const tool = context.tools.filter(tool => 'clearsModalState' in tool).find(tool => tool.clearsModalState === state.type);
|
||||||
|
result.push(`- [${state.description}]: can be handled by the "${tool?.schema.name}" tool`);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
const tabSymbol = Symbol('tabSymbol');
|
const tabSymbol = Symbol('tabSymbol');
|
||||||
|
|||||||
Reference in New Issue
Block a user