@ -33,6 +33,10 @@ function formatRelativeTime(timestamp: number): string {
return ` ${ months } mo ago ` ;
}
function dirBasename ( dirPath : string ) : string {
return dirPath . split ( "/" ) . filter ( Boolean ) . pop ( ) || dirPath ;
}
export function WorkflowBrowserView ( {
onBack ,
onWorkflowLoaded ,
@ -71,27 +75,13 @@ export function WorkflowBrowserView({
}
} , [ ] ) ;
// Fetch workflows when default directory is set
useEffect ( ( ) = > {
if ( defaultDir ) {
fetchWorkflows ( defaultDir ) ;
}
} , [ defaultDir , fetchWorkflows ] ) ;
const handleChooseFolder = useCallback ( async ( ) = > {
try {
const res = await fetch ( "/api/browse-directory" ) ;
const result = await res . json ( ) ;
if ( result . success && ! result . cancelled && result . path ) {
setWorkflowsDirectory ( result . path ) ;
setDefaultDir ( result . path ) ;
}
} catch {
setError ( "Failed to open directory picker" ) ;
}
} , [ ] ) ;
const handleChangeFolder = useCallback ( async ( ) = > {
const browseAndSetDir = useCallback ( async ( ) = > {
try {
const res = await fetch ( "/api/browse-directory" ) ;
const result = await res . json ( ) ;
@ -205,7 +195,7 @@ export function WorkflowBrowserView({
< / p >
< / div >
< button
onClick = { handleChooseFolde r}
onClick = { browseAndSetDi r}
className = "px-4 py-2 text-sm font-medium text-neutral-200 bg-neutral-700 hover:bg-neutral-600 rounded-lg transition-colors"
>
Choose folder
@ -233,82 +223,127 @@ export function WorkflowBrowserView({
{ defaultDir }
< / p >
< / div >
< span className = "text-xs text-neutral-600 tabular-nums flex-shrink-0" >
{ workflows . length } project { workflows . length !== 1 ? "s" : "" }
< / span >
< / div >
< / div >
{ /* Workflow list */ }
< div className = "flex-1 overflow-y-auto px-6 py-3 " >
< div className = "flex-1 overflow-y-auto overscroll-contain px-4 py-2 " >
{ isLoading ? (
< div className = "flex items-center justify-center py-12" >
< div className = "w-5 h-5 border-2 border-neutral-600 border-t-neutral-300 rounded-full animate-spin" / >
< / div >
) : error && workflows . length === 0 ? (
< div className = "flex flex-col items-center justify-center py-12 text-center" >
< p className = "text-sm text-neutral-500" > { error } < / p >
< svg
className = "w-10 h-10 text-neutral-700 mb-3"
fill = "none"
viewBox = "0 0 24 24"
stroke = "currentColor"
strokeWidth = { 1.5 }
>
< path
strokeLinecap = "round"
strokeLinejoin = "round"
d = "M2.25 12.75V12A2.25 2.25 0 014.5 9.75h15A2.25 2.25 0 0121.75 12v.75m-8.69-6.44l-2.12-2.12a1.5 1.5 0 00-1.061-.44H4.5A2.25 2.25 0 002.25 6v12a2.25 2.25 0 002.25 2.25h15A2.25 2.25 0 0021.75 18V9a2.25 2.25 0 00-2.25-2.25h-5.379a1.5 1.5 0 01-1.06-.44z"
/ >
< / svg >
< p className = "text-sm text-neutral-400 mb-1" > No workflows found < / p >
< p className = "text-xs text-neutral-600" >
This folder doesn & apos ; t contain any workflow projects
< / p >
< / div >
) : (
< div className = "flex flex-col gap-1" >
{ workflows . map ( ( entry ) = > (
< button
key = { entry . directoryPath }
onClick = { ( ) = > handleSelectWorkflow ( entry ) }
disabled = { loadingWorkflow !== null }
className = "group text-left px-4 py-3 rounded-lg hover:bg-neutral-700/40 transition-colors disabled:opacity-50"
>
< div className = "flex items-center justify-between" >
< div className = "min-w-0 flex-1" >
< span className = "text-sm font-medium text-neutral-200 group-hover:text-neutral-100 transition-colors" >
{ entry . name }
< / span >
< div className = "mt-0.5" >
< span className = "text-xs text-neutral-500" >
{ formatRelativeTime ( entry . lastModified ) }
< / span >
< div className = "flex flex-col gap-0.5" >
{ workflows . map ( ( entry ) = > {
const isActive = loadingWorkflow === entry . directoryPath ;
return (
< button
key = { entry . directoryPath }
onClick = { ( ) = > handleSelectWorkflow ( entry ) }
disabled = { loadingWorkflow !== null }
className = { `
group text - left px - 3 py - 2.5 rounded - lg transition - all duration - 100
disabled :opacity - 50
$ { isActive
? "bg-neutral-700/60 border border-neutral-600"
: "border border-transparent hover:bg-neutral-700/30 hover:border-neutral-700/50"
}
` }
>
< div className = "flex items-center gap-3" >
{ /* Folder icon */ }
< div className = { `
w - 8 h - 8 rounded - md flex items - center justify - center flex - shrink - 0 transition - colors
$ { isActive ? "bg-blue-500/20" : "bg-neutral-700/50 group-hover:bg-neutral-700" }
` }>
{ isActive ? (
< div className = "w-3.5 h-3.5 border-2 border-blue-400/40 border-t-blue-400 rounded-full animate-spin" / >
) : (
< svg
className = "w-4 h-4 text-neutral-400 group-hover:text-neutral-300 transition-colors"
fill = "none"
viewBox = "0 0 24 24"
stroke = "currentColor"
strokeWidth = { 1.5 }
>
< path
strokeLinecap = "round"
strokeLinejoin = "round"
d = "M2.25 12.75V12A2.25 2.25 0 014.5 9.75h15A2.25 2.25 0 0121.75 12v.75m-8.69-6.44l-2.12-2.12a1.5 1.5 0 00-1.061-.44H4.5A2.25 2.25 0 002.25 6v12a2.25 2.25 0 002.25 2.25h15A2.25 2.25 0 0021.75 18V9a2.25 2.25 0 00-2.25-2.25h-5.379a1.5 1.5 0 01-1.06-.44z"
/ >
< / svg >
) }
< / div >
{ /* Name + folder name */ }
< div className = "flex-1 min-w-0" >
< div className = "text-sm font-medium text-neutral-200 group-hover:text-neutral-100 transition-colors truncate" >
{ entry . name }
< / div >
< div className = "text-[11px] text-neutral-600 truncate" >
{ dirBasename ( entry . directoryPath ) }
< / div >
< / div >
{ /* Timestamp */ }
< span className = "text-[11px] text-neutral-600 tabular-nums flex-shrink-0" >
{ formatRelativeTime ( entry . lastModified ) }
< / span >
< / div >
{ loadingWorkflow === entry . directoryPath ? (
< div className = "w-4 h-4 border-2 border-neutral-600 border-t-neutral-300 rounded-full animate-spin" / >
) : (
< svg
className = "w-4 h-4 text-neutral-600 group-hover:text-neutral-400 transition-colors"
fill = "none"
viewBox = "0 0 24 24"
stroke = "currentColor"
strokeWidth = { 2 }
>
< path
strokeLinecap = "round"
strokeLinejoin = "round"
d = "M8.25 4.5l7.5 7.5-7.5 7.5"
/ >
< / svg >
) }
< / div >
< / button >
) ) }
< / button >
) ;
} ) }
< / div >
) }
{ error && workflows . length > 0 && (
< p className = "text-xs text-red-400 mt-2 px-4 " > { error } < / p >
< p className = "text-xs text-red-400 mt-2 px-3" > { error } < / p >
) }
< / div >
{ /* Footer */ }
< div className = "px-6 py-3 border-t border-neutral-700/50 flex-shrink-0 flex items-center gap-3 " >
< div className = "px-6 py-3 border-t border-neutral-700/50 flex-shrink-0 flex items-center gap-4" >
< button
onClick = { handleBrowseOther }
disabled = { loadingWorkflow !== null }
className = "text-xs text-neutral-400 hover:text-neutral-2 00 transition-colors disabled:opacity-50"
className = "flex items-center gap-1.5 text-xs text-neutral-500 hover:text-neutral-300 transition-colors disabled:opacity-50"
>
Browse other location . . .
< svg className = "w-3.5 h-3.5" fill = "none" viewBox = "0 0 24 24" stroke = "currentColor" strokeWidth = { 2 } >
< path strokeLinecap = "round" strokeLinejoin = "round" d = "M13.5 6H5.25A2.25 2.25 0 003 8.25v10.5A2.25 2.25 0 005.25 21h10.5A2.25 2.25 0 0018 18.75V10.5m-10.5 6L21 3m0 0h-5.25M21 3v5.25" / >
< / svg >
Browse other location
< / button >
< span className = "text-neutral-700" > · < / span >
< button
onClick = { handleChangeFolde r}
onClick = { browseAndSetDi r}
disabled = { loadingWorkflow !== null }
className = "text-xs text-neutral-400 hover:text-neutral-2 00 transition-colors disabled:opacity-50"
className = "flex items-center gap-1.5 text-xs text-neutral-500 hover:text-neutral-3 00 transition-colors disabled:opacity-50"
>
< svg className = "w-3.5 h-3.5" fill = "none" viewBox = "0 0 24 24" stroke = "currentColor" strokeWidth = { 2 } >
< path strokeLinecap = "round" strokeLinejoin = "round" d = "M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0l3.181 3.183a8.25 8.25 0 0013.803-3.7M4.031 9.865a8.25 8.25 0 0113.803-3.7l3.181 3.182" / >
< / svg >
Change folder
< / button >
< / div >