diff --git a/cli/src/commands.rs b/cli/src/commands.rs index d113616b..e841246e 100644 --- a/cli/src/commands.rs +++ b/cli/src/commands.rs @@ -432,6 +432,15 @@ pub fn parse_command(args: &[String], flags: &Flags) -> Result { + obj.insert("diff".to_string(), json!(true)); + } + "--output" | "-o" => { + if let Some(p) = rest.get(i + 1) { + obj.insert("output".to_string(), json!(p)); + i += 1; + } + } _ => {} } i += 1; diff --git a/src/actions.ts b/src/actions.ts index 22314e06..9465bad9 100644 --- a/src/actions.ts +++ b/src/actions.ts @@ -643,9 +643,14 @@ async function handleSnapshot( maxDepth?: number; compact?: boolean; selector?: string; + diff?: boolean; + output?: string; }, browser: BrowserManager ): Promise> { + // Save previous snapshot before getSnapshot() updates lastSnapshot + const previousSnapshot = command.diff ? browser.getLastSnapshot() : ''; + // Use enhanced snapshot with refs and optional filtering const { tree, refs } = await browser.getSnapshot({ interactive: command.interactive, @@ -655,6 +660,47 @@ async function handleSnapshot( selector: command.selector, }); + const snapshot = tree || 'Empty page'; + + // Incremental diff: only return changed lines + if (command.diff && previousSnapshot) { + const oldLines = previousSnapshot.split('\n'); + const newLines = snapshot.split('\n'); + // Use frequency maps to correctly handle duplicate lines + const oldFreq = new Map(); + for (const line of oldLines) oldFreq.set(line, (oldFreq.get(line) || 0) + 1); + const newFreq = new Map(); + for (const line of newLines) newFreq.set(line, (newFreq.get(line) || 0) + 1); + + const removed: string[] = []; + for (const [line, count] of oldFreq) { + const diff = count - (newFreq.get(line) || 0); + for (let i = 0; i < diff; i++) removed.push(line); + } + const added: string[] = []; + for (const [line, count] of newFreq) { + const diff = count - (oldFreq.get(line) || 0); + for (let i = 0; i < diff; i++) added.push(line); + } + + const diffOutput = [ + ...removed.map(l => `- ${l}`), + ...added.map(l => `+ ${l}`), + ].join('\n'); + return successResponse(command.id, { snapshot: diffOutput || '(no changes)' }); + } + + // Save to file instead of returning full snapshot + if (command.output) { + const fs = await import('fs'); + const path = await import('path'); + const resolvedPath = path.resolve(command.output); + fs.writeFileSync(resolvedPath, snapshot, 'utf-8'); + return successResponse(command.id, { + snapshot: `Snapshot saved to ${resolvedPath} (${snapshot.length} chars)`, + }); + } + // Simplify refs for output (just role and name) const simpleRefs: Record = {}; for (const [ref, data] of Object.entries(refs)) { @@ -662,7 +708,7 @@ async function handleSnapshot( } return successResponse(command.id, { - snapshot: tree || 'Empty page', + snapshot, refs: Object.keys(simpleRefs).length > 0 ? simpleRefs : undefined, }); } diff --git a/src/browser.ts b/src/browser.ts index d8c7298d..2fce4514 100644 --- a/src/browser.ts +++ b/src/browser.ts @@ -151,6 +151,13 @@ export class BrowserManager { return this.refMap; } + /** + * Get the last snapshot text for diff comparison + */ + getLastSnapshot(): string { + return this.lastSnapshot; + } + /** * Get a locator from a ref (e.g., "e1", "@e1", "ref=e1") * Returns null if ref doesn't exist or is invalid diff --git a/src/protocol.ts b/src/protocol.ts index cb543ede..2b98e2e8 100644 --- a/src/protocol.ts +++ b/src/protocol.ts @@ -750,6 +750,8 @@ const snapshotSchema = baseCommandSchema.extend({ maxDepth: z.number().nonnegative().optional(), compact: z.boolean().optional(), selector: z.string().optional(), + diff: z.boolean().optional(), + output: z.string().optional(), }); const evaluateSchema = baseCommandSchema.extend({ diff --git a/src/types.ts b/src/types.ts index 06a489df..1c136620 100644 --- a/src/types.ts +++ b/src/types.ts @@ -784,6 +784,8 @@ export interface ScreenshotCommand extends BaseCommand { export interface SnapshotCommand extends BaseCommand { action: 'snapshot'; + diff?: boolean; + output?: string; } export interface EvaluateCommand extends BaseCommand {