@@ -18,7 +18,7 @@ import InfoView from './components/InfoView';
1818import UpdateNotification from './components/UpdateNotification' ;
1919import CreateFromTemplateModal from './components/CreateFromTemplateModal' ;
2020import DocumentHistoryView from './components/PromptHistoryView' ;
21- import FolderOverview , { FolderOverviewMetrics } from './components/FolderOverview' ;
21+ import FolderOverview , { FolderOverviewMetrics , FolderSearchResult , RecentDocumentSummary } from './components/FolderOverview' ;
2222import { PlusIcon , FolderPlusIcon , TrashIcon , GearIcon , InfoIcon , TerminalIcon , DocumentDuplicateIcon , PencilIcon , CopyIcon , CommandIcon , CodeIcon , FolderDownIcon , FormatIcon , SparklesIcon } from './components/Icons' ;
2323import AboutModal from './components/AboutModal' ;
2424import Header from './components/Header' ;
@@ -122,6 +122,9 @@ const MainApp: React.FC = () => {
122122 const [ isDraggingFile , setIsDraggingFile ] = useState ( false ) ;
123123 const [ formatTrigger , setFormatTrigger ] = useState ( 0 ) ;
124124 const [ bodySearchMatches , setBodySearchMatches ] = useState < Map < string , string > > ( new Map ( ) ) ;
125+ const [ folderSearchTerm , setFolderSearchTerm ] = useState ( '' ) ;
126+ const [ folderBodySearchMatches , setFolderBodySearchMatches ] = useState < Map < string , string > > ( new Map ( ) ) ;
127+ const [ isFolderSearchLoading , setIsFolderSearchLoading ] = useState ( false ) ;
125128
126129
127130 const isSidebarResizing = useRef ( false ) ;
@@ -189,6 +192,12 @@ const MainApp: React.FC = () => {
189192 return itemsWithSearchMetadata . find ( p => p . id === activeNodeId ) || null ;
190193 } , [ itemsWithSearchMetadata , activeNodeId ] ) ;
191194
195+ useEffect ( ( ) => {
196+ setFolderSearchTerm ( '' ) ;
197+ setFolderBodySearchMatches ( new Map ( ) ) ;
198+ setIsFolderSearchLoading ( false ) ;
199+ } , [ activeNode ?. id , activeNode ?. type ] ) ;
200+
192201 const activeTemplate = useMemo ( ( ) => {
193202 return templates . find ( t => t . template_id === activeTemplateId ) || null ;
194203 } , [ templates , activeTemplateId ] ) ;
@@ -225,6 +234,53 @@ const MainApp: React.FC = () => {
225234 } ;
226235 } , [ searchTerm ] ) ;
227236
237+ useEffect ( ( ) => {
238+ if ( ! activeNode || activeNode . type !== 'folder' ) {
239+ return ;
240+ }
241+
242+ const term = folderSearchTerm . trim ( ) ;
243+ if ( ! term ) {
244+ setFolderBodySearchMatches ( new Map ( ) ) ;
245+ setIsFolderSearchLoading ( false ) ;
246+ return ;
247+ }
248+
249+ let isCancelled = false ;
250+ setIsFolderSearchLoading ( true ) ;
251+ setFolderBodySearchMatches ( new Map ( ) ) ;
252+
253+ repository . searchDocumentsByBody ( term , 200 )
254+ . then ( results => {
255+ if ( isCancelled ) {
256+ return ;
257+ }
258+ const descendantIds = getDescendantIds ( activeNode . id ) ;
259+ const matches = new Map < string , string > ( ) ;
260+ for ( const result of results ) {
261+ if ( descendantIds . has ( result . nodeId ) ) {
262+ matches . set ( result . nodeId , result . snippet ) ;
263+ }
264+ }
265+ setFolderBodySearchMatches ( matches ) ;
266+ } )
267+ . catch ( error => {
268+ if ( ! isCancelled ) {
269+ console . error ( 'Failed to search within folder:' , error ) ;
270+ setFolderBodySearchMatches ( new Map ( ) ) ;
271+ }
272+ } )
273+ . finally ( ( ) => {
274+ if ( ! isCancelled ) {
275+ setIsFolderSearchLoading ( false ) ;
276+ }
277+ } ) ;
278+
279+ return ( ) => {
280+ isCancelled = true ;
281+ } ;
282+ } , [ activeNode , folderSearchTerm , getDescendantIds ] ) ;
283+
228284 const { documentTree, navigableItems } = useMemo ( ( ) => {
229285 let itemsToBuildFrom = itemsWithSearchMetadata ;
230286 if ( searchTerm . trim ( ) ) {
@@ -296,9 +352,9 @@ const MainApp: React.FC = () => {
296352 return { documentTree : finalTree , navigableItems : flatList } ;
297353 } , [ itemsWithSearchMetadata , templates , searchTerm , expandedFolderIds ] ) ;
298354
299- const activeFolderMetrics = useMemo < FolderOverviewMetrics | null > ( ( ) => {
355+ const { metrics : activeFolderMetrics , documents : activeFolderDocuments } = useMemo ( ( ) => {
300356 if ( ! activeNode || activeNode . type !== 'folder' ) {
301- return null ;
357+ return { metrics : null as FolderOverviewMetrics | null , documents : [ ] as RecentDocumentSummary [ ] } ;
302358 }
303359
304360 const parseDate = ( value ?: string | null ) : Date | null => {
@@ -307,15 +363,15 @@ const MainApp: React.FC = () => {
307363 return Number . isNaN ( date . getTime ( ) ) ? null : date ;
308364 } ;
309365
310- const formatNodeTitle = ( node : DocumentOrFolder ) => {
366+ const formatNodeTitle = ( node : { title : string ; type : 'document' | 'folder' } ) => {
311367 const trimmed = node . title . trim ( ) ;
312368 if ( trimmed ) {
313369 return trimmed ;
314370 }
315371 return node . type === 'folder' ? 'Untitled Folder' : 'Untitled Document' ;
316372 } ;
317373
318- const computeFromTree = ( folderNode : DocumentNode ) : FolderOverviewMetrics => {
374+ const computeFromTree = ( folderNode : DocumentNode ) => {
319375 const recordLatest = ( ( ) => {
320376 let latest : Date | null = null ;
321377 return {
@@ -343,14 +399,14 @@ const MainApp: React.FC = () => {
343399 node : child ,
344400 parentPath : [ ] ,
345401 } ) ) ;
346- const recentDocuments : FolderOverviewMetrics [ 'recentDocuments' ] = [ ] ;
402+ const allDocuments : RecentDocumentSummary [ ] = [ ] ;
347403
348404 while ( stack . length > 0 ) {
349405 const { node : current , parentPath } = stack . pop ( ) ! ;
350406 recordLatest . update ( current . updatedAt ) ;
351407 if ( current . type === 'document' ) {
352408 totalDocumentCount += 1 ;
353- recentDocuments . push ( {
409+ allDocuments . push ( {
354410 id : current . id ,
355411 title : current . title ,
356412 updatedAt : current . updatedAt ,
@@ -365,7 +421,7 @@ const MainApp: React.FC = () => {
365421 }
366422
367423 const latestDate = recordLatest . getValue ( ) ;
368- const sortedRecent = recentDocuments
424+ const recentDocuments = [ ... allDocuments ]
369425 . sort ( ( a , b ) => {
370426 const aDate = parseDate ( a . updatedAt ) ?. getTime ( ) ?? 0 ;
371427 const bDate = parseDate ( b . updatedAt ) ?. getTime ( ) ?? 0 ;
@@ -374,13 +430,16 @@ const MainApp: React.FC = () => {
374430 . slice ( 0 , 5 ) ;
375431
376432 return {
377- directDocumentCount,
378- directFolderCount,
379- totalDocumentCount,
380- totalFolderCount,
381- totalItemCount : totalDocumentCount + totalFolderCount ,
382- lastUpdated : latestDate ? latestDate . toISOString ( ) : null ,
383- recentDocuments : sortedRecent ,
433+ metrics : {
434+ directDocumentCount,
435+ directFolderCount,
436+ totalDocumentCount,
437+ totalFolderCount,
438+ totalItemCount : totalDocumentCount + totalFolderCount ,
439+ lastUpdated : latestDate ? latestDate . toISOString ( ) : null ,
440+ recentDocuments,
441+ } ,
442+ documents : allDocuments ,
384443 } ;
385444 } ;
386445
@@ -396,7 +455,7 @@ const MainApp: React.FC = () => {
396455 return map ;
397456 } ;
398457
399- const computeFromList = ( ) : FolderOverviewMetrics => {
458+ const computeFromList = ( ) => {
400459 const childMap = buildChildMap ( ) ;
401460 const directChildren = childMap . get ( activeNode . id ) ?? [ ] ;
402461
@@ -426,14 +485,14 @@ const MainApp: React.FC = () => {
426485 node : child ,
427486 parentPath : [ ] ,
428487 } ) ) ;
429- const recentDocuments : FolderOverviewMetrics [ 'recentDocuments' ] = [ ] ;
488+ const allDocuments : RecentDocumentSummary [ ] = [ ] ;
430489
431490 while ( stack . length > 0 ) {
432491 const { node : current , parentPath } = stack . pop ( ) ! ;
433492 recordLatest . update ( current . updatedAt ) ;
434493 if ( current . type === 'document' ) {
435494 totalDocumentCount += 1 ;
436- recentDocuments . push ( {
495+ allDocuments . push ( {
437496 id : current . id ,
438497 title : current . title ,
439498 updatedAt : current . updatedAt ,
@@ -448,7 +507,7 @@ const MainApp: React.FC = () => {
448507 }
449508
450509 const latestDate = recordLatest . getValue ( ) ;
451- const sortedRecent = recentDocuments
510+ const recentDocuments = [ ... allDocuments ]
452511 . sort ( ( a , b ) => {
453512 const aDate = parseDate ( a . updatedAt ) ?. getTime ( ) ?? 0 ;
454513 const bDate = parseDate ( b . updatedAt ) ?. getTime ( ) ?? 0 ;
@@ -457,13 +516,16 @@ const MainApp: React.FC = () => {
457516 . slice ( 0 , 5 ) ;
458517
459518 return {
460- directDocumentCount,
461- directFolderCount,
462- totalDocumentCount,
463- totalFolderCount,
464- totalItemCount : totalDocumentCount + totalFolderCount ,
465- lastUpdated : latestDate ? latestDate . toISOString ( ) : null ,
466- recentDocuments : sortedRecent ,
519+ metrics : {
520+ directDocumentCount,
521+ directFolderCount,
522+ totalDocumentCount,
523+ totalFolderCount,
524+ totalItemCount : totalDocumentCount + totalFolderCount ,
525+ lastUpdated : latestDate ? latestDate . toISOString ( ) : null ,
526+ recentDocuments,
527+ } ,
528+ documents : allDocuments ,
467529 } ;
468530 } ;
469531
@@ -489,6 +551,80 @@ const MainApp: React.FC = () => {
489551 return computeFromList ( ) ;
490552 } , [ activeNode , documentTree , items ] ) ;
491553
554+ const folderSearchResults = useMemo < FolderSearchResult [ ] > ( ( ) => {
555+ if ( ! activeNode || activeNode . type !== 'folder' ) {
556+ return [ ] ;
557+ }
558+
559+ const trimmed = folderSearchTerm . trim ( ) ;
560+ if ( ! trimmed ) {
561+ return [ ] ;
562+ }
563+
564+ const lowerTerm = trimmed . toLowerCase ( ) ;
565+
566+ const parseToTimestamp = ( value ?: string | null ) => {
567+ if ( ! value ) {
568+ return 0 ;
569+ }
570+ const date = new Date ( value ) ;
571+ return Number . isNaN ( date . getTime ( ) ) ? 0 : date . getTime ( ) ;
572+ } ;
573+
574+ const computeMatchScore = ( fields : ( 'title' | 'body' ) [ ] ) => {
575+ if ( fields . length === 2 ) {
576+ return 0 ;
577+ }
578+ return fields [ 0 ] === 'title' ? 1 : 2 ;
579+ } ;
580+
581+ type FolderSearchResultWithScore = FolderSearchResult & { matchScore : number ; sortTimestamp : number ; } ;
582+
583+ const results : FolderSearchResultWithScore [ ] = [ ] ;
584+
585+ for ( const document of activeFolderDocuments ) {
586+ const titleLower = document . title . toLowerCase ( ) ;
587+ const hasTitleMatch = titleLower . includes ( lowerTerm ) ;
588+ const snippet = folderBodySearchMatches . get ( document . id ) ;
589+ const hasBodyMatch = Boolean ( snippet ) ;
590+
591+ if ( ! hasTitleMatch && ! hasBodyMatch ) {
592+ continue ;
593+ }
594+
595+ const matchedFields : ( 'title' | 'body' ) [ ] = [ ] ;
596+ if ( hasTitleMatch ) {
597+ matchedFields . push ( 'title' ) ;
598+ }
599+ if ( hasBodyMatch ) {
600+ matchedFields . push ( 'body' ) ;
601+ }
602+
603+ results . push ( {
604+ id : document . id ,
605+ title : document . title ,
606+ updatedAt : document . updatedAt ,
607+ parentPath : document . parentPath ,
608+ searchSnippet : snippet ,
609+ matchedFields,
610+ matchScore : computeMatchScore ( matchedFields ) ,
611+ sortTimestamp : parseToTimestamp ( document . updatedAt ) ,
612+ } ) ;
613+ }
614+
615+ return results
616+ . sort ( ( a , b ) => {
617+ if ( a . matchScore !== b . matchScore ) {
618+ return a . matchScore - b . matchScore ;
619+ }
620+ if ( a . sortTimestamp !== b . sortTimestamp ) {
621+ return b . sortTimestamp - a . sortTimestamp ;
622+ }
623+ return a . title . localeCompare ( b . title ) ;
624+ } )
625+ . map ( ( { matchScore : _matchScore , sortTimestamp : _sortTimestamp , ...rest } ) => rest ) ;
626+ } , [ activeNode , activeFolderDocuments , folderBodySearchMatches , folderSearchTerm ] ) ;
627+
492628 useEffect ( ( ) => {
493629 if ( window . electronAPI ?. getAppVersion ) {
494630 window . electronAPI . getAppVersion ( ) . then ( setAppVersion ) ;
@@ -1367,6 +1503,10 @@ const MainApp: React.FC = () => {
13671503 onNewSubfolder = { ( parentId ) => handleNewFolder ( parentId ) }
13681504 onImportFiles = { handleImportFilesIntoFolder }
13691505 onRenameFolder = { handleStartRenamingNode }
1506+ folderSearchTerm = { folderSearchTerm }
1507+ onFolderSearchTermChange = { setFolderSearchTerm }
1508+ searchResults = { folderSearchResults }
1509+ isSearchLoading = { isFolderSearchLoading }
13701510 />
13711511 ) ;
13721512 }
0 commit comments