11import { PanelMessage } from "@components/ui/PanelMessage" ;
22import { usePanelLayoutStore } from "@features/panels" ;
33import { useTaskData } from "@features/task-detail/hooks/useTaskData" ;
4- import {
5- FileIcon ,
6- FolderIcon ,
7- FolderOpenIcon ,
8- SpinnerGap ,
9- } from "@phosphor-icons/react" ;
4+ import { FileIcon , FolderIcon , FolderOpenIcon } from "@phosphor-icons/react" ;
105import { Box , Flex , Text } from "@radix-ui/themes" ;
116import type { Task } from "@shared/types" ;
12- import {
13- createContext ,
14- useCallback ,
15- useContext ,
16- useEffect ,
17- useMemo ,
18- useRef ,
19- useState ,
20- } from "react" ;
7+ import { useQuery , useQueryClient } from "@tanstack/react-query" ;
8+ import { useEffect , useState } from "react" ;
219
2210interface FileTreePanelProps {
2311 taskId : string ;
@@ -30,29 +18,6 @@ interface DirectoryEntry {
3018 type : "file" | "directory" ;
3119}
3220
33- type DirectoryChangeCallback = ( dirPath : string ) => void ;
34-
35- interface DirectoryChangeContextValue {
36- subscribe : ( dirPath : string , callback : DirectoryChangeCallback ) => ( ) => void ;
37- }
38-
39- const DirectoryChangeContext =
40- createContext < DirectoryChangeContextValue | null > ( null ) ;
41-
42- function useDirectoryChange (
43- dirPath : string | null ,
44- callback : DirectoryChangeCallback ,
45- ) {
46- const ctx = useContext ( DirectoryChangeContext ) ;
47- const callbackRef = useRef ( callback ) ;
48- callbackRef . current = callback ;
49-
50- useEffect ( ( ) => {
51- if ( ! ctx || ! dirPath ) return ;
52- return ctx . subscribe ( dirPath , ( path ) => callbackRef . current ( path ) ) ;
53- } , [ ctx , dirPath ] ) ;
54- }
55-
5621interface LazyTreeItemProps {
5722 entry : DirectoryEntry ;
5823 depth : number ;
@@ -62,62 +27,31 @@ interface LazyTreeItemProps {
6227
6328function LazyTreeItem ( { entry, depth, taskId, repoPath } : LazyTreeItemProps ) {
6429 const [ isExpanded , setIsExpanded ] = useState ( false ) ;
65- const [ children , setChildren ] = useState < DirectoryEntry [ ] | null > ( null ) ;
66- const [ isLoading , setIsLoading ] = useState ( false ) ;
6730 const openFile = usePanelLayoutStore ( ( state ) => state . openFile ) ;
6831
69- const loadChildren = useCallback ( async ( ) => {
70- if ( entry . type !== "directory" ) return ;
32+ const { data : children } = useQuery ( {
33+ queryKey : [ "directory" , entry . path ] ,
34+ queryFn : ( ) => window . electronAPI . listDirectory ( entry . path ) ,
35+ enabled : entry . type === "directory" && isExpanded ,
36+ staleTime : Infinity ,
37+ } ) ;
7138
72- setIsLoading ( true ) ;
73- try {
74- const entries = await window . electronAPI . listDirectory ( entry . path ) ;
75- setChildren ( entries ) ;
76- } catch ( error ) {
77- console . error ( "Failed to load directory:" , error ) ;
78- setChildren ( [ ] ) ;
79- } finally {
80- setIsLoading ( false ) ;
81- }
82- } , [ entry . path , entry . type ] ) ;
83-
84- const handleClick = async ( ) => {
39+ const handleClick = ( ) => {
8540 if ( entry . type === "directory" ) {
86- if ( ! isExpanded && children === null ) {
87- await loadChildren ( ) ;
88- }
8941 setIsExpanded ( ! isExpanded ) ;
9042 } else {
91- const relativePath = entry . path . replace ( `${ repoPath } /` , "" ) ;
92- openFile ( taskId , relativePath ) ;
43+ openFile ( taskId , entry . path . replace ( `${ repoPath } /` , "" ) ) ;
9344 }
9445 } ;
9546
96- useDirectoryChange (
97- entry . type === "directory" && isExpanded ? entry . path : null ,
98- useCallback ( ( ) => {
99- window . electronAPI
100- . listDirectory ( entry . path )
101- . then ( ( entries ) => {
102- setChildren ( entries ) ;
103- } )
104- . catch ( ( error ) => {
105- console . error ( "Failed to refresh directory:" , error ) ;
106- } ) ;
107- } , [ entry . path ] ) ,
108- ) ;
109-
11047 return (
11148 < Box >
11249 < Flex
11350 align = "center"
11451 gap = "2"
11552 py = "1"
11653 px = "2"
117- style = { {
118- paddingLeft : `${ depth * 16 + 8 } px` ,
119- cursor : "pointer" ,
120- } }
54+ style = { { paddingLeft : `${ depth * 16 + 8 } px` , cursor : "pointer" } }
12155 className = "rounded hover:bg-gray-2"
12256 onClick = { handleClick }
12357 >
@@ -133,101 +67,43 @@ function LazyTreeItem({ entry, depth, taskId, repoPath }: LazyTreeItemProps) {
13367 < Text size = "2" style = { { userSelect : "none" } } >
13468 { entry . name }
13569 </ Text >
136- { isLoading && (
137- < SpinnerGap
138- size = { 12 }
139- className = "animate-spin"
140- color = "var(--gray-9)"
141- />
142- ) }
14370 </ Flex >
144- { entry . type === "directory" && isExpanded && children && (
145- < Box >
146- { children . map ( ( child ) => (
147- < LazyTreeItem
148- key = { child . path }
149- entry = { child }
150- depth = { depth + 1 }
151- taskId = { taskId }
152- repoPath = { repoPath }
153- />
154- ) ) }
155- </ Box >
156- ) }
71+ { isExpanded &&
72+ children ?. map ( ( child ) => (
73+ < LazyTreeItem
74+ key = { child . path }
75+ entry = { child }
76+ depth = { depth + 1 }
77+ taskId = { taskId }
78+ repoPath = { repoPath }
79+ />
80+ ) ) }
15781 </ Box >
15882 ) ;
15983}
16084
16185export function FileTreePanel ( { taskId, task } : FileTreePanelProps ) {
16286 const taskData = useTaskData ( { taskId, initialTask : task } ) ;
16387 const repoPath = taskData . repoPath ;
164-
165- const [ rootEntries , setRootEntries ] = useState < DirectoryEntry [ ] | null > ( null ) ;
166- const [ isLoading , setIsLoading ] = useState ( true ) ;
167- const [ error , setError ] = useState < string | null > ( null ) ;
168-
169- const subscribersRef = useRef < Map < string , Set < DirectoryChangeCallback > > > (
170- new Map ( ) ,
171- ) ;
172-
173- const contextValue = useMemo < DirectoryChangeContextValue > (
174- ( ) => ( {
175- subscribe : ( dirPath , callback ) => {
176- if ( ! subscribersRef . current . has ( dirPath ) ) {
177- subscribersRef . current . set ( dirPath , new Set ( ) ) ;
178- }
179- subscribersRef . current . get ( dirPath ) ?. add ( callback ) ;
180- return ( ) => {
181- subscribersRef . current . get ( dirPath ) ?. delete ( callback ) ;
182- if ( subscribersRef . current . get ( dirPath ) ?. size === 0 ) {
183- subscribersRef . current . delete ( dirPath ) ;
184- }
185- } ;
186- } ,
187- } ) ,
188- [ ] ,
189- ) ;
88+ const queryClient = useQueryClient ( ) ;
89+
90+ const {
91+ data : rootEntries ,
92+ isLoading,
93+ error,
94+ } = useQuery ( {
95+ queryKey : [ "directory" , repoPath ] ,
96+ queryFn : ( ) => window . electronAPI . listDirectory ( repoPath ! ) ,
97+ enabled : ! ! repoPath ,
98+ staleTime : Infinity ,
99+ } ) ;
190100
191101 useEffect ( ( ) => {
192102 if ( ! repoPath ) return ;
193-
194- setIsLoading ( true ) ;
195- setError ( null ) ;
196-
197- window . electronAPI
198- . listDirectory ( repoPath )
199- . then ( ( entries ) => {
200- setRootEntries ( entries ) ;
201- } )
202- . catch ( ( err ) => {
203- console . error ( "Failed to load root directory:" , err ) ;
204- setError ( "Failed to load files" ) ;
205- } )
206- . finally ( ( ) => {
207- setIsLoading ( false ) ;
208- } ) ;
209- } , [ repoPath ] ) ;
210-
211- useEffect ( ( ) => {
212- if ( ! repoPath ) return ;
213-
214- const unsub = window . electronAPI . onDirectoryChanged ( ( { dirPath } ) => {
215- if ( dirPath === repoPath ) {
216- window . electronAPI . listDirectory ( repoPath ) . then ( ( entries ) => {
217- setRootEntries ( entries ) ;
218- } ) ;
219- }
220-
221- const callbacks = subscribersRef . current . get ( dirPath ) ;
222- if ( callbacks ) {
223- for ( const cb of callbacks ) {
224- cb ( dirPath ) ;
225- }
226- }
103+ return window . electronAPI . onDirectoryChanged ( ( { dirPath } ) => {
104+ queryClient . invalidateQueries ( { queryKey : [ "directory" , dirPath ] } ) ;
227105 } ) ;
228-
229- return unsub ;
230- } , [ repoPath ] ) ;
106+ } , [ repoPath , queryClient ] ) ;
231107
232108 if ( ! repoPath ) {
233109 return < PanelMessage > No repository path available</ PanelMessage > ;
@@ -238,28 +114,26 @@ export function FileTreePanel({ taskId, task }: FileTreePanelProps) {
238114 }
239115
240116 if ( error ) {
241- return < PanelMessage color = "red" > { error } </ PanelMessage > ;
117+ return < PanelMessage color = "red" > Failed to load files </ PanelMessage > ;
242118 }
243119
244- if ( ! rootEntries || rootEntries . length === 0 ) {
120+ if ( ! rootEntries ? .length ) {
245121 return < PanelMessage > No files found</ PanelMessage > ;
246122 }
247123
248124 return (
249- < DirectoryChangeContext . Provider value = { contextValue } >
250- < Box height = "100%" overflowY = "auto" p = "4" >
251- < Flex direction = "column" gap = "1" >
252- { rootEntries . map ( ( entry ) => (
253- < LazyTreeItem
254- key = { entry . path }
255- entry = { entry }
256- depth = { 0 }
257- taskId = { taskId }
258- repoPath = { repoPath }
259- />
260- ) ) }
261- </ Flex >
262- </ Box >
263- </ DirectoryChangeContext . Provider >
125+ < Box height = "100%" overflowY = "auto" p = "4" >
126+ < Flex direction = "column" gap = "1" >
127+ { rootEntries . map ( ( entry ) => (
128+ < LazyTreeItem
129+ key = { entry . path }
130+ entry = { entry }
131+ depth = { 0 }
132+ taskId = { taskId }
133+ repoPath = { repoPath }
134+ />
135+ ) ) }
136+ </ Flex >
137+ </ Box >
264138 ) ;
265139}
0 commit comments