@@ -3,6 +3,35 @@ import { useEffect, useRef, useState } from "react";
3
3
import { createRoot } from "react-dom/client" ;
4
4
import "./style.css" ;
5
5
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
+
6
35
// Simple API client to replace direct HttpClient usage
7
36
class SandboxApiClient {
8
37
private baseUrl : string ;
@@ -244,6 +273,13 @@ class SandboxApiClient {
244
273
} ) ;
245
274
}
246
275
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
+
247
283
async mkdir ( path : string , options : any = { } ) {
248
284
return this . doFetch ( "/api/mkdir" , {
249
285
method : "POST" ,
@@ -1385,6 +1421,10 @@ function FilesTab({
1385
1421
const [ moveSourcePath , setMoveSourcePath ] = useState ( "" ) ;
1386
1422
const [ moveDestPath , setMoveDestPath ] = useState ( "" ) ;
1387
1423
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 [ ] > ( [ ] ) ;
1388
1428
1389
1429
// Git Operations
1390
1430
const [ gitRepoUrl , setGitRepoUrl ] = useState ( "" ) ;
@@ -1468,6 +1508,63 @@ function FilesTab({
1468
1508
}
1469
1509
} ;
1470
1510
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
+
1471
1568
const handleGitCheckout = async ( ) => {
1472
1569
if ( ! client || ! gitRepoUrl . trim ( ) ) return ;
1473
1570
try {
@@ -1658,6 +1755,91 @@ function FilesTab({
1658
1755
</ button >
1659
1756
</ div >
1660
1757
</ 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 >
1661
1843
</ div >
1662
1844
{ /* Git Operations */ }
1663
1845
< div className = "git-section" >
@@ -3047,7 +3229,7 @@ result = x / y`,
3047
3229
}
3048
3230
3049
3231
function SandboxTester ( ) {
3050
- const [ activeTab , setActiveTab ] = useState < TabType > ( "notebook " ) ;
3232
+ const [ activeTab , setActiveTab ] = useState < TabType > ( "commands " ) ;
3051
3233
const [ client , setClient ] = useState < SandboxApiClient | null > ( null ) ;
3052
3234
const [ connectionStatus , setConnectionStatus ] = useState <
3053
3235
"disconnected" | "connecting" | "connected"
0 commit comments