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 { Page, Locator } from 'playwright-core'
import { createPatch } from 'diff'
import { formatHtmlForPrompt } from './htmlrewrite.js' import { formatHtmlForPrompt } from './htmlrewrite.js'
import { createSmartDiff } from './diff-utils.js'
export interface GetCleanHTMLOptions { export interface GetCleanHTMLOptions {
locator: Locator | Page 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.' 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', { const diffResult = createSmartDiff({
context: 3, oldContent: previousSnapshot,
newContent: htmlStr,
label: 'html',
}) })
pageSnapshots.set(snapshotKey, htmlStr) pageSnapshots.set(snapshotKey, htmlStr)
if (patch.split('\n').length <= 4) { return diffResult.content
return 'No changes detected since last snapshot'
}
return patch
} }
// Store snapshot for future diffs // 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 { createRequire } from 'node:module'
import { fileURLToPath } from 'node:url' import { fileURLToPath } from 'node:url'
import vm from 'node:vm' import vm from 'node:vm'
import { createPatch } from 'diff' import { createSmartDiff } from './diff-utils.js'
import { getCdpUrl } from './utils.js' import { getCdpUrl } from './utils.js'
import { waitForPageLoad, WaitForPageLoadOptions, WaitForPageLoadResult } from './wait-for-page-load.js' import { waitForPageLoad, WaitForPageLoadOptions, WaitForPageLoadResult } from './wait-for-page-load.js'
import { getCDPSessionForPage, CDPSession, ICDPSession } from './cdp-session.js' import { getCDPSessionForPage, CDPSession, ICDPSession } from './cdp-session.js'
@@ -427,11 +427,13 @@ export class PlaywrightExecutor {
this.lastSnapshots.set(targetPage, snapshotStr) this.lastSnapshots.set(targetPage, snapshotStr)
return 'No previous snapshot available. This is the first call for this page. Full snapshot stored for next diff.' 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 }) const diffResult = createSmartDiff({
if (patch.split('\n').length <= 4) { oldContent: previousSnapshot,
return 'No changes detected since last snapshot' newContent: snapshotStr,
} label: 'snapshot',
return patch })
this.lastSnapshots.set(targetPage, snapshotStr)
return diffResult.content
} }
this.lastSnapshots.set(targetPage, snapshotStr) this.lastSnapshots.set(targetPage, snapshotStr)