@@ -480,6 +480,7 @@ export function renderFiles(container: HTMLElement, channelId: number, initialPa
480480 const info = h ( "aside" , { class : "hidden min-h-0 w-60 shrink-0 overflow-y-auto border-l border-line bg-raised/35 p-4 xl:block" , dataset : { fileMetadata : "" } } ) ;
481481 const crumbs = h ( "nav" , { class : "flex min-w-0 flex-1 items-center gap-1 overflow-x-auto font-mono text-xs" , "aria-label" : "File breadcrumbs" , dataset : { fileBreadcrumbs : "" } } ) ;
482482 const fileInput = h ( "input" , { type : "file" , multiple : true , class : "hidden" } ) as HTMLInputElement ;
483+ let directoryCache : ChannelFile [ ] | null = null ;
483484 const open = ( entry : ChannelFile ) : void => {
484485 if ( entry . kind === "directory" ) { currentPath = entry . path ; selected = null ; void load ( ) ; return ; }
485486 const target = coworkPath ( entry . path ) ;
@@ -535,16 +536,24 @@ export function renderFiles(container: HTMLElement, channelId: number, initialPa
535536 selected . kind === "file" ? h ( "button" , { class : "btn-subtle text-xs" , type : "button" , onclick : ( ) => { void downloadAuthenticatedFile ( `/api/channels/${ channelId } /files/content?path=${ encodeURIComponent ( selected ! . path ) } &download=1` , selected ! . name ) ; } } , "Download" ) : null ,
536537 h ( "button" , { class : "btn-ghost text-xs text-danger" , type : "button" , onclick : ( ) => { void mutate ( "delete" ) ; } } , "Delete" ) ) ) ;
537538 } ;
538- const load = async ( ) : Promise < void > => {
539+ const refreshDirectories = async ( ) : Promise < void > => {
540+ try {
541+ directoryCache = ( await api < { directories : ChannelFile [ ] } > ( `/api/channels/${ channelId } /files/directories` ) ) . directories ;
542+ drawTree ( directoryCache ) ;
543+ } catch { /* the current directory remains usable while the tree retries */ }
544+ } ;
545+ const redrawSelection = ( ) : void => {
546+ main . querySelectorAll < HTMLElement > ( "[data-file-path]" ) . forEach ( ( node ) => node . classList . toggle ( "is-selected" , node . dataset . filePath === selected ?. path ) ) ;
547+ drawInfo ( ) ;
548+ } ;
549+ const load = async ( options : { refreshTree ?: boolean } = { } ) : Promise < void > => {
539550 const requestedPath = currentPath ;
540551 try {
541- const [ result , folders ] = await Promise . all ( [
542- api < { path ?: string ; files : ChannelFile [ ] } > ( `/api/channels/${ channelId } /files?path=${ encodeURIComponent ( requestedPath ) } ` ) ,
543- api < { directories : ChannelFile [ ] } > ( `/api/channels/${ channelId } /files/directories` ) ,
544- ] ) ;
552+ const result = await api < { path ?: string ; files : ChannelFile [ ] } > ( `/api/channels/${ channelId } /files?path=${ encodeURIComponent ( requestedPath ) } ` ) ;
545553 if ( requestedPath !== currentPath ) return ;
546554 currentPath = result . path ?? requestedPath ; heading . textContent = `/workspace${ currentPath ? `/${ currentPath } ` : "" } ` ; main . dataset . fileDirectory = currentPath || "/" ;
547- drawTree ( folders . directories ) ;
555+ if ( directoryCache ) drawTree ( directoryCache ) ;
556+ else void refreshDirectories ( ) ;
548557 clear ( crumbs ) ;
549558 const segments = currentPath ? currentPath . split ( "/" ) : [ ] ;
550559 const addCrumb = ( label : string , path : string ) : void => {
@@ -558,22 +567,22 @@ export function renderFiles(container: HTMLElement, channelId: number, initialPa
558567 if ( ! files . length ) main . append ( empty ( result . files . length ? "No matches" : "This folder is empty" , result . files . length ? "Try a different search." : "Create a folder or file, upload something, or let the resident agent add it." ) ) ;
559568 else main . append ( h ( "div" , { class : "file-grid" , role : "list" } , ...files . map ( ( entry ) => h ( "button" , {
560569 class : `file-grid-item ${ selected ?. path === entry . path ? "is-selected" : "" } ` , type : "button" , role : "listitem" , dataset : { filePath : entry . path , fileKind : entry . kind } ,
561- onclick : ( ) => { selected = entry ; void load ( ) ; } , ondblclick : ( ) => open ( entry ) ,
570+ onclick : ( ) => { selected = entry ; redrawSelection ( ) ; } , ondblclick : ( ) => open ( entry ) ,
562571 } , h ( "span" , { class : `file-grid-icon ${ entry . kind === "directory" ? "is-folder" : "is-file" } ` } , workspaceIcon ( entry , 27 ) ) , h ( "span" , { class : "min-w-0 flex-1 text-left" } , h ( "span" , { class : "block truncate text-sm font-semibold text-fg" } , entry . name ) , h ( "span" , { class : "mt-0.5 block truncate text-[11px] text-muted" } , entry . kind === "directory" ? "Folder" : `${ formatBytes ( entry . size ) } · ${ timeLabel ( entry . modified ) } ` ) ) , h ( "span" , { class : "file-grid-kind" } , entry . kind === "directory" ? "Folder" : ( entry . name . split ( "." ) . pop ( ) ?. toUpperCase ( ) || "File" ) ) ) ) ) ) ;
563572 drawInfo ( ) ; status . textContent = `${ result . files . length } item${ result . files . length === 1 ? "" : "s" } ` ;
564573 } catch ( error ) { panelError ( main , error ) ; }
565574 } ;
566575 search . oninput = ( ) => { filter = search . value . trim ( ) . toLowerCase ( ) ; void load ( ) ; } ;
567576 const sortSelect = h ( "select" , { class : "field h-9 w-auto min-w-28 text-xs" , "aria-label" : "Sort files" , onchange : ( event : Event ) => { sort = ( event . target as HTMLSelectElement ) . value as typeof sort ; void load ( ) ; } } , h ( "option" , { value : "name" } , "Name" ) , h ( "option" , { value : "modified" } , "Modified" ) , h ( "option" , { value : "size" } , "Size" ) ) ;
568- const newFolder = async ( ) : Promise < void > => { const name = await appPrompt ( "Folder name" ) ; if ( ! name ) return ; try { await api ( `/api/channels/${ channelId } /files/directories` , { body : { path : currentPath , name } } ) ; await load ( ) ; } catch ( error ) { status . textContent = ( error as Error ) . message ; } } ;
577+ const newFolder = async ( ) : Promise < void > => { const name = await appPrompt ( "Folder name" ) ; if ( ! name ) return ; try { await api ( `/api/channels/${ channelId } /files/directories` , { body : { path : currentPath , name } } ) ; directoryCache = null ; await load ( ) ; } catch ( error ) { status . textContent = ( error as Error ) . message ; } } ;
569578 const newFile = async ( ) : Promise < void > => { const name = await appPrompt ( "File name" , "untitled.md" ) ; if ( ! name ) return ; try { await api ( `/api/channels/${ channelId } /files/entries` , { body : { parent : currentPath , name, content : "" } } ) ; await load ( ) ; } catch ( error ) { status . textContent = ( error as Error ) . message ; } } ;
570579 fileInput . onchange = async ( ) => { const chosen = Array . from ( fileInput . files || [ ] ) ; if ( ! chosen . length ) return ; status . textContent = `Uploading ${ chosen . length } item${ chosen . length === 1 ? "" : "s" } …` ; try { for ( const file of chosen ) { const upload = await uploadFile ( file ) ; await api ( `/api/channels/${ channelId } /files/upload` , { body : { ...upload , path : currentPath } } ) ; } fileInput . value = "" ; await load ( ) ; } catch ( error ) { status . textContent = ( error as Error ) . message ; } } ;
571580 root . append (
572581 h ( "header" , { class : "flex min-h-14 flex-wrap items-center gap-2 border-b border-line px-3 py-2 sm:px-4" } , h ( "span" , { class : "text-accent" } , icon ( "folderOpen" , 20 ) ) , heading , h ( "div" , { class : "flex-1" } ) , status , h ( "button" , { class : "btn-subtle text-xs" , type : "button" , onclick : ( ) => { void newFile ( ) ; } } , icon ( "plus" , 14 ) , "New file" ) , h ( "button" , { class : "btn-subtle text-xs" , type : "button" , onclick : ( ) => { void newFolder ( ) ; } } , icon ( "folder" , 14 ) , "New folder" ) , h ( "button" , { class : "btn-primary text-xs" , type : "button" , onclick : ( ) => fileInput . click ( ) } , "Upload" ) , fileInput ) ,
573582 h ( "div" , { class : "flex min-h-0 flex-1" } ,
574583 h ( "aside" , { class : "hidden min-h-0 w-60 shrink-0 flex-col border-r border-line bg-raised/35 md:flex" } , h ( "div" , { class : "border-b border-line p-3" } , h ( "div" , { class : "eyebrow text-muted" } , "Folders" ) ) , tree ) ,
575584 h ( "section" , { class : "flex min-h-0 min-w-0 flex-1 flex-col" } , h ( "div" , { class : "flex flex-wrap items-center gap-2 border-b border-line bg-raised/25 px-3 py-2" } , crumbs , h ( "div" , { class : "w-full sm:w-48" } , search ) , sortSelect ) , main ) , info ) ) ;
576- fileBrowserSurfaces . set ( channelId , { node : root , reload : load } ) ;
585+ fileBrowserSurfaces . set ( channelId , { node : root , reload : async ( ) => { directoryCache = null ; await load ( ) ; } } ) ;
577586 clear ( container ) ; container . append ( root ) ; void load ( ) ;
578587}
579588
0 commit comments