diff --git a/app/src/components/ServerSettings/ModelManagement.tsx b/app/src/components/ServerSettings/ModelManagement.tsx
index 71c8b68b..a5fafdbf 100644
--- a/app/src/components/ServerSettings/ModelManagement.tsx
+++ b/app/src/components/ServerSettings/ModelManagement.tsx
@@ -157,16 +157,30 @@ export function ModelManagement() {
});
const handleCancel = (modelName: string) => {
- // Immediately hide the error and suppress downloading state in UI
+ // Snapshot previous state for rollback
+ const prevDismissed = dismissedErrors;
+ const prevLocalErrors = localErrors;
+ const prevDownloadingModel = downloadingModel;
+ const prevDownloadingDisplayName = downloadingDisplayName;
+
+ // Optimistically hide the error and suppress downloading state in UI
setDismissedErrors((prev) => new Set(prev).add(modelName));
setLocalErrors((prev) => { const next = new Map(prev); next.delete(modelName); return next; });
- // Also clear local downloading state if this was our current download
if (downloadingModel === modelName) {
setDownloadingModel(null);
setDownloadingDisplayName(null);
}
- // Fire-and-forget the backend cancel, then refetch to sync
- cancelMutation.mutate(modelName);
+
+ cancelMutation.mutate(modelName, {
+ onError: () => {
+ // Rollback optimistic updates on failure
+ setDismissedErrors(prevDismissed);
+ setLocalErrors(prevLocalErrors);
+ setDownloadingModel(prevDownloadingModel);
+ setDownloadingDisplayName(prevDownloadingDisplayName);
+ toast({ title: 'Cancel failed', description: 'Could not cancel the download task.', variant: 'destructive' });
+ },
+ });
};
const clearAllMutation = useMutation({
@@ -259,7 +273,7 @@ export function ModelManagement() {
}}
onCancel={() => handleCancel(model.model_name)}
isDownloading={downloadingModel === model.model_name}
- isCancelling={cancelMutation.isPending}
+ isCancelling={cancelMutation.isPending && cancelMutation.variables === model.model_name}
isDismissed={dismissedErrors.has(model.model_name)}
erroredDownload={erroredDownloads.get(model.model_name)}
formatSize={formatSize}
@@ -291,7 +305,7 @@ export function ModelManagement() {
}}
onCancel={() => handleCancel(model.model_name)}
isDownloading={downloadingModel === model.model_name}
- isCancelling={cancelMutation.isPending}
+ isCancelling={cancelMutation.isPending && cancelMutation.variables === model.model_name}
isDismissed={dismissedErrors.has(model.model_name)}
erroredDownload={erroredDownloads.get(model.model_name)}
formatSize={formatSize}
@@ -310,9 +324,9 @@ export function ModelManagement() {
className="flex items-center gap-2 hover:text-foreground transition-colors"
>
{consoleOpen ? (
-
- ) : (
+ ) : (
+
)}
Problems
diff --git a/backend/main.py b/backend/main.py
index 0893e626..3f58aa8b 100644
--- a/backend/main.py
+++ b/backend/main.py
@@ -932,7 +932,11 @@ async def transcribe_audio(
# Check if Whisper model is downloaded (uses default size "base")
model_size = whisper_model.model_size
- model_name = f"openai/whisper-{model_size}"
+ # Map model sizes to HF repo IDs (whisper-large needs -v3 suffix)
+ whisper_hf_repos = {
+ "large": "openai/whisper-large-v3",
+ }
+ model_name = whisper_hf_repos.get(model_size, f"openai/whisper-{model_size}")
# Check if model is cached
from huggingface_hub import constants as hf_constants
@@ -1595,11 +1599,15 @@ async def cancel_model_download(request: models.ModelDownloadRequest):
removed = task_manager.cancel_download(request.model_name)
# Also clear progress state so the model doesn't show as downloading
+ progress_removed = False
with progress_manager._lock:
if request.model_name in progress_manager._progress:
del progress_manager._progress[request.model_name]
+ progress_removed = True
- return {"message": f"Download task for {request.model_name} cancelled"}
+ if removed or progress_removed:
+ return {"message": f"Download task for {request.model_name} cancelled"}
+ return {"message": f"No active task found for {request.model_name}"}
@app.post("/tasks/clear")
@@ -1613,8 +1621,8 @@ async def clear_all_tasks():
with progress_manager._lock:
progress_manager._progress.clear()
- progress_manager._last_notify_time.clear()
- progress_manager._last_notify_progress.clear()
+ progress_manager._last_notify_time.clear()
+ progress_manager._last_notify_progress.clear()
return {"message": "All task state cleared"}
@@ -1771,6 +1779,7 @@ async def get_active_tasks():
model_name=model_name,
status=progress.get("status", "downloading"),
started_at=started_at,
+ error=progress.get("error"),
))
# Get active generations
diff --git a/backend/utils/tasks.py b/backend/utils/tasks.py
index 98ead512..da13011a 100644
--- a/backend/utils/tasks.py
+++ b/backend/utils/tasks.py
@@ -74,10 +74,7 @@ class TaskManager:
def cancel_download(self, model_name: str) -> bool:
"""Cancel/dismiss a download task (removes it from active list)."""
- if model_name in self._active_downloads:
- del self._active_downloads[model_name]
- return True
- return False
+ return self._active_downloads.pop(model_name, None) is not None
def is_download_active(self, model_name: str) -> bool:
"""Check if a download is active."""