@@ -3,6 +3,7 @@ import { useSize } from "react-use"
33import { useTranslation , Trans } from "react-i18next"
44import deepEqual from "fast-deep-equal"
55import { VSCodeBadge } from "@vscode/webview-ui-toolkit/react"
6+ import { structuredPatch } from "diff"
67
78import type { ClineMessage , FollowUpData , SuggestionItem } from "@roo-code/types"
89import { Mode } from "@roo/modes"
@@ -15,7 +16,6 @@ import { useExtensionState } from "@src/context/ExtensionStateContext"
1516import { findMatchingResourceOrTemplate } from "@src/utils/mcp"
1617import { vscode } from "@src/utils/vscode"
1718import { removeLeadingNonAlphanumeric } from "@src/utils/removeLeadingNonAlphanumeric"
18- import { getLanguageFromPath } from "@src/utils/getLanguageFromPath"
1919
2020import { ToolUseBlock , ToolUseBlockHeader } from "../common/ToolUseBlock"
2121import UpdateTodoListToolBlock from "./UpdateTodoListToolBlock"
@@ -115,6 +115,116 @@ const ChatRow = memo(
115115
116116export default ChatRow
117117
118+ function computeDiffStats ( diff ?: string ) : { added : number ; removed : number } | null {
119+ if ( ! diff ) return null
120+
121+ // Strategy 1: Unified diff (+/- lines)
122+ let added = 0
123+ let removed = 0
124+ let sawPlusMinus = false
125+ for ( const line of diff . split ( "\n" ) ) {
126+ if ( line . startsWith ( "+++ " ) || line . startsWith ( "--- " ) || line . startsWith ( "@@" ) ) continue
127+ if ( line . startsWith ( "+" ) ) {
128+ added ++
129+ sawPlusMinus = true
130+ } else if ( line . startsWith ( "-" ) ) {
131+ removed ++
132+ sawPlusMinus = true
133+ }
134+ }
135+ if ( sawPlusMinus ) {
136+ if ( added === 0 && removed === 0 ) return null
137+ return { added, removed }
138+ }
139+
140+ // Strategy 2: Roo multi-search-replace blocks
141+ // Count lines in SEARCH vs REPLACE sections across all blocks
142+ // Matches optional metadata lines and optional '-------' line
143+ const blockRegex =
144+ / < < < < < < ? \s * S E A R C H [ \s \S ] * ?(?: ^ : s t a r t _ l i n e : .* \n ) ? (?: ^ : e n d _ l i n e : .* \n ) ? (?: ^ - - - - - - - \s * \n ) ? ( [ \s \S ] * ?) ^ (?: = = = = = = = \s * \n ) ( [ \s \S ] * ?) ^ (?: > > > > > > > R E P L A C E ) / gim
145+
146+ let hasBlocks = false
147+ added = 0
148+ removed = 0
149+
150+ const asLines = ( s : string ) => {
151+ // Normalize Windows newlines and trim trailing newline so counts reflect real lines
152+ const norm = s . replace ( / \r \n / g, "\n" )
153+ if ( norm === "" ) return 0
154+ // Split, drop potential trailing empty caused by final newline
155+ const parts = norm . split ( "\n" )
156+ return parts [ parts . length - 1 ] === "" ? parts . length - 1 : parts . length
157+ }
158+
159+ let match : RegExpExecArray | null
160+ while ( ( match = blockRegex . exec ( diff ) ) !== null ) {
161+ hasBlocks = true
162+ const searchContent = match [ 1 ] ?? ""
163+ const replaceContent = match [ 2 ] ?? ""
164+ const searchCount = asLines ( searchContent )
165+ const replaceCount = asLines ( replaceContent )
166+ if ( replaceCount > searchCount ) added += replaceCount - searchCount
167+ else if ( searchCount > replaceCount ) removed += searchCount - replaceCount
168+ }
169+
170+ if ( hasBlocks ) {
171+ if ( added === 0 && removed === 0 ) return null
172+ return { added, removed }
173+ }
174+
175+ return null
176+ }
177+
178+ /**
179+ * Converts new file content to unified diff format (all lines as additions)
180+ */
181+ function convertNewFileToUnifiedDiff ( content : string , filePath ?: string ) : string {
182+ const fileName = filePath || "file"
183+ const lines = content . split ( "\n" )
184+
185+ let diff = `--- /dev/null\n`
186+ diff += `+++ ${ fileName } \n`
187+ diff += `@@ -0,0 +1,${ lines . length } @@\n`
188+
189+ for ( const line of lines ) {
190+ diff += `+${ line } \n`
191+ }
192+
193+ return diff
194+ }
195+
196+ /**
197+ * Converts Roo's SEARCH/REPLACE format to unified diff format for better readability
198+ */
199+ function convertSearchReplaceToUnifiedDiff ( content : string , filePath ?: string ) : string {
200+ const blockRegex =
201+ / < < < < < < ? \s * S E A R C H [ \s \S ] * ?(?: ^ : s t a r t _ l i n e : .* \n ) ? (?: ^ : e n d _ l i n e : .* \n ) ? (?: ^ - - - - - - - \s * \n ) ? ( [ \s \S ] * ?) ^ (?: = = = = = = = \s * \n ) ( [ \s \S ] * ?) ^ (?: > > > > > > > R E P L A C E ) / gim
202+
203+ let hasBlocks = false
204+ let combinedDiff = ""
205+ const fileName = filePath || "file"
206+
207+ let match : RegExpExecArray | null
208+ while ( ( match = blockRegex . exec ( content ) ) !== null ) {
209+ hasBlocks = true
210+ const searchContent = ( match [ 1 ] ?? "" ) . replace ( / \n $ / , "" ) // Remove trailing newline
211+ const replaceContent = ( match [ 2 ] ?? "" ) . replace ( / \n $ / , "" )
212+
213+ // Use the diff library to create a proper unified diff
214+ const patch = structuredPatch ( fileName , fileName , searchContent , replaceContent , "" , "" , { context : 3 } )
215+
216+ // Convert to unified diff format
217+ if ( patch . hunks . length > 0 ) {
218+ for ( const hunk of patch . hunks ) {
219+ combinedDiff += `@@ -${ hunk . oldStart } ,${ hunk . oldLines } +${ hunk . newStart } ,${ hunk . newLines } @@\n`
220+ combinedDiff += hunk . lines . join ( "\n" ) + "\n"
221+ }
222+ }
223+ }
224+
225+ return hasBlocks ? combinedDiff : content
226+ }
227+
118228export const ChatRowContent = ( {
119229 message,
120230 lastModifiedMessage,
@@ -334,6 +444,59 @@ export const ChatRowContent = ({
334444 [ message . ask , message . text ] ,
335445 )
336446
447+ // Inline diff stats for edit/apply_diff/insert/search-replace/newFile asks
448+ const diffTextForStats = useMemo ( ( ) => {
449+ if ( ! tool ) return ""
450+ let content = ""
451+ switch ( tool . tool ) {
452+ case "editedExistingFile" :
453+ case "appliedDiff" :
454+ content = ( tool . content ?? tool . diff ) || ""
455+ break
456+ case "insertContent" :
457+ case "searchAndReplace" :
458+ content = tool . diff || ""
459+ break
460+ case "newFileCreated" :
461+ // For new files, convert to unified diff format
462+ const newFileContent = tool . content || ""
463+ content = convertNewFileToUnifiedDiff ( newFileContent , tool . path )
464+ break
465+ default :
466+ return ""
467+ }
468+ // Strip CDATA markers for proper parsing
469+ return content . replace ( / < ! \[ C D A T A \[ / g, "" ) . replace ( / \] \] > / g, "" )
470+ } , [ tool ] )
471+
472+ const diffStatsForInline = useMemo ( ( ) => {
473+ if ( tool ?. tool === "newFileCreated" ) {
474+ // For new files, count all lines as additions
475+ const content = diffTextForStats
476+ if ( ! content ) return null
477+ const lines = content . split ( "\n" ) . length
478+ return { added : lines , removed : 0 }
479+ }
480+ return computeDiffStats ( diffTextForStats )
481+ } , [ diffTextForStats , tool ] )
482+
483+ // Clean diff content for display (remove CDATA markers and convert to unified diff)
484+ const cleanDiffContent = useMemo ( ( ) => {
485+ if ( ! tool ) return undefined
486+ const raw = ( tool as any ) . content ?? ( tool as any ) . diff
487+ if ( ! raw ) return undefined
488+
489+ // Remove CDATA markers
490+ const withoutCData = raw . replace ( / < ! \[ C D A T A \[ / g, "" ) . replace ( / \] \] > / g, "" )
491+
492+ // Check if it's SEARCH/REPLACE format and convert to unified diff
493+ if ( / < < < < < < < ? \s * S E A R C H / i. test ( withoutCData ) ) {
494+ return convertSearchReplaceToUnifiedDiff ( withoutCData , tool . path )
495+ }
496+
497+ return withoutCData
498+ } , [ tool ] )
499+
337500 const followUpData = useMemo ( ( ) => {
338501 if ( message . type === "ask" && message . ask === "followup" && ! message . partial ) {
339502 return safeJsonParse < FollowUpData > ( message . text )
@@ -389,12 +552,13 @@ export const ChatRowContent = ({
389552 < div className = "pl-6" >
390553 < CodeAccordian
391554 path = { tool . path }
392- code = { tool . content ?? tool . diff }
555+ code = { cleanDiffContent ?? tool . content ?? tool . diff }
393556 language = "diff"
394557 progressStatus = { message . progressStatus }
395558 isLoading = { message . partial }
396559 isExpanded = { isExpanded }
397560 onToggleExpand = { handleToggleExpand }
561+ diffStats = { diffStatsForInline ?? undefined }
398562 />
399563 </ div >
400564 </ >
@@ -426,12 +590,13 @@ export const ChatRowContent = ({
426590 < div className = "pl-6" >
427591 < CodeAccordian
428592 path = { tool . path }
429- code = { tool . diff }
593+ code = { cleanDiffContent ?? tool . diff }
430594 language = "diff"
431595 progressStatus = { message . progressStatus }
432596 isLoading = { message . partial }
433597 isExpanded = { isExpanded }
434598 onToggleExpand = { handleToggleExpand }
599+ diffStats = { diffStatsForInline ?? undefined }
435600 />
436601 </ div >
437602 </ >
@@ -459,12 +624,13 @@ export const ChatRowContent = ({
459624 < div className = "pl-6" >
460625 < CodeAccordian
461626 path = { tool . path }
462- code = { tool . diff }
627+ code = { cleanDiffContent ?? tool . diff }
463628 language = "diff"
464629 progressStatus = { message . progressStatus }
465630 isLoading = { message . partial }
466631 isExpanded = { isExpanded }
467632 onToggleExpand = { handleToggleExpand }
633+ diffStats = { diffStatsForInline ?? undefined }
468634 />
469635 </ div >
470636 </ >
@@ -527,12 +693,13 @@ export const ChatRowContent = ({
527693 < div className = "pl-6" >
528694 < CodeAccordian
529695 path = { tool . path }
530- code = { tool . content }
531- language = { getLanguageFromPath ( tool . path || "" ) || "log" }
696+ code = { convertNewFileToUnifiedDiff ( tool . content || "" , tool . path ) }
697+ language = "diff"
532698 isLoading = { message . partial }
533699 isExpanded = { isExpanded }
534700 onToggleExpand = { handleToggleExpand }
535701 onJumpToFile = { ( ) => vscode . postMessage ( { type : "openFile" , text : "./" + tool . path } ) }
702+ diffStats = { diffStatsForInline ?? undefined }
536703 />
537704 </ div >
538705 </ >
0 commit comments