- {#if downloadEntries.length > 0}
-
- {#snippet icon()}
-
- {/snippet}
-
- {#each downloadEntries as entry (entry.repoWithTag)}
-
- {/each}
-
- {/if}
-
{#each sections as group (group.key)}
{#if group.items.length > 0}
{#snippet groupIcon()}
- {#if group.kind === ModelsTableGroupKind.FAVORITES}
+ {#if group.kind === ModelsTableGroupKind.DOWNLOADING}
+
+ {:else if group.kind === ModelsTableGroupKind.FAVORITES}
{:else if group.kind === ModelsTableGroupKind.LOADED}
diff --git a/tools/ui/src/lib/components/app/models/ModelsManager/ModelsManagerStatusCell.svelte b/tools/ui/src/lib/components/app/models/ModelsManager/ModelsManagerStatusCell.svelte
index 9f3d2873a4..92fcc061aa 100644
--- a/tools/ui/src/lib/components/app/models/ModelsManager/ModelsManagerStatusCell.svelte
+++ b/tools/ui/src/lib/components/app/models/ModelsManager/ModelsManagerStatusCell.svelte
@@ -1,15 +1,19 @@
-
+{#if download}
+
+{:else}
+
+{/if}
diff --git a/tools/ui/src/lib/components/app/models/ModelsManager/row-actions.ts b/tools/ui/src/lib/components/app/models/ModelsManager/row-actions.ts
index 48452d0bac..41b96afbd3 100644
--- a/tools/ui/src/lib/components/app/models/ModelsManager/row-actions.ts
+++ b/tools/ui/src/lib/components/app/models/ModelsManager/row-actions.ts
@@ -1,4 +1,5 @@
import { Eye, EyeOff, Heart, HeartOff, Trash2 } from '@lucide/svelte';
+import { MODEL_DOWNLOAD_ICONS, ModelRowDownloadState } from '$lib/constants';
import { modelsStore } from '$lib/stores';
import type { ModelOption } from '$lib/types/models';
@@ -7,8 +8,35 @@ export function modelRowActions(
option: ModelOption,
favorite: boolean,
isHidden: boolean,
- onDelete: (option: ModelOption) => void
+ onDelete: (option: ModelOption) => void,
+ /** Download state, when the row stands for a tracked download. */
+ download?: ModelRowDownloadState | null
) {
+ // a tracked download is paused, resumed or dropped: it is not loaded, hidden or
+ // drafted. Deleting it stops the download and removes what is on disk, which is
+ // what the same wording offers on a cached model
+ if (download) {
+ const isPaused = download === ModelRowDownloadState.PAUSED;
+
+ return [
+ {
+ icon: isPaused ? MODEL_DOWNLOAD_ICONS.resume : MODEL_DOWNLOAD_ICONS.pause,
+ label: isPaused ? 'Resume downloading' : 'Pause downloading',
+ onclick: () =>
+ void (isPaused
+ ? modelsStore.status.downloadModel(option.model)
+ : modelsStore.status.pauseDownload(option.model))
+ },
+ {
+ icon: Trash2,
+ label: 'Delete from disk',
+ onclick: () => onDelete(option),
+ separator: true,
+ variant: 'destructive' as const
+ }
+ ];
+ }
+
return [
{
icon: favorite ? HeartOff : Heart,
diff --git a/tools/ui/src/lib/components/app/models/ModelsManager/utils.ts b/tools/ui/src/lib/components/app/models/ModelsManager/utils.ts
index fd84ef4470..1bb9eb696c 100644
--- a/tools/ui/src/lib/components/app/models/ModelsManager/utils.ts
+++ b/tools/ui/src/lib/components/app/models/ModelsManager/utils.ts
@@ -1,8 +1,8 @@
-import { ModelGroupKind, ModelsTableGroupKind } from '$lib/constants';
+import { LOCAL_BACKEND_ID, ModelGroupKind, ModelsTableGroupKind } from '$lib/constants';
import { ModelCapability, ServerModelStatus } from '$lib/enums';
import { HuggingFaceService, ModelsService } from '$lib/services';
import { modelsStore } from '$lib/stores';
-import type { ModelOption } from '$lib/types/models';
+import type { ModelDownloadProgress, ModelOption } from '$lib/types/models';
import { detectThinkingSupport, detectToolUseSupport } from '$lib/utils';
/** One repo of the table, with the rows it ships as. */
@@ -13,6 +13,45 @@ export interface ModelQuantGroup {
quants: ModelOption[];
}
+/** Byte counts of a tracked download: live while it runs, frozen while paused. */
+export function downloadProgressFor(repoWithTag: string): ModelDownloadProgress | null {
+ return (
+ modelsStore.status.getDownloadProgress(repoWithTag) ??
+ modelsStore.status.getPausedDownloadProgress(repoWithTag)
+ );
+}
+
+/** One tracked download of the status feed. */
+export interface DownloadEntry {
+ isPaused: boolean;
+ progress: ModelDownloadProgress | null;
+ repoWithTag: string;
+}
+
+/**
+ * One table row per tracked download, so the table lists them the way it lists
+ * any other model. A paused download is already in the router's listing, so its
+ * own option is reused; a fresh one stands for its tag alone.
+ */
+export function downloadGroups(entries: DownloadEntry[], models: ModelOption[]): ModelQuantGroup[] {
+ return entries.map((entry) => {
+ const option = models.find((candidate) => candidate.model === entry.repoWithTag) ?? {
+ backendId: LOCAL_BACKEND_ID,
+ capabilities: [],
+ id: entry.repoWithTag,
+ model: entry.repoWithTag,
+ name: entry.repoWithTag
+ };
+
+ return {
+ base: option,
+ key: `download::${entry.repoWithTag}`,
+ kind: ModelGroupKind.QUANTS,
+ quants: [option]
+ };
+ });
+}
+
/** One collapsible block of the manager's table. */
export interface ModelsTableGroup {
/** One entry per repo, its quants hanging off it. */
diff --git a/tools/ui/src/lib/components/app/models/ModelsSelector/ModelsSelectorDownloadItem.svelte b/tools/ui/src/lib/components/app/models/ModelsSelector/ModelsSelectorDownloadItem.svelte
index f5718b0352..760520e05e 100644
--- a/tools/ui/src/lib/components/app/models/ModelsSelector/ModelsSelectorDownloadItem.svelte
+++ b/tools/ui/src/lib/components/app/models/ModelsSelector/ModelsSelectorDownloadItem.svelte
@@ -1,10 +1,10 @@