1- import { useEffect , useState , useCallback , useMemo } from 'react'
1+ import { useEffect , useState , useCallback , useMemo , useRef } from 'react'
22import ReactFlow , {
33 Controls ,
44 Background ,
@@ -44,6 +44,11 @@ const LAYOUT_CONFIG = {
4444const DEFAULT_VISIBLE_COUNT = 15
4545
4646function getLayoutedElements ( nodes : Node [ ] , edges : Edge [ ] ) {
47+ // Guard: if no nodes, return empty
48+ if ( nodes . length === 0 ) {
49+ return { nodes : [ ] , edges : [ ] }
50+ }
51+
4752 const dagreGraph = new dagre . graphlib . Graph ( )
4853 dagreGraph . setDefaultEdgeLabel ( ( ) => ( { } ) )
4954 dagreGraph . setGraph ( {
@@ -59,19 +64,26 @@ function getLayoutedElements(nodes: Node[], edges: Edge[]) {
5964 } )
6065 } )
6166
67+ // Only add edges where both source and target exist in nodes
68+ const nodeIds = new Set ( nodes . map ( n => n . id ) )
6269 edges . forEach ( ( edge ) => {
63- dagreGraph . setEdge ( edge . source , edge . target )
70+ if ( nodeIds . has ( edge . source ) && nodeIds . has ( edge . target ) ) {
71+ dagreGraph . setEdge ( edge . source , edge . target )
72+ }
6473 } )
6574
6675 dagre . layout ( dagreGraph )
6776
6877 const layoutedNodes = nodes . map ( ( node ) => {
6978 const nodeWithPosition = dagreGraph . node ( node . id )
79+ // Guard: if dagre failed to position node, use fallback
80+ const x = nodeWithPosition ?. x ?? 0
81+ const y = nodeWithPosition ?. y ?? 0
7082 return {
7183 ...node ,
7284 position : {
73- x : nodeWithPosition . x - LAYOUT_CONFIG . nodeWidth / 2 ,
74- y : nodeWithPosition . y - LAYOUT_CONFIG . nodeHeight / 2 ,
85+ x : x - LAYOUT_CONFIG . nodeWidth / 2 ,
86+ y : y - LAYOUT_CONFIG . nodeHeight / 2 ,
7587 } ,
7688 }
7789 } )
@@ -113,6 +125,7 @@ function DependencyGraphInner({ repoId, apiUrl, apiKey }: DependencyGraphProps)
113125 const [ clusterByDir , setClusterByDir ] = useState ( false )
114126 const [ expandedDirs , setExpandedDirs ] = useState < Set < string > > ( new Set ( ) )
115127 const [ rawGraphData , setRawGraphData ] = useState < any > ( null )
128+ const [ renderKey , setRenderKey ] = useState ( 0 ) // Force re-render key
116129
117130 const { fitView } = useReactFlow ( )
118131 const { resolvedTheme } = useTheme ( )
@@ -125,6 +138,23 @@ function DependencyGraphInner({ repoId, apiUrl, apiKey }: DependencyGraphProps)
125138 if ( data ) setRawGraphData ( data )
126139 } , [ data ] )
127140
141+ // Handle tab visibility changes - force re-render when tab becomes visible
142+ useEffect ( ( ) => {
143+ const handleVisibilityChange = ( ) => {
144+ if ( document . visibilityState === 'visible' ) {
145+ // Force re-render by updating key
146+ setRenderKey ( k => k + 1 )
147+ // Also trigger fitView after a short delay
148+ setTimeout ( ( ) => {
149+ fitView ( { padding : 0.2 , duration : 200 } )
150+ } , 100 )
151+ }
152+ }
153+
154+ document . addEventListener ( 'visibilitychange' , handleVisibilityChange )
155+ return ( ) => document . removeEventListener ( 'visibilitychange' , handleVisibilityChange )
156+ } , [ fitView ] )
157+
128158 const visibleNodeIds = useMemo ( ( ) => {
129159 if ( ! rawGraphData || ! impact . isReady ) return new Set < string > ( )
130160
@@ -229,9 +259,17 @@ function DependencyGraphInner({ repoId, apiUrl, apiKey }: DependencyGraphProps)
229259 let flowNodes : Node [ ] = [ ]
230260 let flowEdges : Edge [ ] = [ ]
231261
232- if ( clusterByDir && clusteredData ) {
262+ // Only use cluster mode if we have clustered data ready
263+ const useClusterMode = clusterByDir && clusteredData && clusteredData . dirNodes . length > 0
264+
265+ if ( useClusterMode ) {
266+ // Safety: only apply selection highlighting if the selected node is actually visible
267+ const effectiveSelectedIdCluster = selectedNodeId &&
268+ ( clusteredData ! . visibleFiles . has ( selectedNodeId ) || selectedNodeId . startsWith ( 'dir:' ) )
269+ ? selectedNodeId : null
270+
233271 // Add directory nodes
234- clusteredData . dirNodes . forEach ( dir => {
272+ clusteredData ! . dirNodes . forEach ( dir => {
235273 flowNodes . push ( {
236274 id : dir . id ,
237275 type : 'directory' ,
@@ -242,19 +280,17 @@ function DependencyGraphInner({ repoId, apiUrl, apiKey }: DependencyGraphProps)
242280
243281 // Add visible file nodes
244282 rawGraphData . nodes
245- . filter ( ( node : any ) => clusteredData . visibleFiles . has ( node . id ) )
283+ . filter ( ( node : any ) => clusteredData ! . visibleFiles . has ( node . id ) )
246284 . forEach ( ( node : any ) => {
247285 const fileName = node . label || node . id . split ( '/' ) . pop ( )
248286 const metrics = impact . getFileMetrics ( node . id )
249287
288+ // Simplified state - only highlight, don't dim
250289 let state : GraphNodeData [ 'state' ] = 'default'
251- if ( selectedNodeId === node . id ) state = 'selected'
290+ if ( effectiveSelectedIdCluster === node . id ) state = 'selected'
252291 else if ( selectedImpact ?. directDependents . includes ( node . id ) ) state = 'direct'
253292 else if ( selectedImpact ?. transitiveDependents . includes ( node . id ) ) state = 'transitive'
254- else if ( selectedNodeId && ! selectedNodeId . startsWith ( 'dir:' ) ) state = 'dimmed'
255-
256- // Hover highlighting in clustered mode
257- if ( hoveredFileId === node . id && state === 'dimmed' ) state = 'direct'
293+ // Don't dim - keep as default
258294
259295 flowNodes . push ( {
260296 id : node . id ,
@@ -274,7 +310,7 @@ function DependencyGraphInner({ repoId, apiUrl, apiKey }: DependencyGraphProps)
274310 } )
275311
276312 // Add edges
277- clusteredData . edges . forEach ( ( [ source , target ] ) => {
313+ clusteredData ! . edges . forEach ( ( [ source , target ] ) => {
278314 flowEdges . push ( {
279315 id : `${ source } -${ target } ` ,
280316 source,
@@ -284,22 +320,24 @@ function DependencyGraphInner({ repoId, apiUrl, apiKey }: DependencyGraphProps)
284320 } )
285321 } else {
286322 // Non-clustered mode (original logic)
323+ // Safety: only apply selection highlighting if the selected node is actually visible
324+ const effectiveSelectedId = selectedNodeId && visibleNodeIds . has ( selectedNodeId ) ? selectedNodeId : null
325+
287326 flowNodes = rawGraphData . nodes
288327 . filter ( ( node : any ) => visibleNodeIds . has ( node . id ) )
289328 . map ( ( node : any ) => {
290329 const fileName = node . label || node . id . split ( '/' ) . pop ( )
291330 const metrics = impact . getFileMetrics ( node . id )
292331
332+ // Simplified state - only highlight selected and dependents, don't dim others
293333 let state : GraphNodeData [ 'state' ] = 'default'
294- if ( selectedNodeId ) {
295- if ( node . id === selectedNodeId ) state = 'selected'
334+ if ( effectiveSelectedId ) {
335+ if ( node . id === effectiveSelectedId ) state = 'selected'
296336 else if ( selectedImpact ?. directDependents . includes ( node . id ) ) state = 'direct'
297337 else if ( selectedImpact ?. transitiveDependents . includes ( node . id ) ) state = 'transitive'
298- else state = 'dimmed'
338+ // Don't dim - keep as default for visibility
299339 }
300340
301- if ( hoveredFileId === node . id && state === 'dimmed' ) state = 'direct'
302-
303341 return {
304342 id : node . id ,
305343 type : 'custom' ,
@@ -320,11 +358,12 @@ function DependencyGraphInner({ repoId, apiUrl, apiKey }: DependencyGraphProps)
320358 flowEdges = rawGraphData . edges
321359 . filter ( ( edge : any ) => visibleNodeIds . has ( edge . source ) && visibleNodeIds . has ( edge . target ) )
322360 . map ( ( edge : any ) => {
361+ // Simplified - only highlight connected edges, don't dim others
323362 let edgeState : 'default' | 'highlighted' | 'dimmed' | 'incoming' | 'outgoing' = 'default'
324- if ( selectedNodeId ) {
325- if ( edge . source === selectedNodeId ) edgeState = 'outgoing'
326- else if ( edge . target === selectedNodeId ) edgeState = 'incoming'
327- else edgeState = 'dimmed'
363+ if ( effectiveSelectedId ) {
364+ if ( edge . source === effectiveSelectedId ) edgeState = 'outgoing'
365+ else if ( edge . target === effectiveSelectedId ) edgeState = 'incoming'
366+ // Don't dim - keep as default
328367 }
329368
330369 return {
@@ -342,12 +381,14 @@ function DependencyGraphInner({ repoId, apiUrl, apiKey }: DependencyGraphProps)
342381 setEdges ( layoutedEdges )
343382 } , [ rawGraphData , impact . isReady , visibleNodeIds , selectedNodeId , selectedImpact , hoveredFileId , isDark , clusterByDir , clusteredData ] )
344383
384+ // Fit view when nodes change or panel opens/closes
345385 useEffect ( ( ) => {
346386 if ( nodes . length > 0 ) {
347387 const minZoom = nodes . length > 20 ? 0.5 : 0.3
348- setTimeout ( ( ) => fitView ( { padding : 0.2 , duration : 300 , minZoom } ) , 100 )
388+ // Delay to allow container resize when panel opens/closes
389+ setTimeout ( ( ) => fitView ( { padding : 0.2 , duration : 300 , minZoom } ) , 150 )
349390 }
350- } , [ showAll , showTests , clusterByDir , expandedDirs ] )
391+ } , [ nodes . length , selectedNodeId , showAll , showTests , clusterByDir , expandedDirs , fitView ] )
351392
352393 const handleNodeClick = useCallback ( ( _ : any , node : Node ) => {
353394 // Toggle directory expansion
@@ -369,12 +410,26 @@ function DependencyGraphInner({ repoId, apiUrl, apiKey }: DependencyGraphProps)
369410 } , [ ] )
370411
371412 const handlePanelFileClick = useCallback ( ( fileId : string ) => {
413+ // Only select if the file is currently visible in the graph
414+ // Otherwise clicking on a non-visible dependent breaks the view
415+ if ( ! visibleNodeIds . has ( fileId ) ) {
416+ // If not visible, enable "show all" to make it visible first
417+ if ( ! showAll ) {
418+ setShowAll ( true )
419+ // Small delay to let the nodes render, then select
420+ setTimeout ( ( ) => {
421+ setSelectedNodeId ( fileId )
422+ } , 100 )
423+ return
424+ }
425+ }
426+
372427 setSelectedNodeId ( fileId )
373428 const node = nodes . find ( n => n . id === fileId )
374429 if ( node ) {
375430 fitView ( { nodes : [ node ] , padding : 0.5 , duration : 300 } )
376431 }
377- } , [ nodes , fitView ] )
432+ } , [ nodes , fitView , visibleNodeIds , showAll ] )
378433
379434 const handleResetView = useCallback ( ( ) => {
380435 setSelectedNodeId ( null )
@@ -460,6 +515,7 @@ function DependencyGraphInner({ repoId, apiUrl, apiKey }: DependencyGraphProps)
460515 < div className = "flex overflow-hidden" style = { { height : '600px' } } >
461516 < div className = "relative" style = { { flex : 1 , height : '600px' } } >
462517 < ReactFlow
518+ key = { renderKey }
463519 nodes = { nodes }
464520 edges = { edges }
465521 onNodesChange = { onNodesChange }
0 commit comments