Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions api/routers/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,17 @@ async def switch_model(model_id: str):
raise HTTPException(400, str(e))


@router.post("/unload/{model_id}")
async def unload_model(model_id: str):
"""Unloads a model from memory so its files can be safely deleted."""
try:
gen = generator_registry.get_generator(model_id)
gen.unload()
return {"unloaded": True}
except ValueError:
return {"unloaded": True} # already not loaded, that's fine


@router.get("/hf-download")
async def hf_download(repo_id: str, model_id: str):
"""
Expand Down
13 changes: 9 additions & 4 deletions electron/main/ipc-handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,13 +96,18 @@ export function setupIpcHandlers(pythonBridge: PythonBridge, getWindow: WindowGe
return result.canceled ? null : result.filePath
})

ipcMain.handle('model:delete', async (_, modelId: string): Promise<boolean> => {
ipcMain.handle('model:delete', async (_, modelId: string): Promise<{ success: boolean; error?: string }> => {
const modelDir = join(app.getPath('userData'), 'models', modelId)
try {
await rmAsync(modelDir, { recursive: true, force: true })
return true
await axios.post(`${API_BASE_URL}/model/unload/${encodeURIComponent(modelId)}`, {}, { timeout: 5000 })
} catch {
return false
// unload is best-effort — proceed with deletion anyway
}
try {
await rmAsync(modelDir, { recursive: true, force: true })
return { success: true }
} catch (err) {
return { success: false, error: String(err) }
}
})

Expand Down
12 changes: 9 additions & 3 deletions src/areas/models/ModelsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export default function ModelsPage(): JSX.Element {
const [models, setModels] = useState<LocalModel[]>([])
const [downloading, setDownloading] = useState<Record<string, number>>({})
const [deleteTarget, setDeleteTarget] = useState<LocalModel | null>(null)
const [deleteError, setDeleteError] = useState<string | null>(null)
const [uninstallTarget, setUninstallTarget] = useState<string | null>(null)

// GitHub extension install form
Expand Down Expand Up @@ -64,8 +65,13 @@ export default function ModelsPage(): JSX.Element {
}, [installError])

async function handleDelete(model: LocalModel) {
await window.electron.model.delete(model.id)
const result = await window.electron.model.delete(model.id)
if (!result.success) {
setDeleteError('Failed to delete the model. Try restarting the app and deleting again.')
return
}
setDeleteTarget(null)
setDeleteError(null)
refresh()
}

Expand Down Expand Up @@ -331,12 +337,12 @@ export default function ModelsPage(): JSX.Element {
{deleteTarget && (
<ConfirmModal
title={`Uninstall ${formatModelName(deleteTarget.id)}?`}
description="This will permanently delete the model weights from your disk. You can re-download it anytime."
description={deleteError ?? "This will permanently delete the model weights from your disk. You can re-download it anytime."}
confirmLabel="Uninstall"
cancelLabel="Keep"
variant="danger"
onConfirm={() => handleDelete(deleteTarget)}
onCancel={() => setDeleteTarget(null)}
onCancel={() => { setDeleteTarget(null); setDeleteError(null) }}
/>
)}

Expand Down
2 changes: 1 addition & 1 deletion src/shared/types/electron.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ declare global {
listDownloaded: () => Promise<{ id: string; name: string; size_gb: number }[]>
isDownloaded: (modelId: string) => Promise<boolean>
download: (repoId: string, modelId: string) => Promise<{ success: boolean; error?: string }>
delete: (modelId: string) => Promise<boolean>
delete: (modelId: string) => Promise<{ success: boolean; error?: string }>
onProgress: (cb: (data: { modelId: string; percent: number }) => void) => void
offProgress: () => void
}
Expand Down