88import { createTwoFilesPatch } from "diff" ;
99import { resolve as resolvePath } from "node:path" ;
1010import { findAgentFileContext , loadAgentContext } from "./agent" ;
11+ import { createSkippedBinaryMetadata , isProbablyBinaryFile , patchLooksBinary } from "./binary" ;
1112import {
1213 buildGitDiffArgs ,
1314 buildGitShowArgs ,
@@ -132,15 +133,20 @@ function findPatchChunk(metadata: FileDiffMetadata, chunks: string[], index: num
132133 ) ;
133134}
134135
136+ interface BuildDiffFileOptions {
137+ isUntracked ?: boolean ;
138+ previousPath ?: string ;
139+ isBinary ?: boolean ;
140+ }
141+
135142/** Build the normalized per-file model used by the UI regardless of input mode. */
136143function buildDiffFile (
137144 metadata : FileDiffMetadata ,
138145 patch : string ,
139146 index : number ,
140147 sourcePrefix : string ,
141148 agentContext : AgentContext | null ,
142- isUntracked ?: boolean ,
143- previousPath ?: string ,
149+ { isUntracked, previousPath, isBinary } : BuildDiffFileOptions = { } ,
144150) : DiffFile {
145151 return {
146152 id : `${ sourcePrefix } :${ index } :${ metadata . name } ` ,
@@ -152,6 +158,7 @@ function buildDiffFile(
152158 metadata,
153159 agent : findAgentFileContext ( agentContext , metadata . name , metadata . prevName ) ,
154160 isUntracked,
161+ isBinary : isBinary ?? patchLooksBinary ( patch ) ,
155162 } ;
156163}
157164
@@ -237,7 +244,9 @@ function buildUntrackedDiffFile(
237244 index ,
238245 sourcePrefix ,
239246 agentContext ,
240- true , // isUntracked
247+ {
248+ isUntracked : true ,
249+ } ,
241250 ) ;
242251}
243252
@@ -326,6 +335,52 @@ function normalizePatchChangeset(
326335 } ;
327336}
328337
338+ /** Return the change type to show when direct file comparison skips binary contents. */
339+ function resolveBinaryComparisonType (
340+ leftPath : string ,
341+ rightPath : string ,
342+ ) : FileDiffMetadata [ "type" ] {
343+ if ( leftPath === "/dev/null" ) {
344+ return "new" ;
345+ }
346+
347+ if ( rightPath === "/dev/null" ) {
348+ return "deleted" ;
349+ }
350+
351+ return "change" ;
352+ }
353+
354+ /** Build a placeholder changeset for direct file comparisons that include binary content. */
355+ function buildBinaryFileDiffChangeset (
356+ input : FileCommandInput | DiffToolCommandInput ,
357+ displayPath : string ,
358+ title : string ,
359+ leftPath : string ,
360+ rightPath : string ,
361+ agentContext : AgentContext | null ,
362+ ) {
363+ return {
364+ id : `pair:${ displayPath } ` ,
365+ sourceLabel : input . kind === "difftool" ? "git difftool" : "file compare" ,
366+ title,
367+ agentSummary : agentContext ?. summary ,
368+ files : [
369+ buildDiffFile (
370+ createSkippedBinaryMetadata ( displayPath , resolveBinaryComparisonType ( leftPath , rightPath ) ) ,
371+ `Binary file skipped: ${ basename ( input . left ) } ↔ ${ basename ( input . right ) } \n` ,
372+ 0 ,
373+ displayPath ,
374+ agentContext ,
375+ {
376+ previousPath : basename ( input . left ) ,
377+ isBinary : true ,
378+ } ,
379+ ) ,
380+ ] ,
381+ } satisfies Changeset ;
382+ }
383+
329384/** Build a changeset by diffing two concrete files on disk. */
330385async function loadFileDiffChangeset (
331386 input : FileCommandInput | DiffToolCommandInput ,
@@ -334,8 +389,6 @@ async function loadFileDiffChangeset(
334389) {
335390 const leftPath = resolvePath ( cwd , input . left ) ;
336391 const rightPath = resolvePath ( cwd , input . right ) ;
337- const leftText = await Bun . file ( leftPath ) . text ( ) ;
338- const rightText = await Bun . file ( rightPath ) . text ( ) ;
339392 const displayPath =
340393 input . kind === "difftool" ? ( input . path ?? basename ( input . right ) ) : basename ( input . right ) ;
341394 const title =
@@ -345,6 +398,19 @@ async function loadFileDiffChangeset(
345398 ? displayPath
346399 : `${ basename ( input . left ) } ↔ ${ basename ( input . right ) } ` ;
347400
401+ if ( isProbablyBinaryFile ( leftPath ) || isProbablyBinaryFile ( rightPath ) ) {
402+ return buildBinaryFileDiffChangeset (
403+ input ,
404+ displayPath ,
405+ title ,
406+ leftPath ,
407+ rightPath ,
408+ agentContext ,
409+ ) ;
410+ }
411+
412+ const leftText = await Bun . file ( leftPath ) . text ( ) ;
413+ const rightText = await Bun . file ( rightPath ) . text ( ) ;
348414 const oldFile : FileContents = {
349415 name : displayPath ,
350416 contents : leftText ,
@@ -367,7 +433,9 @@ async function loadFileDiffChangeset(
367433 title,
368434 agentSummary : agentContext ?. summary ,
369435 files : [
370- buildDiffFile ( metadata , patch , 0 , displayPath , agentContext , undefined , basename ( input . left ) ) ,
436+ buildDiffFile ( metadata , patch , 0 , displayPath , agentContext , {
437+ previousPath : basename ( input . left ) ,
438+ } ) ,
371439 ] ,
372440 } satisfies Changeset ;
373441}
0 commit comments