ui : lift download delete/cancel confirmations into the options panel

Move the two confirmation dialogs out of the quant download chip into the
download-options parent, which now owns a single pair keyed by the acted-on
repo+tag. The chip stays presentational and only signals delete/cancel intent.

Assisted-by: llama-ui:Qwen3.8-Flash-Next
This commit is contained in:
Aleksander Grygier
2026-09-04 20:07:38 +02:00
parent caa00695a1
commit 8f750fe451
3 changed files with 65 additions and 40 deletions
@@ -2,6 +2,7 @@
import { classify, labelFor } from './download-options.utils';
import ModelsDiscoverDetailsDownloadOptionsDownloadCommand from './ModelsDiscoverDetailsDownloadOptionsDownloadCommand.svelte';
import ModelsDiscoverDetailsDownloadOptionsRow from './ModelsDiscoverDetailsDownloadOptionsRow.svelte';
import DialogConfirmation from '$lib/components/app/dialogs/DialogConfirmation.svelte';
import { SelectableFileKind } from '$lib/enums';
import { HuggingFaceService, ModelsService } from '$lib/services';
import { modelsStore } from '$lib/stores';
@@ -22,6 +23,29 @@
let { bitDepthRows, getDownloadState, modelId }: Props = $props();
// Destructive chip actions (delete a downloaded model, cancel a download) are
// confirmed here rather than inside each chip: one pair of dialogs owned by
// the options panel, keyed by the repo+tag the user acted on.
let pending: { action: 'cancel' | 'delete'; repoWithTag: string } | null = $state(null);
function requestCancel(repoWithTag: string) {
pending = { action: 'cancel', repoWithTag };
}
function requestDelete(repoWithTag: string) {
pending = { action: 'delete', repoWithTag };
}
function closePending() {
pending = null;
}
function confirmPending() {
if (pending) void modelsStore.status.cancelDownload(pending.repoWithTag);
pending = null;
}
function stateFor(repoWithTag: string, filePath: string, isSidecar: boolean): DownloadEntryState {
if (getDownloadState) return getDownloadState(repoWithTag, filePath, isSidecar);
@@ -117,7 +141,12 @@
lifecycle state; nothing here selects anything. -->
<div class="flex w-full flex-col divide-y divide-border/50 px-4 pb-1 dark:divide-border/35">
{#each rows as row (row.bitDepth)}
<ModelsDiscoverDetailsDownloadOptionsRow bitDepth={row.bitDepth} files={row.files} />
<ModelsDiscoverDetailsDownloadOptionsRow
bitDepth={row.bitDepth}
files={row.files}
onRequestCancel={requestCancel}
onRequestDelete={requestDelete}
/>
{/each}
</div>
@@ -127,3 +156,17 @@
</div>
</section>
{/if}
{#if pending}
<DialogConfirmation
confirmText={pending.action === 'delete' ? 'Delete' : 'Cancel download'}
description={pending.action === 'delete'
? `This permanently removes ${modelsStore.toDisplayName(pending.repoWithTag)} from disk. You can download it again later.`
: `This stops the download of ${modelsStore.toDisplayName(pending.repoWithTag)} and removes the partial files. Pause it instead to keep the progress.`}
onCancel={closePending}
onConfirm={confirmPending}
open
title={pending.action === 'delete' ? 'Delete model' : 'Cancel download'}
variant="destructive"
/>
{/if}
@@ -2,7 +2,6 @@
import DownloadProgressBar from '../../DownloadProgressBar.svelte';
import { labelFor } from './download-options.utils';
import { Check, Download, Loader2, Pause, Play, RotateCw, X } from '@lucide/svelte';
import DialogConfirmation from '$lib/components/app/dialogs/DialogConfirmation.svelte';
import * as Tooltip from '$lib/components/ui/tooltip';
import { HuggingFaceService } from '$lib/services';
import { modelsStore } from '$lib/stores';
@@ -13,13 +12,16 @@
file: HfModelSibling;
/** Download state of the file, from the parent's status feed. */
entry: DownloadEntryState;
/**
* Ask the parent to confirm deleting a downloaded model. The chip owns no
* dialog; the parent renders the single confirmation and acts on confirm.
*/
onRequestDelete?: (repoWithTag: string) => void;
/** Ask the parent to confirm cancelling an in-flight download. */
onRequestCancel?: (repoWithTag: string) => void;
}
let { entry, file }: Props = $props();
// delete / cancel confirmation state
let confirmDeleteOpen = $state(false);
let confirmCancelOpen = $state(false);
let { entry, file, onRequestCancel, onRequestDelete }: Props = $props();
/** Queue the download; a failed attempt leaves partial files, drop them first. */
async function startDownload() {
@@ -62,7 +64,7 @@
aria-label={tooltipText}
class="group relative inline-flex h-auto cursor-pointer items-center gap-1 rounded-md! border px-2 py-1 text-left font-mono text-xs shadow-xs transition-[background-color,border-color,transform] duration-200 ease-[cubic-bezier(0.23,1,0.32,1)] active:scale-[0.97]
border-green-600/25 bg-green-500/5 hover:border-destructive/50 hover:bg-destructive/10 dark:border-green-500/30 dark:bg-green-500/10 dark:hover:border-destructive/50 dark:hover:bg-destructive/15"
onclick={() => (confirmDeleteOpen = true)}
onclick={() => onRequestDelete?.(entry.repoWithTag)}
type="button"
>
{#if meta?.sidecar}
@@ -98,20 +100,6 @@
<p>{tooltipText}</p>
</Tooltip.Content>
</Tooltip.Root>
<DialogConfirmation
confirmText="Delete"
description={`This permanently removes ${modelsStore.toDisplayName(entry.repoWithTag)} from disk. You can download it again later.`}
onCancel={() => (confirmDeleteOpen = false)}
onConfirm={() => {
confirmDeleteOpen = false;
void modelsStore.status.cancelDownload(entry.repoWithTag);
}}
open={confirmDeleteOpen}
title="Delete model"
variant="destructive"
/>
{:else if entry.isDownloading || entry.isPaused}
<!-- in-flight / paused chips: the chip body pauses / resumes on click, the X
inside the chip cancels (stops and discards the partial files). The X slot
@@ -188,7 +176,7 @@
<button
aria-label="Cancel downloading"
class="relative inline-flex h-3.5 w-3.5 shrink-0 cursor-pointer items-center justify-center text-muted-foreground/70"
onclick={() => (confirmCancelOpen = true)}
onclick={() => onRequestCancel?.(entry.repoWithTag)}
type="button"
>
<X
@@ -261,18 +249,3 @@
</Tooltip.Content>
</Tooltip.Root>
{/if}
<DialogConfirmation
cancelText="Keep downloading"
confirmText="Cancel download"
description={`This stops the download of ${modelsStore.toDisplayName(entry.repoWithTag)} and removes the partial files. Pause it instead to keep the progress.`}
onCancel={() => (confirmCancelOpen = false)}
onConfirm={() => {
confirmCancelOpen = false;
void modelsStore.status.cancelDownload(entry.repoWithTag);
}}
open={confirmCancelOpen}
title="Cancel download"
variant="destructive"
/>
@@ -15,9 +15,13 @@
bitDepth: number;
/** Every GGUF of this bit depth, with download state attached. */
files: (SelectableFile & { state: DownloadEntryState })[];
/** Forwarded to each chip: ask the parent to confirm a cancel. */
onRequestCancel?: (repoWithTag: string) => void;
/** Forwarded to each chip: ask the parent to confirm a delete. */
onRequestDelete?: (repoWithTag: string) => void;
}
let { bitDepth, files }: Props = $props();
let { bitDepth, files, onRequestCancel, onRequestDelete }: Props = $props();
let mainFile = $derived(files.find((f) => f.kind === SelectableFileKind.MAIN) ?? null);
let draftFile = $derived(files.find((f) => f.kind === SelectableFileKind.DRAFT) ?? null);
@@ -45,7 +49,12 @@
<div class="flex flex-wrap justify-end gap-1.5">
{#each files as file (file.path)}
<ModelsDiscoverDetailsDownloadOptionsQuantDownloadButton entry={file.state} {file} />
<ModelsDiscoverDetailsDownloadOptionsQuantDownloadButton
entry={file.state}
{file}
{onRequestCancel}
{onRequestDelete}
/>
{/each}
</div>
</div>