From 4d65bad24f375ec5cd74785b0887e5c9c5343213 Mon Sep 17 00:00:00 2001 From: iliya Date: Sat, 21 Mar 2026 13:30:53 +0200 Subject: [PATCH] fix: consistent diff stats between preview badge and DiffViewer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Export computeDiffLineStats from DiffViewer — uses the same LCS/semantic diff algorithm so stats always match the rendered diff - Remove custom computeDiffStats (set-based) that produced different counts - Add quickStats from toolInput.content for Write tool — shows +N immediately before IPC completes (no need to expand first) - diffStats (accurate, from IPC) replaces quickStats once available --- .../components/chat/viewers/DiffViewer.tsx | 13 ++++ .../team/ToolApprovalDiffPreview.tsx | 68 +++++++------------ 2 files changed, 36 insertions(+), 45 deletions(-) diff --git a/src/renderer/components/chat/viewers/DiffViewer.tsx b/src/renderer/components/chat/viewers/DiffViewer.tsx index 34eab870..3400051d 100644 --- a/src/renderer/components/chat/viewers/DiffViewer.tsx +++ b/src/renderer/components/chat/viewers/DiffViewer.tsx @@ -149,6 +149,19 @@ function generateDiff(oldLines: string[], newLines: string[]): DiffLine[] { return result; } +/** + * Computes diff statistics from old/new strings using the same algorithm as DiffViewer. + */ +export function computeDiffLineStats( + oldString: string, + newString: string +): { added: number; removed: number } { + const oldLines = oldString.split(/\r?\n/); + const newLines = newString.split(/\r?\n/); + const diffLines = generateDiff(oldLines, newLines); + return computeStats(diffLines); +} + /** * Computes diff statistics. */ diff --git a/src/renderer/components/team/ToolApprovalDiffPreview.tsx b/src/renderer/components/team/ToolApprovalDiffPreview.tsx index 6d0a79e7..308aed1a 100644 --- a/src/renderer/components/team/ToolApprovalDiffPreview.tsx +++ b/src/renderer/components/team/ToolApprovalDiffPreview.tsx @@ -1,6 +1,6 @@ import React, { useMemo, useState } from 'react'; -import { DiffViewer } from '@renderer/components/chat/viewers/DiffViewer'; +import { computeDiffLineStats, DiffViewer } from '@renderer/components/chat/viewers/DiffViewer'; import { useToolApprovalDiff } from '@renderer/hooks/useToolApprovalDiff'; import { AlertTriangle, ChevronDown, ChevronRight, FileDiff, Loader2 } from 'lucide-react'; @@ -35,51 +35,18 @@ function saveExpandedPref(value: boolean): void { } // ============================================================================= -// Diff stats helper +// Quick stats from toolInput (before IPC, for Write tool) // ============================================================================= -function computeDiffStats( - oldString: string, - newString: string -): { added: number; removed: number } { - const oldLines = oldString.split(/\r?\n/); - const newLines = newString.split(/\r?\n/); - // Simple line-count based stats (matches DiffViewer's own count) - const maxLen = Math.max(oldLines.length, newLines.length); - let added = 0; - let removed = 0; - // Count lines that differ - if (oldString === '' && newString !== '') { - added = newLines.length; - } else if (newString === '' && oldString !== '') { - removed = oldLines.length; - } else { - // Diff-based: count added/removed from line diff - const oldSet = new Map(); - for (const line of oldLines) { - oldSet.set(line, (oldSet.get(line) ?? 0) + 1); - } - const newSet = new Map(); - for (const line of newLines) { - newSet.set(line, (newSet.get(line) ?? 0) + 1); - } - // Lines in new but not in old - for (const [line, count] of newSet) { - const oldCount = oldSet.get(line) ?? 0; - if (count > oldCount) added += count - oldCount; - } - // Lines in old but not in new - for (const [line, count] of oldSet) { - const newCount = newSet.get(line) ?? 0; - if (count > newCount) removed += count - newCount; - } - // Ensure at least something shows if strings differ but stats are 0 - if (added === 0 && removed === 0 && oldString !== newString) { - added = Math.max(0, newLines.length - maxLen); - removed = Math.max(0, oldLines.length - maxLen); - } +function computeQuickStats( + toolName: string, + toolInput: Record +): { added: number; removed: number } | null { + if (toolName === 'Write' && typeof toolInput.content === 'string') { + const lines = toolInput.content.split(/\r?\n/).length; + return { added: lines, removed: 0 }; } - return { added, removed }; + return null; } // ============================================================================= @@ -95,12 +62,23 @@ export const ToolApprovalDiffPreview: React.FC = ( const [expanded, setExpanded] = useState(loadExpandedPref); const diff = useToolApprovalDiff(toolName, toolInput, requestId, expanded); - const stats = useMemo(() => { + // Stats from actual diff data (after IPC), using the same algorithm as DiffViewer + const diffStats = useMemo(() => { if (!diff.hasDiff || diff.loading || diff.isBinary || diff.error) return null; if (!diff.oldString && !diff.newString) return null; - return computeDiffStats(diff.oldString, diff.newString); + return computeDiffLineStats(diff.oldString, diff.newString); }, [diff.hasDiff, diff.loading, diff.isBinary, diff.error, diff.oldString, diff.newString]); + // Quick stats from toolInput (available immediately, before diff loads) + const quickStats = useMemo( + () => computeQuickStats(toolName, toolInput), + // eslint-disable-next-line react-hooks/exhaustive-deps -- toolInput identity tied to requestId + [toolName, requestId] + ); + + // Prefer accurate diffStats when available, fall back to quickStats + const stats = diffStats ?? quickStats; + if (!DIFF_TOOLS.has(toolName)) return null; const toggleExpanded = (): void => {