0.37.5: overwrite prompt for file upload + auto-reveal uploaded hidden files

The reported "upload doesn't work, file never appears" was two things, both
hit when uploading config files like .env:

1. Single-file upload used overwrite=false and dead-ended on "Already
   exists: .env — rename or remove the existing file first." with no way to
   replace the file. Now a conflict opens an Overwrite confirmation dialog
   (mirroring the copy/paste conflict flow) that retries with overwrite=true.
2. .env (and any dotfile) is hidden, so even a successful upload stayed
   invisible unless "Show hidden" was on. After an upload whose name/path
   has a dot-segment, "Show hidden" is now auto-enabled so the file shows.

The single-file upload mutation now takes {file, overwrite}; folder upload
(already overwrite=true) also auto-reveals hidden results.

Frontend-only; all 3 images rebuilt+pushed 0.37.5.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
menzelj
2026-06-21 21:20:15 +00:00
co-authored by Claude Opus 4.8
parent 98d756faf6
commit a40dd0de3e
3 changed files with 35 additions and 8 deletions
+1 -1
View File
@@ -1,3 +1,3 @@
"""Single source of truth for the StackPilot release version."""
APP_VERSION = "0.37.4"
APP_VERSION = "0.37.5"
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "stackpilot-frontend",
"private": true,
"version": "0.37.4",
"version": "0.37.5",
"type": "module",
"scripts": {
"dev": "vite",
+33 -6
View File
@@ -82,6 +82,7 @@ export function Files() {
const [deleting, setDeleting] = useState<HostPathEntry | null>(null);
const [newKind, setNewKind] = useState<"dir" | "file" | null>(null);
const [clip, setClip] = useState<Clipboard | null>(null);
const [uploadConflict, setUploadConflict] = useState<File | null>(null);
const [pasteConflict, setPasteConflict] = useState(false);
const [progress, setProgress] = useState<{
label: string;
@@ -131,10 +132,18 @@ export function Files() {
const refresh = () => qc.invalidateQueries({ queryKey: ["files"] });
// An uploaded dotfile (e.g. .env) is hidden by default — auto-reveal it so
// the user can actually see what they just uploaded.
const revealIfHidden = (...names: string[]) => {
if (!showHidden && names.some((n) => n.split("/").some((seg) => seg.startsWith(".")))) {
setShowHidden(true);
}
};
const upload = useMutation({
mutationFn: (file: File) => {
mutationFn: ({ file, overwrite }: { file: File; overwrite: boolean }) => {
setProgress({ label: file.name, pct: 0, kind: "upload" });
return filesApi.upload(path, file, false, "", host, (pct) =>
return filesApi.upload(path, file, overwrite, "", host, (pct) =>
setProgress({
label: file.name,
pct,
@@ -145,12 +154,15 @@ export function Files() {
},
onSuccess: (r) => {
toast.success(`Uploaded ${r.name}`);
setUploadConflict(null);
revealIfHidden(r.name);
refresh();
},
onError: (e: unknown) => {
onError: (e: unknown, vars) => {
const msg = apiErrorMessage(e);
// Offer to overwrite instead of dead-ending on "Already exists".
if (msg.startsWith("Already exists")) {
toast.error(`${msg} — rename or remove the existing file first.`);
setUploadConflict(vars.file);
} else {
toast.error(msg);
}
@@ -201,9 +213,12 @@ export function Files() {
}
return { ok, failed };
},
onSuccess: ({ ok, failed }) => {
onSuccess: ({ ok, failed }, files) => {
if (failed) toast.warning(`Uploaded ${ok} file(s), ${failed} failed`);
else toast.success(`Uploaded ${ok} file(s)`);
revealIfHidden(
...files.map((f) => (f as File & { webkitRelativePath?: string }).webkitRelativePath || f.name),
);
refresh();
},
onError: (e) => toast.error(apiErrorMessage(e)),
@@ -335,7 +350,7 @@ export function Files() {
className="hidden"
onChange={(e) => {
const f = e.target.files?.[0];
if (f) upload.mutate(f);
if (f) upload.mutate({ file: f, overwrite: false });
}}
/>
{/* webkitdirectory is set imperatively via the callback ref. */}
@@ -610,6 +625,18 @@ export function Files() {
onCancel={() => setPasteConflict(false)}
/>
)}
{uploadConflict && (
<ConfirmDialog
title={`${uploadConflict.name}” already exists here`}
message={`Overwrite the existing file at ${path}?`}
confirmLabel="Overwrite"
danger
busy={upload.isPending}
onConfirm={() => upload.mutate({ file: uploadConflict, overwrite: true })}
onCancel={() => setUploadConflict(null)}
/>
)}
</div>
);
}