diff --git a/backend/src/models/task_attempt.rs b/backend/src/models/task_attempt.rs index af267041..62690558 100644 --- a/backend/src/models/task_attempt.rs +++ b/backend/src/models/task_attempt.rs @@ -941,10 +941,15 @@ impl TaskAttempt { // second parent is the branch that was merged let parents: Vec<_> = merge_commit.parents().collect(); + // Create diff options with more context + let mut diff_opts = git2::DiffOptions::new(); + diff_opts.context_lines(10); // Include 10 lines of context around changes + diff_opts.interhunk_lines(0); // Don't merge hunks + let diff = if parents.len() >= 2 { let base_tree = parents[0].tree()?; // Main branch before merge let merged_tree = parents[1].tree()?; // The branch that was merged - main_repo.diff_tree_to_tree(Some(&base_tree), Some(&merged_tree), None)? + main_repo.diff_tree_to_tree(Some(&base_tree), Some(&merged_tree), Some(&mut diff_opts))? } else { // Fast-forward merge or single parent - compare merge commit with its parent let base_tree = if !parents.is_empty() { @@ -954,7 +959,7 @@ impl TaskAttempt { main_repo.find_tree(git2::Oid::zero())? }; let merged_tree = merge_commit.tree()?; - main_repo.diff_tree_to_tree(Some(&base_tree), Some(&merged_tree), None)? + main_repo.diff_tree_to_tree(Some(&base_tree), Some(&merged_tree), Some(&mut diff_opts))? }; // Process each diff delta (file change) @@ -1028,9 +1033,13 @@ impl TaskAttempt { let current_commit = worktree_repo.find_commit(worktree_head_oid)?; let current_tree = current_commit.tree()?; - // Create a diff between the base tree and current tree + // Create a diff between the base tree and current tree with more context + let mut diff_opts = git2::DiffOptions::new(); + diff_opts.context_lines(10); // Include 10 lines of context around changes + diff_opts.interhunk_lines(0); // Don't merge hunks + let diff = - worktree_repo.diff_tree_to_tree(Some(&base_tree), Some(¤t_tree), None)?; + worktree_repo.diff_tree_to_tree(Some(&base_tree), Some(¤t_tree), Some(&mut diff_opts))?; // Process each diff delta (file change) diff.foreach( @@ -1114,14 +1123,18 @@ impl TaskAttempt { None }; - // Generate patch using Git's diff algorithm + // Generate patch using Git's diff algorithm with context + let mut diff_opts = git2::DiffOptions::new(); + diff_opts.context_lines(10); // Include 10 lines of context around changes + diff_opts.interhunk_lines(0); // Don't merge hunks + let patch = match (old_blob.as_ref(), new_blob.as_ref()) { (Some(old_b), Some(new_b)) => git2::Patch::from_blobs( old_b, Some(Path::new(file_path)), new_b, Some(Path::new(file_path)), - None, + Some(&mut diff_opts), )?, (None, Some(new_b)) => { // File was added - diff from empty buffer to new blob content @@ -1130,7 +1143,7 @@ impl TaskAttempt { Some(Path::new(file_path)), new_b.content(), // new blob content as buffer Some(Path::new(file_path)), - None, + Some(&mut diff_opts), )? } (Some(old_b), None) => { @@ -1140,7 +1153,7 @@ impl TaskAttempt { Some(Path::new(file_path)), &[], Some(Path::new(file_path)), - None, + Some(&mut diff_opts), )? } (None, None) => { diff --git a/frontend/src/pages/task-attempt-compare.tsx b/frontend/src/pages/task-attempt-compare.tsx index 39fe2334..fd5cf4e9 100644 --- a/frontend/src/pages/task-attempt-compare.tsx +++ b/frontend/src/pages/task-attempt-compare.tsx @@ -18,6 +18,8 @@ import { RefreshCw, GitBranch, Trash2, + Eye, + EyeOff, } from "lucide-react"; import { makeRequest } from "@/lib/api"; import type { @@ -53,6 +55,7 @@ export function TaskAttemptComparePage() { const [expandedSections, setExpandedSections] = useState>( new Set() ); + const [showAllUnchanged, setShowAllUnchanged] = useState(false); const [deletingFiles, setDeletingFiles] = useState>(new Set()); const [fileToDelete, setFileToDelete] = useState(null); @@ -184,13 +187,13 @@ export function TaskAttemptComparePage() { }; const getChunkClassName = (chunkType: DiffChunkType) => { - const baseClass = "font-mono text-sm whitespace-pre px-3 py-1"; + const baseClass = "font-mono text-sm whitespace-pre py-1 flex"; switch (chunkType) { case "Insert": - return `${baseClass} bg-green-50 text-green-800 border-l-2 border-green-400`; + return `${baseClass} bg-green-50 dark:bg-green-900/20 text-green-800 dark:text-green-200 border-l-2 border-green-400 dark:border-green-500`; case "Delete": - return `${baseClass} bg-red-50 text-red-800 border-l-2 border-red-400`; + return `${baseClass} bg-red-50 dark:bg-red-900/20 text-red-800 dark:text-red-200 border-l-2 border-red-400 dark:border-red-500`; case "Equal": default: return `${baseClass} text-muted-foreground`; @@ -212,7 +215,8 @@ export function TaskAttemptComparePage() { interface ProcessedLine { content: string; chunkType: DiffChunkType; - lineNumber: number; + oldLineNumber?: number; + newLineNumber?: number; } interface ProcessedSection { @@ -226,7 +230,8 @@ export function TaskAttemptComparePage() { const processFileChunks = (chunks: DiffChunk[], fileIndex: number) => { const CONTEXT_LINES = 3; const lines: ProcessedLine[] = []; - let currentLineNumber = 1; + let oldLineNumber = 1; + let newLineNumber = 1; // Convert chunks to lines with line numbers chunks.forEach((chunk) => { @@ -234,11 +239,28 @@ export function TaskAttemptComparePage() { chunkLines.forEach((line, index) => { if (index < chunkLines.length - 1 || line !== "") { // Skip empty last line from split - lines.push({ + const processedLine: ProcessedLine = { content: line, chunkType: chunk.chunk_type, - lineNumber: currentLineNumber++, - }); + }; + + // Set line numbers based on chunk type + switch (chunk.chunk_type) { + case "Equal": + processedLine.oldLineNumber = oldLineNumber++; + processedLine.newLineNumber = newLineNumber++; + break; + case "Delete": + processedLine.oldLineNumber = oldLineNumber++; + // No new line number for deletions + break; + case "Insert": + processedLine.newLineNumber = newLineNumber++; + // No old line number for insertions + break; + } + + lines.push(processedLine); } }); }); @@ -267,9 +289,10 @@ export function TaskAttemptComparePage() { if ( contextLength <= CONTEXT_LINES * 2 || - (!hasPrevChange && !hasNextChange) + (!hasPrevChange && !hasNextChange) || + showAllUnchanged ) { - // Show all context if it's short or if there are no changes around it + // Show all context if it's short, no changes around it, or global toggle is on sections.push({ type: "context", lines: lines.slice(i, nextChangeIndex), @@ -292,7 +315,7 @@ export function TaskAttemptComparePage() { if (expandEnd > expandStart) { const expandKey = `${fileIndex}-${expandStart}-${expandEnd}`; - const isExpanded = expandedSections.has(expandKey); + const isExpanded = expandedSections.has(expandKey) || showAllUnchanged; if (isExpanded) { sections.push({ @@ -514,13 +537,35 @@ export function TaskAttemptComparePage() { - - Diff: Base Commit vs. Current Worktree - -

- Shows changes made in the task attempt worktree compared to the base - commit -

+
+
+ + Diff: Base Commit vs. Current Worktree + +

+ Shows changes made in the task attempt worktree compared to the base + commit +

+
+ +
{!diff || diff.files.length === 0 ? ( @@ -562,43 +607,45 @@ export function TaskAttemptComparePage() { {processFileChunks(file.chunks, fileIndex).map( (section, sectionIndex) => { if ( - section.type === "context" && - section.lines.length === 0 && - section.expandKey + section.type === "context" && + section.lines.length === 0 && + section.expandKey && + !showAllUnchanged ) { - // Render expand button - const lineCount = - parseInt(section.expandKey.split("-")[2]) - - parseInt(section.expandKey.split("-")[1]); - return ( -
- -
- ); + // Render expand button (only when global toggle is off) + const lineCount = + parseInt(section.expandKey.split("-")[2]) - + parseInt(section.expandKey.split("-")[1]); + return ( +
+ +
+ ); + } // Render lines (context, change, or expanded) return (
{section.type === "expanded" && - section.expandKey && ( + section.expandKey && + !showAllUnchanged && (
))}