feat: add 50% threshold for diffing - show full content when changes are large

When showDiffSinceLastCall detects >50% of lines changed, returns full
new content instead of a diff patch. This makes large changes more readable.
This commit is contained in:
Tommy D. Rossi
2026-02-01 17:14:11 +01:00
parent 8532de46fc
commit 453e0783e6
4 changed files with 208 additions and 13 deletions
+6 -7
View File
@@ -1,6 +1,6 @@
import { Page, Locator } from 'playwright-core'
import { createPatch } from 'diff'
import { formatHtmlForPrompt } from './htmlrewrite.js'
import { createSmartDiff } from './diff-utils.js'
export interface GetCleanHTMLOptions {
locator: Locator | Page
@@ -81,16 +81,15 @@ export async function getCleanHTML(options: GetCleanHTMLOptions): Promise<string
return 'No previous snapshot available. This is the first call for this locator. Full snapshot stored for next diff.'
}
const patch = createPatch('html', previousSnapshot, htmlStr, 'previous', 'current', {
context: 3,
const diffResult = createSmartDiff({
oldContent: previousSnapshot,
newContent: htmlStr,
label: 'html',
})
pageSnapshots.set(snapshotKey, htmlStr)
if (patch.split('\n').length <= 4) {
return 'No changes detected since last snapshot'
}
return patch
return diffResult.content
}
// Store snapshot for future diffs
+119
View File
@@ -0,0 +1,119 @@
import { describe, it, expect } from 'vitest'
import { createSmartDiff } from './diff-utils.js'
describe('createSmartDiff', () => {
it('returns no-change when content is identical', () => {
const result = createSmartDiff({
oldContent: 'hello\nworld',
newContent: 'hello\nworld',
})
expect(result.type).toBe('no-change')
expect(result.content).toBe('No changes detected since last snapshot')
})
it('returns diff for small changes', () => {
const result = createSmartDiff({
oldContent: 'line1\nline2\nline3\nline4\nline5',
newContent: 'line1\nline2-modified\nline3\nline4\nline5',
})
expect(result.type).toBe('diff')
expect(result.content).toContain('-line2')
expect(result.content).toContain('+line2-modified')
})
it('returns full content when changes exceed 50% threshold', () => {
const result = createSmartDiff({
oldContent: 'a\nb\nc\nd',
newContent: 'x\ny\nz\nw',
})
expect(result.type).toBe('full')
expect(result.content).toContain('Content changed significantly')
expect(result.content).toContain('x\ny\nz\nw')
})
it('respects custom threshold', () => {
// 2 out of 4 lines changed = 50%
const oldContent = 'a\nb\nc\nd'
const newContent = 'a\nX\nY\nd'
// With 40% threshold, should return full (50% >= 40%)
const resultLow = createSmartDiff({
oldContent,
newContent,
threshold: 0.4,
})
expect(resultLow.type).toBe('full')
// With 60% threshold, should return diff (50% < 60%)
const resultHigh = createSmartDiff({
oldContent,
newContent,
threshold: 0.6,
})
expect(resultHigh.type).toBe('diff')
})
it('uses custom label in diff output', () => {
const result = createSmartDiff({
oldContent: 'line1\nline2\nline3\nline4\nline5',
newContent: 'line1\nmodified\nline3\nline4\nline5',
label: 'snapshot',
threshold: 0.5, // 20% change, will return diff
})
expect(result.type).toBe('diff')
expect(result.content).toContain('--- snapshot')
expect(result.content).toContain('+++ snapshot')
})
it('handles empty old content (all additions)', () => {
const result = createSmartDiff({
oldContent: '',
newContent: 'new\ncontent\nhere',
})
// 100% new content should trigger full
expect(result.type).toBe('full')
expect(result.content).toContain('new\ncontent\nhere')
})
it('handles empty new content (all deletions)', () => {
const result = createSmartDiff({
oldContent: 'old\ncontent\nhere',
newContent: '',
})
expect(result.type).toBe('full')
expect(result.content).toContain('100%')
})
it('handles single line changes', () => {
const result = createSmartDiff({
oldContent: 'single line',
newContent: 'modified line',
})
// 100% change on single line
expect(result.type).toBe('full')
})
it('calculates percentage correctly in output', () => {
// 4 lines changed out of 5 = 80%
const result = createSmartDiff({
oldContent: 'a\nb\nc\nd\ne',
newContent: 'a\nX\nY\nZ\nW',
})
expect(result.type).toBe('full')
expect(result.content).toContain('(80% of lines)')
})
it('includes context lines in diff output', () => {
const result = createSmartDiff({
oldContent: 'line1\nline2\nline3\nline4\nline5\nline6\nline7\nline8\nline9\nline10',
newContent: 'line1\nline2\nline3\nline4\nMODIFIED\nline6\nline7\nline8\nline9\nline10',
threshold: 0.9, // Force diff
})
expect(result.type).toBe('diff')
// Should have context around the change
expect(result.content).toContain('line4')
expect(result.content).toContain('-line5')
expect(result.content).toContain('+MODIFIED')
expect(result.content).toContain('line6')
})
})
+75
View File
@@ -0,0 +1,75 @@
import { structuredPatch } from 'diff'
export interface SmartDiffResult {
type: 'diff' | 'full' | 'no-change'
content: string
}
export interface CreateSmartDiffOptions {
oldContent: string
newContent: string
/** Threshold ratio (0-1) above which full content is returned instead of diff. Default 0.5 (50%) */
threshold?: number
/** Label for the diff output */
label?: string
}
/**
* Creates a smart diff that returns full content when changes exceed threshold.
*
* When more than `threshold` (default 50%) of lines have changed, showing a diff
* is not useful - we return the full new content instead.
*/
export function createSmartDiff(options: CreateSmartDiffOptions): SmartDiffResult {
const { oldContent, newContent, threshold = 0.5, label = 'content' } = options
const patch = structuredPatch(label, label, oldContent, newContent, 'previous', 'current', {
context: 3,
})
// Count added and removed lines
let addedLines = 0
let removedLines = 0
for (const hunk of patch.hunks) {
for (const line of hunk.lines) {
if (line.startsWith('+')) {
addedLines++
} else if (line.startsWith('-')) {
removedLines++
}
}
}
// No changes
if (addedLines === 0 && removedLines === 0) {
return { type: 'no-change', content: 'No changes detected since last snapshot' }
}
// Calculate change ratio: use max(added, removed) since a replacement counts as both
// This ensures the ratio stays in 0-100% range
const oldLineCount = oldContent.split('\n').length
const newLineCount = newContent.split('\n').length
const maxLines = Math.max(oldLineCount, newLineCount, 1) // Avoid division by zero
const changedLines = Math.max(addedLines, removedLines)
const changeRatio = Math.min(changedLines / maxLines, 1) // Cap at 100%
if (changeRatio >= threshold) {
const percentChanged = Math.round(changeRatio * 100)
return {
type: 'full',
content: `Content changed significantly (${percentChanged}% of lines). Full new content:\n\n${newContent}`,
}
}
// Build unified diff string from structured patch
const diffLines: string[] = [
`--- ${label} (previous)`,
`+++ ${label} (current)`,
]
for (const hunk of patch.hunks) {
diffLines.push(`@@ -${hunk.oldStart},${hunk.oldLines} +${hunk.newStart},${hunk.newLines} @@`)
diffLines.push(...hunk.lines)
}
return { type: 'diff', content: diffLines.join('\n') }
}
+8 -6
View File
@@ -12,7 +12,7 @@ import util from 'node:util'
import { createRequire } from 'node:module'
import { fileURLToPath } from 'node:url'
import vm from 'node:vm'
import { createPatch } from 'diff'
import { createSmartDiff } from './diff-utils.js'
import { getCdpUrl } from './utils.js'
import { waitForPageLoad, WaitForPageLoadOptions, WaitForPageLoadResult } from './wait-for-page-load.js'
import { getCDPSessionForPage, CDPSession, ICDPSession } from './cdp-session.js'
@@ -427,11 +427,13 @@ export class PlaywrightExecutor {
this.lastSnapshots.set(targetPage, snapshotStr)
return 'No previous snapshot available. This is the first call for this page. Full snapshot stored for next diff.'
}
const patch = createPatch('snapshot', previousSnapshot, snapshotStr, 'previous', 'current', { context: 3 })
if (patch.split('\n').length <= 4) {
return 'No changes detected since last snapshot'
}
return patch
const diffResult = createSmartDiff({
oldContent: previousSnapshot,
newContent: snapshotStr,
label: 'snapshot',
})
this.lastSnapshots.set(targetPage, snapshotStr)
return diffResult.content
}
this.lastSnapshots.set(targetPage, snapshotStr)