11import { ExternalLink , RefreshCw } from 'lucide-react' ;
2- import { useState } from 'react' ;
2+ import { memo , useCallback , useState } from 'react' ;
33import { useTranslation } from 'react-i18next' ;
44
55import { api } from '../../../utils/api' ;
@@ -42,13 +42,13 @@ export default function WorkspaceChangesTab({
4242 const { files : lastTurnFiles , refresh : refreshLastTurn } = useLastTurnChanges ( sessionId , active && scope === 'lastTurn' ) ;
4343 const [ openPath , setOpenPath ] = useState < string | null > ( null ) ;
4444
45- const openInEditor = ( path : string ) => {
45+ const openInEditor = useCallback ( ( path : string ) => {
4646 if ( ! projectPath ) {
4747 return ;
4848 }
4949 const absolutePath = path . startsWith ( '/' ) ? path : `${ projectPath . replace ( / \/ $ / , '' ) } /${ path } ` ;
5050 void api . system . openFile ( absolutePath ) ;
51- } ;
51+ } , [ projectPath ] ) ;
5252
5353 const refresh = ( ) => {
5454 if ( scope === 'workingTree' ) {
@@ -109,9 +109,10 @@ export default function WorkspaceChangesTab({
109109 < LastTurnChangeRow
110110 key = { `${ index } :${ file . path } ` }
111111 file = { file }
112- expanded = { openPath === `${ index } :${ file . path } ` }
113- onToggle = { ( ) => setOpenPath ( ( current ) => current === `${ index } :${ file . path } ` ? null : `${ index } :${ file . path } ` ) }
114- onOpenInEditor = { ( ) => openInEditor ( file . path ) }
112+ rowKey = { `${ index } :${ file . path } ` }
113+ openPath = { openPath }
114+ onSetOpenPath = { setOpenPath }
115+ onOpenInEditor = { openInEditor }
115116 onComposerInsert = { onComposerInsert }
116117 t = { t }
117118 />
@@ -120,8 +121,11 @@ export default function WorkspaceChangesTab({
120121 ) : state . kind === 'ready' ? (
121122 < >
122123 < div className = "truncate border-b border-border/60 px-2.5 py-1.5 text-muted-foreground" >
123- { state . changes . branch ?? projectName ?? '—' } · { t ( 'workspace.changes.files' , { count : state . changes . files . length } ) }
124+ { state . changes . branch ?? projectName ?? '—' } · { t ( 'workspace.changes.files' , { count : state . changes . truncated ? state . changes . totalFiles : state . changes . files . length } ) }
124125 </ div >
126+ { state . changes . truncated && (
127+ < p className = "border-b border-border/60 px-2.5 py-1.5 text-muted-foreground" > { t ( 'workspace.changes.truncated' , { shown : state . changes . files . length , total : state . changes . totalFiles } ) } </ p >
128+ ) }
125129 { ! state . changes . hasCommits && (
126130 < p className = "border-b border-border/60 px-2.5 py-1.5 text-muted-foreground" > { t ( 'workspace.changes.noCommits' ) } </ p >
127131 ) }
@@ -135,9 +139,9 @@ export default function WorkspaceChangesTab({
135139 < ChangeRow
136140 key = { file . path }
137141 file = { file }
138- expanded = { openPath === file . path }
139- onToggle = { ( ) => setOpenPath ( ( current ) => current === file . path ? null : file . path ) }
140- onOpenInEditor = { ( ) => openInEditor ( file . path ) }
142+ openPath = { openPath }
143+ onSetOpenPath = { setOpenPath }
144+ onOpenInEditor = { openInEditor }
141145 onComposerInsert = { onComposerInsert }
142146 t = { t }
143147 />
@@ -157,22 +161,24 @@ export default function WorkspaceChangesTab({
157161 ) ;
158162}
159163
160- export function ChangeRow ( {
164+ export const ChangeRow = memo ( function ChangeRow ( {
161165 file,
162- expanded ,
163- onToggle ,
166+ openPath ,
167+ onSetOpenPath ,
164168 onOpenInEditor,
165169 onComposerInsert,
166170 t,
167171} : {
168172 file : ProjectChange ;
169- expanded : boolean ;
170- onToggle : ( ) => void ;
171- onOpenInEditor : ( ) => void ;
173+ openPath : string | null ;
174+ onSetOpenPath : ( path : string | null ) => void ;
175+ onOpenInEditor : ( path : string ) => void ;
172176 onComposerInsert ?: ( text : string ) => void ;
173177 t : ( key : string , options ?: Record < string , unknown > ) => string ;
174178} ) {
175179 const [ commentRow , setCommentRow ] = useState < DiffCommentRow | null > ( null ) ;
180+ const expanded = openPath === file . path ;
181+ const onToggle = ( ) => onSetOpenPath ( expanded ? null : file . path ) ;
176182 const appearance = statusAppearance [ file . status ] ;
177183 return (
178184 < div className = "border-b border-border/60 last:border-b-0" >
@@ -194,7 +200,7 @@ export function ChangeRow({
194200 </ button >
195201 < button
196202 type = "button"
197- onClick = { onOpenInEditor }
203+ onClick = { ( ) => onOpenInEditor ( file . path ) }
198204 title = { t ( 'workspace.changes.openInEditor' ) }
199205 aria-label = { t ( 'workspace.changes.openInEditor' ) }
200206 className = "rounded p-1 text-muted-foreground transition-colors hover:bg-muted/50 hover:text-foreground"
@@ -220,7 +226,7 @@ export function ChangeRow({
220226 ) }
221227 </ div >
222228 ) ;
223- }
229+ } ) ;
224230
225231const lastTurnAppearance : Record < LastTurnFile [ 'kind' ] , { label : string ; className : string } > = {
226232 edit : { label : 'E' , className : 'bg-muted text-muted-foreground' } ,
@@ -229,22 +235,26 @@ const lastTurnAppearance: Record<LastTurnFile['kind'], { label: string; classNam
229235 move : { label : 'M' , className : 'bg-diff-added text-diff-added-foreground' } ,
230236} ;
231237
232- function LastTurnChangeRow ( {
238+ const LastTurnChangeRow = memo ( function LastTurnChangeRow ( {
233239 file,
234- expanded,
235- onToggle,
240+ rowKey,
241+ openPath,
242+ onSetOpenPath,
236243 onOpenInEditor,
237244 onComposerInsert,
238245 t,
239246} : {
240247 file : LastTurnFile ;
241- expanded : boolean ;
242- onToggle : ( ) => void ;
243- onOpenInEditor : ( ) => void ;
248+ rowKey : string ;
249+ openPath : string | null ;
250+ onSetOpenPath : ( path : string | null ) => void ;
251+ onOpenInEditor : ( path : string ) => void ;
244252 onComposerInsert ?: ( text : string ) => void ;
245253 t : ( key : string , options ?: Record < string , unknown > ) => string ;
246254} ) {
247255 const [ commentRow , setCommentRow ] = useState < DiffCommentRow | null > ( null ) ;
256+ const expanded = openPath === rowKey ;
257+ const onToggle = ( ) => onSetOpenPath ( expanded ? null : rowKey ) ;
248258 const appearance = lastTurnAppearance [ file . kind ] ;
249259 return (
250260 < div className = "border-b border-border/60 last:border-b-0" >
@@ -255,7 +265,7 @@ function LastTurnChangeRow({
255265 { file . oldPath ? < > { file . oldPath } < span className = "text-muted-foreground" > { t ( 'workspace.changes.renameArrow' ) } </ span > { file . path } </ > : file . path }
256266 </ span >
257267 </ button >
258- < button type = "button" onClick = { onOpenInEditor } title = { t ( 'workspace.changes.openInEditor' ) } aria-label = { t ( 'workspace.changes.openInEditor' ) } className = "rounded p-1 text-muted-foreground transition-colors hover:bg-muted/50 hover:text-foreground" >
268+ < button type = "button" onClick = { ( ) => onOpenInEditor ( file . path ) } title = { t ( 'workspace.changes.openInEditor' ) } aria-label = { t ( 'workspace.changes.openInEditor' ) } className = "rounded p-1 text-muted-foreground transition-colors hover:bg-muted/50 hover:text-foreground" >
259269 < ExternalLink className = "h-3 w-3" />
260270 </ button >
261271 </ div >
@@ -275,7 +285,7 @@ function LastTurnChangeRow({
275285 ) }
276286 </ div >
277287 ) ;
278- }
288+ } ) ;
279289
280290
281291const COMMENT_MARKER = { added : '+' , removed : '-' , context : ' ' } as const ;
0 commit comments