@@ -3,6 +3,35 @@ import { useEffect, useRef, useState } from "react";
33import { createRoot } from "react-dom/client" ;
44import "./style.css" ;
55
6+ // Type definitions
7+ interface FileInfo {
8+ name : string ;
9+ absolutePath : string ;
10+ relativePath : string ;
11+ type : 'file' | 'directory' | 'symlink' | 'other' ;
12+ size : number ;
13+ modifiedAt : string ;
14+ mode : string ;
15+ permissions : {
16+ readable : boolean ;
17+ writable : boolean ;
18+ executable : boolean ;
19+ } ;
20+ }
21+
22+ interface ListFilesOptions {
23+ recursive ?: boolean ;
24+ includeHidden ?: boolean ;
25+ }
26+
27+ interface ListFilesResponse {
28+ success : boolean ;
29+ path : string ;
30+ files : FileInfo [ ] ;
31+ count : number ;
32+ timestamp : string ;
33+ }
34+
635// Simple API client to replace direct HttpClient usage
736class SandboxApiClient {
837 private baseUrl : string ;
@@ -244,6 +273,13 @@ class SandboxApiClient {
244273 } ) ;
245274 }
246275
276+ async listFiles ( path : string , options : ListFilesOptions = { } ) : Promise < ListFilesResponse > {
277+ return this . doFetch ( "/api/list-files" , {
278+ method : "POST" ,
279+ body : JSON . stringify ( { path, options } ) ,
280+ } ) ;
281+ }
282+
247283 async mkdir ( path : string , options : any = { } ) {
248284 return this . doFetch ( "/api/mkdir" , {
249285 method : "POST" ,
@@ -1385,6 +1421,10 @@ function FilesTab({
13851421 const [ moveSourcePath , setMoveSourcePath ] = useState ( "" ) ;
13861422 const [ moveDestPath , setMoveDestPath ] = useState ( "" ) ;
13871423 const [ deleteFilePath , setDeleteFilePath ] = useState ( "" ) ;
1424+ const [ listPath , setListPath ] = useState ( "/workspace" ) ;
1425+ const [ listRecursive , setListRecursive ] = useState ( false ) ;
1426+ const [ listHidden , setListHidden ] = useState ( false ) ;
1427+ const [ listedFiles , setListedFiles ] = useState < FileInfo [ ] > ( [ ] ) ;
13881428
13891429 // Git Operations
13901430 const [ gitRepoUrl , setGitRepoUrl ] = useState ( "" ) ;
@@ -1468,6 +1508,63 @@ function FilesTab({
14681508 }
14691509 } ;
14701510
1511+ const handleListFiles = async ( ) => {
1512+ if ( ! client || ! listPath . trim ( ) ) return ;
1513+ try {
1514+ const result = await client . listFiles ( listPath , {
1515+ recursive : listRecursive ,
1516+ includeHidden : listHidden ,
1517+ } ) ;
1518+
1519+ // Sort files for proper tree display using relativePath
1520+ const sortedFiles = ( result . files || [ ] ) . sort ( ( a , b ) => {
1521+ // Use relativePath for cleaner sorting
1522+ const aSegments = a . relativePath . split ( '/' ) . filter ( s => s ) ;
1523+ const bSegments = b . relativePath . split ( '/' ) . filter ( s => s ) ;
1524+
1525+ // Compare segment by segment
1526+ const minLength = Math . min ( aSegments . length , bSegments . length ) ;
1527+
1528+ for ( let i = 0 ; i < minLength ; i ++ ) {
1529+ // If we're at the last segment for either path
1530+ const aIsLast = i === aSegments . length - 1 ;
1531+ const bIsLast = i === bSegments . length - 1 ;
1532+
1533+ // If one is a parent of the other
1534+ if ( aIsLast && ! bIsLast ) {
1535+ // a is a parent directory of b (if a is a directory)
1536+ return a . type === 'directory' ? - 1 : 1 ;
1537+ }
1538+ if ( ! aIsLast && bIsLast ) {
1539+ // b is a parent directory of a (if b is a directory)
1540+ return b . type === 'directory' ? 1 : - 1 ;
1541+ }
1542+
1543+ // If both are at the same level (both last or both not last)
1544+ if ( aIsLast && bIsLast ) {
1545+ // Same directory level - directories first, then alphabetical
1546+ if ( a . type === 'directory' && b . type !== 'directory' ) return - 1 ;
1547+ if ( a . type !== 'directory' && b . type === 'directory' ) return 1 ;
1548+ }
1549+
1550+ // Compare the segments alphabetically
1551+ const segmentCompare = aSegments [ i ] . localeCompare ( bSegments [ i ] ) ;
1552+ if ( segmentCompare !== 0 ) return segmentCompare ;
1553+ }
1554+
1555+ // If we get here, one path is a prefix of the other
1556+ // The shorter path (parent) should come first
1557+ return aSegments . length - bSegments . length ;
1558+ } ) ;
1559+
1560+ setListedFiles ( sortedFiles ) ;
1561+ addResult ( "success" , `Listed ${ result . count || 0 } files in: ${ listPath } ` ) ;
1562+ } catch ( error : any ) {
1563+ addResult ( "error" , `Failed to list files: ${ error . message } ` ) ;
1564+ setListedFiles ( [ ] ) ;
1565+ }
1566+ } ;
1567+
14711568 const handleGitCheckout = async ( ) => {
14721569 if ( ! client || ! gitRepoUrl . trim ( ) ) return ;
14731570 try {
@@ -1658,6 +1755,91 @@ function FilesTab({
16581755 </ button >
16591756 </ div >
16601757 </ div >
1758+
1759+ { /* List Files */ }
1760+ < div className = "operation-group" >
1761+ < h3 > List Files</ h3 >
1762+ < div className = "input-group" >
1763+ < input
1764+ type = "text"
1765+ placeholder = "Directory path (e.g., /workspace)"
1766+ value = { listPath }
1767+ onChange = { ( e ) => setListPath ( e . target . value ) }
1768+ className = "file-input"
1769+ />
1770+ < button
1771+ onClick = { handleListFiles }
1772+ disabled = { ! listPath . trim ( ) || connectionStatus !== "connected" }
1773+ className = "action-button"
1774+ >
1775+ List Files
1776+ </ button >
1777+ </ div >
1778+ < div className = "list-options" >
1779+ < label >
1780+ < input
1781+ type = "checkbox"
1782+ checked = { listRecursive }
1783+ onChange = { ( e ) => setListRecursive ( e . target . checked ) }
1784+ />
1785+ Recursive
1786+ </ label >
1787+ < label >
1788+ < input
1789+ type = "checkbox"
1790+ checked = { listHidden }
1791+ onChange = { ( e ) => setListHidden ( e . target . checked ) }
1792+ />
1793+ Include Hidden
1794+ </ label >
1795+ </ div >
1796+ { listedFiles . length > 0 && (
1797+ < div className = "file-list-results" >
1798+ < h4 > Files ({ listedFiles . length } ):</ h4 >
1799+ < div className = "file-list" >
1800+ { listedFiles . map ( ( file , index ) => {
1801+ // Calculate indentation level using the relativePath field
1802+ const depth = listRecursive ? ( file . relativePath . split ( '/' ) . filter ( s => s ) . length - 1 ) : 0 ;
1803+
1804+ // For directories, add a trailing slash for clarity
1805+ const displayName = file . type === 'directory' ? `${ file . name } /` : file . name ;
1806+
1807+ // Add tree-like prefix for better hierarchy visualization
1808+ const treePrefix = depth > 0 ? '├── ' : '' ;
1809+
1810+ return (
1811+ < div
1812+ key = { index }
1813+ className = "file-item"
1814+ style = { {
1815+ paddingLeft : `${ depth * 16 + 8 } px` ,
1816+ fontWeight : file . type === 'directory' ? '500' : 'normal'
1817+ } }
1818+ >
1819+ { depth > 0 && < span className = "tree-prefix" > { treePrefix } </ span > }
1820+ < span className = "file-icon" >
1821+ { file . type === 'directory' ? '📁' :
1822+ file . permissions . executable ? '⚙️' : '📄' }
1823+ </ span >
1824+ < span className = "file-mode" > { file . mode } </ span >
1825+ < span className = "file-name" title = { file . absolutePath } >
1826+ { displayName }
1827+ </ span >
1828+ < span className = "file-details" >
1829+ { file . type === 'file' && (
1830+ < span className = "file-size" > { file . size . toLocaleString ( ) } bytes</ span >
1831+ ) }
1832+ < span className = "file-date" >
1833+ { new Date ( file . modifiedAt ) . toLocaleDateString ( ) }
1834+ </ span >
1835+ </ span >
1836+ </ div >
1837+ ) ;
1838+ } ) }
1839+ </ div >
1840+ </ div >
1841+ ) }
1842+ </ div >
16611843 </ div >
16621844 { /* Git Operations */ }
16631845 < div className = "git-section" >
@@ -3047,7 +3229,7 @@ result = x / y`,
30473229}
30483230
30493231function SandboxTester ( ) {
3050- const [ activeTab , setActiveTab ] = useState < TabType > ( "notebook " ) ;
3232+ const [ activeTab , setActiveTab ] = useState < TabType > ( "commands " ) ;
30513233 const [ client , setClient ] = useState < SandboxApiClient | null > ( null ) ;
30523234 const [ connectionStatus , setConnectionStatus ] = useState <
30533235 "disconnected" | "connecting" | "connected"
0 commit comments