@@ -114,11 +114,13 @@ function FileSessionView({ session }: { session: FileSession }) {
114114 interface UploadFile {
115115 name : string ;
116116 size : number ;
117- status : "pending" | "uploading" | "done" | "error" ;
117+ status : "pending" | "uploading" | "done" | "error" | "cancelled" ;
118118 progress : number ; // 0-100
119119 error ?: string ;
120120 }
121121 const [ uploadQueue , setUploadQueue ] = useState < UploadFile [ ] > ( [ ] ) ;
122+ const uploadXhrs = useRef < Map < number , XMLHttpRequest > > ( new Map ( ) ) ;
123+ const cancelledIndices = useRef < Set < number > > ( new Set ( ) ) ;
122124 const uploading = uploadQueue . length > 0 ;
123125
124126 const fullPath = ( name : string ) => ( cwd === "/" ? `/${ name } ` : `${ cwd } /${ name } ` ) ;
@@ -233,20 +235,35 @@ function FileSessionView({ session }: { session: FileSession }) {
233235
234236 const handleInfo = ( entry : FileEntry ) => setDialog ( { type : "info" , entry } ) ;
235237
238+ const cancelUpload = ( index : number ) => {
239+ const xhr = uploadXhrs . current . get ( index ) ;
240+ if ( xhr ) {
241+ xhr . abort ( ) ;
242+ uploadXhrs . current . delete ( index ) ;
243+ }
244+ cancelledIndices . current . add ( index ) ;
245+ setUploadQueue ( ( q ) => q . map ( ( f , i ) => i === index && f . status !== "done" ? { ...f , status : "cancelled" , progress : 0 } : f ) ) ;
246+ } ;
247+
236248 const uploadFile = ( file : File , index : number ) : Promise < void > => {
249+ if ( cancelledIndices . current . has ( index ) ) return Promise . resolve ( ) ;
250+
237251 return new Promise ( ( resolve ) => {
238252 const formData = new FormData ( ) ;
239253 formData . append ( "file" , file ) ;
240254 formData . append ( "dir" , cwd ) ;
241255
242256 const xhr = new XMLHttpRequest ( ) ;
257+ uploadXhrs . current . set ( index , xhr ) ;
258+
243259 xhr . upload . onprogress = ( e ) => {
244260 if ( e . lengthComputable ) {
245261 const pct = Math . round ( ( e . loaded / e . total ) * 100 ) ;
246262 setUploadQueue ( ( q ) => q . map ( ( f , i ) => i === index ? { ...f , progress : pct } : f ) ) ;
247263 }
248264 } ;
249265 xhr . onload = ( ) => {
266+ uploadXhrs . current . delete ( index ) ;
250267 try {
251268 const data = JSON . parse ( xhr . responseText ) ;
252269 if ( data . error ) {
@@ -260,9 +277,14 @@ function FileSessionView({ session }: { session: FileSession }) {
260277 resolve ( ) ;
261278 } ;
262279 xhr . onerror = ( ) => {
280+ uploadXhrs . current . delete ( index ) ;
263281 setUploadQueue ( ( q ) => q . map ( ( f , i ) => i === index ? { ...f , status : "error" , error : "Network error" } : f ) ) ;
264282 resolve ( ) ;
265283 } ;
284+ xhr . onabort = ( ) => {
285+ uploadXhrs . current . delete ( index ) ;
286+ resolve ( ) ;
287+ } ;
266288
267289 setUploadQueue ( ( q ) => q . map ( ( f , i ) => i === index ? { ...f , status : "uploading" } : f ) ) ;
268290 xhr . open ( "POST" , "/api/files/upload" ) ;
@@ -272,6 +294,7 @@ function FileSessionView({ session }: { session: FileSession }) {
272294
273295 const handleUpload = async ( files : FileList | null ) => {
274296 if ( ! files || files . length === 0 ) return ;
297+ cancelledIndices . current . clear ( ) ;
275298 const queue : UploadFile [ ] = Array . from ( files ) . map ( ( f ) => ( {
276299 name : f . name ,
277300 size : f . size ,
@@ -282,11 +305,12 @@ function FileSessionView({ session }: { session: FileSession }) {
282305
283306 const fileArray = Array . from ( files ) ;
284307 for ( let i = 0 ; i < fileArray . length ; i ++ ) {
285- await uploadFile ( fileArray [ i ] , i ) ;
308+ if ( ! cancelledIndices . current . has ( i ) ) {
309+ await uploadFile ( fileArray [ i ] , i ) ;
310+ }
286311 }
287312 await loadDirectory ( cwd ) ;
288- // Keep queue visible briefly so user sees completion
289- setTimeout ( ( ) => setUploadQueue ( [ ] ) , 1500 ) ;
313+ setTimeout ( ( ) => { setUploadQueue ( [ ] ) ; uploadXhrs . current . clear ( ) ; cancelledIndices . current . clear ( ) ; } , 1500 ) ;
290314 if ( uploadInputRef . current ) uploadInputRef . current . value = "" ;
291315 } ;
292316
@@ -392,16 +416,25 @@ function FileSessionView({ session }: { session: FileSession }) {
392416 { uploadQueue . map ( ( file , i ) => (
393417 < div key = { i } className = "flex flex-col gap-1" >
394418 < div className = "flex items-center justify-between text-[11px]" >
395- < span className = { `truncate flex-1 mr-2 ${ file . status === "error" ? "text-red-400" : file . status === "done" ? "text-green-400" : "text-gray-300" } ` } >
419+ < span className = { `truncate flex-1 mr-2 ${ file . status === "error" ? "text-red-400" : file . status === "cancelled" ? "text-yellow-400" : file . status === " done" ? "text-green-400" : "text-gray-300" } ` } >
396420 { file . name }
397421 </ span >
398- < span className = "text-gray-500 shrink-0" >
399- { file . status === "error" ? file . error : file . status === "done" ? "done" : file . status === "uploading" ? `${ file . progress } %` : "waiting" }
422+ < span className = "text-gray-500 shrink-0 flex items-center gap-1.5" >
423+ { file . status === "error" ? file . error : file . status === "cancelled" ? "cancelled" : file . status === "done" ? "done" : file . status === "uploading" ? `${ file . progress } %` : "waiting" }
424+ { ( file . status === "pending" || file . status === "uploading" ) && (
425+ < button
426+ onClick = { ( ) => cancelUpload ( i ) }
427+ className = "text-gray-500 hover:text-red-400 transition-colors"
428+ title = "Cancel upload"
429+ >
430+ < svg className = "w-3 h-3" fill = "none" stroke = "currentColor" viewBox = "0 0 24 24" > < path strokeLinecap = "round" strokeLinejoin = "round" strokeWidth = { 2 } d = "M6 18L18 6M6 6l12 12" /> </ svg >
431+ </ button >
432+ ) }
400433 </ span >
401434 </ div >
402435 < div className = "w-full h-1 bg-gray-800 rounded overflow-hidden" >
403436 < div
404- className = { `h-full transition-all duration-200 rounded ${ file . status === "error" ? "bg-red-500" : file . status === "done" ? "bg-green-500" : "bg-blue-500" } ` }
437+ className = { `h-full transition-all duration-200 rounded ${ file . status === "error" ? "bg-red-500" : file . status === "cancelled" ? "bg-yellow-500" : file . status === " done" ? "bg-green-500" : "bg-blue-500" } ` }
405438 style = { { width : `${ file . progress } %` } }
406439 />
407440 </ div >
0 commit comments