Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 12 additions & 8 deletions static/app/components/events/autofix/useExplorerAutofix.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -236,27 +236,31 @@ export function getOrderedArtifactKeys(
}

This comment was marked as outdated.


/**
* 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<string, ExplorerFilePatch>();

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());
}

/**
Comment on lines +247 to 257

This comment was marked as outdated.

* 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 {
Expand Down
48 changes: 1 addition & 47 deletions static/app/components/events/autofix/v2/artifactCards.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -708,59 +708,13 @@ interface CodeChangesCardProps {
prStates?: Record<string, RepoPRState>;
}

/**
* 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<string, ExplorerFilePatch[]>();

// 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<string, ExplorerFilePatch[]>();
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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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;
}
Expand Down
10 changes: 5 additions & 5 deletions static/app/components/events/autofix/v2/explorerSeerDrawer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import AutofixFeedback from 'sentry/components/events/autofix/autofixFeedback';
import {
hasCodeChanges as checkHasCodeChanges,
getArtifactsFromBlocks,
getFilePatchesFromBlocks,
getMergedFilePatchesFromBlocks,
getOrderedArtifactKeys,
useExplorerAutofix,
type AutofixExplorerStep,
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -352,10 +352,10 @@ export function ExplorerSeerDrawer({
return null;
}
})}
{filePatches.length > 0 && (
{/* Code changes from merged file patches */}
{mergedPatches.length > 0 && (
<CodeChangesCard
key="code_changes"
patches={filePatches}
patches={mergedPatches}
prStates={prStates}
onCreatePR={handleCreatePR}
/>
Expand Down
78 changes: 43 additions & 35 deletions static/app/views/seerExplorer/prWidget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,48 +45,56 @@ export function usePRWidgetData({
onCreatePR: (repoName?: string) => void;
repoPRStates: Record<string, RepoPRState>;
}) {
// 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;
}
Comment on lines +57 to +59

This comment was marked as outdated.

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<string, RepoStats> = {};
const fileStats: Record<string, FileStats[]> = {};
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 {
Expand All @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion static/app/views/seerExplorer/topBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down
3 changes: 2 additions & 1 deletion static/app/views/seerExplorer/types.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, string>;
todos?: TodoItem[];
tool_links?: Array<ToolLink | null>;
Expand Down
Loading