diff --git a/static/app/components/events/autofix/useExplorerAutofix.tsx b/static/app/components/events/autofix/useExplorerAutofix.tsx index e5a534bb46423d..3fa9a3b0699c3b 100644 --- a/static/app/components/events/autofix/useExplorerAutofix.tsx +++ b/static/app/components/events/autofix/useExplorerAutofix.tsx @@ -236,27 +236,31 @@ export function getOrderedArtifactKeys( } /** - * Extract file patches from Explorer blocks. + * Extract merged file patches from Explorer blocks. + * Returns the latest merged patch (original → current) for each file. */ -export function getFilePatchesFromBlocks(blocks: Block[]): ExplorerFilePatch[] { - const patches: ExplorerFilePatch[] = []; +export function getMergedFilePatchesFromBlocks(blocks: Block[]): ExplorerFilePatch[] { + const mergedByFile = new Map(); for (const block of blocks) { - if (block.file_patches) { - for (const filePatch of block.file_patches) { - patches.push(filePatch); + if (block.merged_file_patches) { + for (const patch of block.merged_file_patches) { + const key = `${patch.repo_name}:${patch.patch.path}`; + mergedByFile.set(key, patch); } } } - return patches; + return Array.from(mergedByFile.values()); } /** * Check if there are code changes in the state. */ export function hasCodeChanges(blocks: Block[]): boolean { - return blocks.some(block => block.file_patches && block.file_patches.length > 0); + return blocks.some( + block => block.merged_file_patches && block.merged_file_patches.length > 0 + ); } interface UseExplorerAutofixOptions { diff --git a/static/app/components/events/autofix/v2/artifactCards.tsx b/static/app/components/events/autofix/v2/artifactCards.tsx index 04f908a7d70a74..cde958e04d2510 100644 --- a/static/app/components/events/autofix/v2/artifactCards.tsx +++ b/static/app/components/events/autofix/v2/artifactCards.tsx @@ -708,59 +708,13 @@ interface CodeChangesCardProps { prStates?: Record; } -/** - * Merge consecutive patches to the same file into a single unified diff. - * This is needed because the Explorer may create multiple patches for the same file. - */ -function mergeFilePatches(patches: ExplorerFilePatch[]): ExplorerFilePatch[] { - const patchesByFile = new Map(); - - // Group patches by repo + file path - for (const patch of patches) { - const key = `${patch.repo_name}:${patch.patch.path}`; - const existing = patchesByFile.get(key) || []; - existing.push(patch); - patchesByFile.set(key, existing); - } - - // Merge patches for each file - const merged: ExplorerFilePatch[] = []; - for (const [, filePatches] of patchesByFile) { - const firstPatch = filePatches[0]; - if (!firstPatch) { - continue; - } - - if (filePatches.length === 1) { - merged.push(firstPatch); - } else { - // Merge hunks from multiple patches - const mergedHunks = filePatches.flatMap(p => p.patch.hunks); - - merged.push({ - repo_name: firstPatch.repo_name, - patch: { - ...firstPatch.patch, - hunks: mergedHunks, - added: filePatches.reduce((sum, p) => sum + p.patch.added, 0), - removed: filePatches.reduce((sum, p) => sum + p.patch.removed, 0), - }, - }); - } - } - - return merged; -} - /** * Code Changes card showing file diffs. */ export function CodeChangesCard({patches, prStates, onCreatePR}: CodeChangesCardProps) { - const mergedPatches = mergeFilePatches(patches); - // Group by repo const patchesByRepo = new Map(); - for (const patch of mergedPatches) { + for (const patch of patches) { const existing = patchesByRepo.get(patch.repo_name) || []; existing.push(patch); patchesByRepo.set(patch.repo_name, existing); diff --git a/static/app/components/events/autofix/v2/artifactPreviews.tsx b/static/app/components/events/autofix/v2/artifactPreviews.tsx index 7181536471ddd1..c7386ec8e5655e 100644 --- a/static/app/components/events/autofix/v2/artifactPreviews.tsx +++ b/static/app/components/events/autofix/v2/artifactPreviews.tsx @@ -4,7 +4,7 @@ import {Container, Flex} from '@sentry/scraps/layout'; import {Text} from '@sentry/scraps/text'; import { - getFilePatchesFromBlocks, + getMergedFilePatchesFromBlocks, getOrderedArtifactKeys, } from 'sentry/components/events/autofix/useExplorerAutofix'; import {getArtifactIcon} from 'sentry/components/events/autofix/v2/artifactCards'; @@ -53,7 +53,7 @@ function getOneLineDescription( case 'impact_assessment': return getStringField(data, 'one_line_description'); case 'code_changes': { - const filePatches = getFilePatchesFromBlocks(blocks); + const filePatches = getMergedFilePatchesFromBlocks(blocks); if (filePatches.length === 0) { return null; } diff --git a/static/app/components/events/autofix/v2/explorerSeerDrawer.tsx b/static/app/components/events/autofix/v2/explorerSeerDrawer.tsx index 4aa00155123e07..2633a1d3e371b8 100644 --- a/static/app/components/events/autofix/v2/explorerSeerDrawer.tsx +++ b/static/app/components/events/autofix/v2/explorerSeerDrawer.tsx @@ -13,7 +13,7 @@ import AutofixFeedback from 'sentry/components/events/autofix/autofixFeedback'; import { hasCodeChanges as checkHasCodeChanges, getArtifactsFromBlocks, - getFilePatchesFromBlocks, + getMergedFilePatchesFromBlocks, getOrderedArtifactKeys, useExplorerAutofix, type AutofixExplorerStep, @@ -149,7 +149,7 @@ export function ExplorerSeerDrawer({ // Extract data from run state const blocks = useMemo(() => runState?.blocks ?? [], [runState?.blocks]); const artifacts = useMemo(() => getArtifactsFromBlocks(blocks), [blocks]); - const filePatches = useMemo(() => getFilePatchesFromBlocks(blocks), [blocks]); + const mergedPatches = useMemo(() => getMergedFilePatchesFromBlocks(blocks), [blocks]); const loadingBlock = useMemo(() => blocks.find(block => block.loading), [blocks]); const hasChanges = checkHasCodeChanges(blocks); const prStates = runState?.repo_pr_states; @@ -352,10 +352,10 @@ export function ExplorerSeerDrawer({ return null; } })} - {filePatches.length > 0 && ( + {/* Code changes from merged file patches */} + {mergedPatches.length > 0 && ( diff --git a/static/app/views/seerExplorer/prWidget.tsx b/static/app/views/seerExplorer/prWidget.tsx index 135a37407c1be2..f4459884b38b5a 100644 --- a/static/app/views/seerExplorer/prWidget.tsx +++ b/static/app/views/seerExplorer/prWidget.tsx @@ -45,48 +45,56 @@ export function usePRWidgetData({ onCreatePR: (repoName?: string) => void; repoPRStates: Record; }) { - // Compute aggregated stats from all blocks + // Compute aggregated stats from merged patches (latest patch per file) const {totalAdded, totalRemoved, repoStats, repoFileStats} = useMemo(() => { + // Collect latest merged patch per file (later blocks override earlier) + const mergedByFile = new Map< + string, + {added: number; path: string; removed: number; repoName: string} + >(); + + for (const block of blocks) { + if (!block.merged_file_patches) { + continue; + } + for (const filePatch of block.merged_file_patches) { + const key = `${filePatch.repo_name}:${filePatch.patch.path}`; + mergedByFile.set(key, { + repoName: filePatch.repo_name, + path: filePatch.patch.path, + added: filePatch.patch.added, + removed: filePatch.patch.removed, + }); + } + } + + // Aggregate stats from merged patches const stats: Record = {}; const fileStats: Record = {}; let added = 0; let removed = 0; - for (const block of blocks) { - if (!block.file_patches) { - continue; + for (const patch of mergedByFile.values()) { + added += patch.added; + removed += patch.removed; + + if (!stats[patch.repoName]) { + stats[patch.repoName] = {added: 0, removed: 0}; + } + const repoStat = stats[patch.repoName]; + if (repoStat) { + repoStat.added += patch.added; + repoStat.removed += patch.removed; } - for (const filePatch of block.file_patches) { - added += filePatch.patch.added; - removed += filePatch.patch.removed; - if (!stats[filePatch.repo_name]) { - stats[filePatch.repo_name] = {added: 0, removed: 0}; - } - const repoStat = stats[filePatch.repo_name]; - if (repoStat) { - repoStat.added += filePatch.patch.added; - repoStat.removed += filePatch.patch.removed; - } - // Track file-level stats - if (!fileStats[filePatch.repo_name]) { - fileStats[filePatch.repo_name] = []; - } - const repoFiles = fileStats[filePatch.repo_name]; - if (repoFiles) { - const existingFile = repoFiles.find(f => f.path === filePatch.patch.path); - if (existingFile) { - existingFile.added += filePatch.patch.added; - existingFile.removed += filePatch.patch.removed; - } else { - repoFiles.push({ - added: filePatch.patch.added, - path: filePatch.patch.path, - removed: filePatch.patch.removed, - }); - } - } + if (!fileStats[patch.repoName]) { + fileStats[patch.repoName] = []; } + fileStats[patch.repoName]?.push({ + added: patch.added, + path: patch.path, + removed: patch.removed, + }); } return { @@ -107,10 +115,10 @@ export function usePRWidgetData({ let isOutOfSync = !hasPR; // No PR means out of sync if (hasPR && prState?.commit_sha) { - // Find last block with patches for this repo + // Find last block with merged patches for this repo for (let i = blocks.length - 1; i >= 0; i--) { const block = blocks[i]; - if (block?.file_patches?.some(p => p.repo_name === repoName)) { + if (block?.merged_file_patches?.some(p => p.repo_name === repoName)) { const blockSha = block.pr_commit_shas?.[repoName]; isOutOfSync = blockSha !== prState.commit_sha; break; diff --git a/static/app/views/seerExplorer/topBar.tsx b/static/app/views/seerExplorer/topBar.tsx index e20dcf3e80e154..62b594594e54ba 100644 --- a/static/app/views/seerExplorer/topBar.tsx +++ b/static/app/views/seerExplorer/topBar.tsx @@ -59,7 +59,7 @@ function TopBar({ }: TopBarProps) { // Check if there are any file patches const hasCodeChanges = useMemo(() => { - return blocks.some(b => b.file_patches && b.file_patches.length > 0); + return blocks.some(b => b.merged_file_patches && b.merged_file_patches.length > 0); }, [blocks]); return ( diff --git a/static/app/views/seerExplorer/types.tsx b/static/app/views/seerExplorer/types.tsx index 329fb10b5667ee..bdf48b8f6d8afb 100644 --- a/static/app/views/seerExplorer/types.tsx +++ b/static/app/views/seerExplorer/types.tsx @@ -33,8 +33,9 @@ export interface Block { message: Message; timestamp: string; artifacts?: Artifact[]; - file_patches?: ExplorerFilePatch[]; + file_patches?: ExplorerFilePatch[]; // Incremental patches (for approval) loading?: boolean; + merged_file_patches?: ExplorerFilePatch[]; // Merged patches (original → current) for files touched in this block pr_commit_shas?: Record; todos?: TodoItem[]; tool_links?: Array;