From cf046648bdf42f5113f5dea9557dc926a1acc33a Mon Sep 17 00:00:00 2001 From: menzelj Date: Mon, 8 Jun 2026 17:26:09 +0000 Subject: [PATCH] File browser: upload progress bar (0.21.5) filesApi.upload now accepts an onProgress callback wired to axios onUploadProgress; the Files page shows a progress bar with percentage while uploading. Single-file upload tracks that file's bytes; folder upload tracks overall progress across the N files (file i/N + current file's bytes). Co-Authored-By: Claude Opus 4.8 --- backend/agent_app.py | 2 +- backend/main.py | 2 +- frontend/package.json | 2 +- frontend/src/api/files.ts | 18 ++++++++++++++-- frontend/src/pages/Files.tsx | 40 +++++++++++++++++++++++++++++++++--- 5 files changed, 56 insertions(+), 8 deletions(-) diff --git a/backend/agent_app.py b/backend/agent_app.py index abf1eea..9a33b9e 100644 --- a/backend/agent_app.py +++ b/backend/agent_app.py @@ -62,7 +62,7 @@ def _map_docker(exc: DockerError): raise HTTPException(status_code=code, detail=exc.detail or exc.error) raise exc # falls through to the global 502 DockerError handler -AGENT_VERSION = "0.21.4" +AGENT_VERSION = "0.21.5" # --------------------------------------------------------------------------- # diff --git a/backend/main.py b/backend/main.py index 3364377..918d322 100644 --- a/backend/main.py +++ b/backend/main.py @@ -55,7 +55,7 @@ async def lifespan(app: FastAPI): schedule_task.cancel() -app = FastAPI(title="StackPilot", version="0.21.4", lifespan=lifespan) +app = FastAPI(title="StackPilot", version="0.21.5", lifespan=lifespan) app.add_middleware( CORSMiddleware, diff --git a/frontend/package.json b/frontend/package.json index f0c4e2f..f1746f1 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,7 +1,7 @@ { "name": "stackpilot-frontend", "private": true, - "version": "0.21.4", + "version": "0.21.5", "type": "module", "scripts": { "dev": "vite", diff --git a/frontend/src/api/files.ts b/frontend/src/api/files.ts index ee17faa..16b4db1 100644 --- a/frontend/src/api/files.ts +++ b/frontend/src/api/files.ts @@ -63,13 +63,27 @@ export const filesApi = { triggerDownload(res.data as Blob, filename); }, - upload: async (path: string, file: File, overwrite = false, relPath = "", agentId?: number) => { + upload: async ( + path: string, + file: File, + overwrite = false, + relPath = "", + agentId?: number, + onProgress?: (pct: number) => void, + ) => { const form = new FormData(); form.append("path", path); form.append("overwrite", String(overwrite)); if (relPath) form.append("rel_path", relPath); form.append("file", file); - const res = await api.post<{ ok: boolean; name: string }>(`${base(agentId)}/upload`, form); + const res = await api.post<{ ok: boolean; name: string }>(`${base(agentId)}/upload`, form, { + onUploadProgress: onProgress + ? (e) => { + const pct = e.total ? (e.loaded / e.total) * 100 : (e.progress ?? 0) * 100; + onProgress(Math.min(pct, 100)); + } + : undefined, + }); return res.data; }, }; diff --git a/frontend/src/pages/Files.tsx b/frontend/src/pages/Files.tsx index 2c56c20..e0e5862 100644 --- a/frontend/src/pages/Files.tsx +++ b/frontend/src/pages/Files.tsx @@ -83,6 +83,7 @@ export function Files() { const [newKind, setNewKind] = useState<"dir" | "file" | null>(null); const [clip, setClip] = useState(null); const [pasteConflict, setPasteConflict] = useState(false); + const [progress, setProgress] = useState<{ label: string; pct: number } | null>(null); const fileInput = useRef(null); const folderInput = useRef(null); @@ -109,7 +110,12 @@ export function Files() { const refresh = () => qc.invalidateQueries({ queryKey: ["files"] }); const upload = useMutation({ - mutationFn: (file: File) => filesApi.upload(path, file, false, "", host), + mutationFn: (file: File) => { + setProgress({ label: file.name, pct: 0 }); + return filesApi.upload(path, file, false, "", host, (pct) => + setProgress({ label: file.name, pct }) + ); + }, onSuccess: (r) => { toast.success(`Uploaded ${r.name}`); refresh(); @@ -123,6 +129,7 @@ export function Files() { } }, onSettled: () => { + setProgress(null); if (fileInput.current) fileInput.current.value = ""; }, }); @@ -131,12 +138,19 @@ export function Files() { // recreates the directory structure. Continues past individual failures. const uploadFolder = useMutation({ mutationFn: async (files: File[]) => { + const total = files.length; let ok = 0; let failed = 0; - for (const f of files) { + for (let i = 0; i < files.length; i++) { + const f = files[i]; const rel = (f as File & { webkitRelativePath?: string }).webkitRelativePath || f.name; try { - await filesApi.upload(path, f, true, rel, host); + await filesApi.upload(path, f, true, rel, host, (filePct) => + setProgress({ + label: `${f.name} (${i + 1}/${total})`, + pct: ((i + filePct / 100) / total) * 100, + }) + ); ok += 1; } catch { failed += 1; @@ -151,6 +165,7 @@ export function Files() { }, onError: (e) => toast.error(apiErrorMessage(e)), onSettled: () => { + setProgress(null); if (folderInput.current) folderInput.current.value = ""; }, }); @@ -279,6 +294,25 @@ export function Files() { + {/* Upload progress */} + {progress && ( +
+
+ + + Uploading {progress.label} + + {Math.round(progress.pct)}% +
+
+
+
+
+ )} + {/* Breadcrumb */}