Skip to content

Commit 8dd62bd

Browse files
committed
feat: add session-based snapshot diffing to take_snapshot (--diff)
1 parent d177419 commit 8dd62bd

9 files changed

Lines changed: 779 additions & 660 deletions

File tree

src/McpContext.ts

Lines changed: 48 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -728,6 +728,9 @@ export class McpContext implements Context {
728728
page: McpPage,
729729
verbose = false,
730730
devtoolsData: DevToolsData | undefined = undefined,
731+
options: {
732+
diff?: boolean;
733+
} = {},
731734
): Promise<void> {
732735
const rootNode = await page.pptrPage.accessibility.snapshot({
733736
includeIframes: true,
@@ -745,10 +748,20 @@ export class McpContext implements Context {
745748
let idCounter = 0;
746749
const idToNode = new Map<string, TextSnapshotNode>();
747750
const seenUniqueIds = new Set<string>();
748-
const assignIds = (node: SerializedAXNode): TextSnapshotNode => {
751+
const assignIds = (
752+
node: SerializedAXNode,
753+
parentId = 'root',
754+
index = 0,
755+
): TextSnapshotNode => {
749756
let id = '';
750-
// @ts-expect-error untyped loaderId & backendNodeId.
751-
const uniqueBackendId = `${node.loaderId}_${node.backendNodeId}`;
757+
const nodeAny = node as any;
758+
// StaticText nodes often have unstable backendNodeIds in some contexts,
759+
// or we might want to group them by their parent.
760+
const uniqueBackendId =
761+
nodeAny.backendNodeId && node.role !== 'StaticText'
762+
? `${nodeAny.loaderId}_${nodeAny.backendNodeId}`
763+
: `${nodeAny.loaderId}_${nodeAny.role}_${parentId}_${index}`;
764+
752765
if (uniqueBackendNodeIdToMcpId.has(uniqueBackendId)) {
753766
// Re-use MCP exposed ID if the uniqueId is the same.
754767
id = uniqueBackendNodeIdToMcpId.get(uniqueBackendId)!;
@@ -763,7 +776,7 @@ export class McpContext implements Context {
763776
...node,
764777
id,
765778
children: node.children
766-
? node.children.map(child => assignIds(child))
779+
? node.children.map((child, i) => assignIds(child, id, i))
767780
: [],
768781
};
769782

@@ -788,7 +801,38 @@ export class McpContext implements Context {
788801
hasSelectedElement: false,
789802
verbose,
790803
};
804+
805+
if (options.diff && page.lastSnapshot) {
806+
const lastIdToNode = page.lastSnapshot.idToNode;
807+
const added: string[] = [];
808+
const changed: string[] = [];
809+
const removed: string[] = [];
810+
811+
for (const [id, node] of idToNode) {
812+
const lastNode = lastIdToNode.get(id);
813+
if (!lastNode) {
814+
added.push(id);
815+
} else if (
816+
node.name !== lastNode.name ||
817+
node.value !== lastNode.value ||
818+
node.description !== lastNode.description ||
819+
node.role !== lastNode.role
820+
) {
821+
changed.push(id);
822+
}
823+
}
824+
825+
for (const id of lastIdToNode.keys()) {
826+
if (!idToNode.has(id)) {
827+
removed.push(id);
828+
}
829+
}
830+
831+
snapshot.diff = {added, changed, removed};
832+
}
833+
791834
page.textSnapshot = snapshot;
835+
page.lastSnapshot = snapshot;
792836
const data = devtoolsData ?? (await this.getDevToolsData(page));
793837
if (data?.cdpBackendNodeId) {
794838
snapshot.hasSelectedElement = true;

src/McpPage.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ export class McpPage implements ContextPage {
3333

3434
// Snapshot
3535
textSnapshot: TextSnapshot | null = null;
36+
lastSnapshot: TextSnapshot | null = null;
3637
uniqueBackendNodeIdToMcpId = new Map<string, string>();
3738

3839
// Emulation
@@ -45,14 +46,19 @@ export class McpPage implements ContextPage {
4546
// Dialog
4647
#dialog?: Dialog;
4748
#dialogHandler: (dialog: Dialog) => void;
49+
#navigationHandler: () => void;
4850

4951
constructor(page: Page, id: number) {
5052
this.pptrPage = page;
5153
this.id = id;
5254
this.#dialogHandler = (dialog: Dialog): void => {
5355
this.#dialog = dialog;
5456
};
57+
this.#navigationHandler = (): void => {
58+
this.lastSnapshot = null;
59+
};
5560
page.on('dialog', this.#dialogHandler);
61+
page.on('framenavigated', this.#navigationHandler);
5662
}
5763

5864
get dialog(): Dialog | undefined {
@@ -93,6 +99,7 @@ export class McpPage implements ContextPage {
9399

94100
dispose(): void {
95101
this.pptrPage.off('dialog', this.#dialogHandler);
102+
this.pptrPage.off('framenavigated', this.#navigationHandler);
96103
}
97104

98105
async getElementByUid(uid: string): Promise<ElementHandle<Element>> {

src/McpResponse.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,9 @@ export class McpResponse implements Response {
276276
this.#page,
277277
this.#snapshotParams.verbose,
278278
this.#devToolsData,
279+
{
280+
diff: this.#snapshotParams.diff,
281+
},
279282
);
280283
const textSnapshot = this.#page.textSnapshot;
281284
if (textSnapshot) {

0 commit comments

Comments
 (0)