Skip to content

Commit 4e95e95

Browse files
davindicodeclaude
andcommitted
Add per-file cancel button for upload jobs
- Cancel button (X) shown next to pending and in-progress uploads - Aborts the XHR for active uploads, skips pending ones - Cancelled files shown in yellow with "cancelled" status - Cancellation state tracked via refs to survive across async iterations Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 0266ead commit 4e95e95

1 file changed

Lines changed: 41 additions & 8 deletions

File tree

app/components/files/FilesPage.tsx

Lines changed: 41 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)