diff --git a/tools/ui/src/lib/components/app/models/discover/ModelsDiscoverDetails/ModelsDiscoverDetailsDownloadOptions/download-options.utils.ts b/tools/ui/src/lib/components/app/models/discover/ModelsDiscoverDetails/ModelsDiscoverDetailsDownloadOptions/download-options.utils.ts
new file mode 100644
index 0000000000..4dd8accb42
--- /dev/null
+++ b/tools/ui/src/lib/components/app/models/discover/ModelsDiscoverDetails/ModelsDiscoverDetailsDownloadOptions/download-options.utils.ts
@@ -0,0 +1,28 @@
+import { DRAFT_FILE_LABEL, MODEL_ID, PATH_SEPARATOR } from '$lib/constants';
+import { ModelSelectableFileKind } from '$lib/enums';
+import { HuggingFaceService } from '$lib/services';
+import { isAuxSidecar, isDraftSidecar } from '$lib/utils';
+
+/** Kind of a file path: the main weights, a draft sidecar, or an aux sidecar (mmproj). */
+export function classify(path: string): ModelSelectableFileKind {
+ const sidecar = HuggingFaceService.extractQuantMeta(path)?.sidecar;
+
+ if (!sidecar) return ModelSelectableFileKind.MAIN;
+
+ return isAuxSidecar(sidecar) ? ModelSelectableFileKind.AUX : ModelSelectableFileKind.DRAFT;
+}
+
+/** Display label of a file: its quant, else the file name without the extension. */
+export function labelFor(path: string): string {
+ const meta = HuggingFaceService.extractQuantMeta(path);
+
+ if (meta?.quant) return meta.quant;
+
+ // Quantless sidecar files: the chip badge already carries the sidecar type,
+ // so the label only marks draft files; aux sidecars badge alone.
+ if (meta?.sidecar) return isDraftSidecar(meta.sidecar) ? DRAFT_FILE_LABEL : '';
+
+ const basename = path.split(PATH_SEPARATOR).pop() ?? path;
+
+ return basename.replace(MODEL_ID.WEIGHT_EXTENSION_REGEX, '');
+}
diff --git a/tools/ui/src/lib/components/app/models/discover/ModelsDiscoverDetails/ModelsDiscoverDetailsDownloadOptions/index.ts b/tools/ui/src/lib/components/app/models/discover/ModelsDiscoverDetails/ModelsDiscoverDetailsDownloadOptions/index.ts
new file mode 100644
index 0000000000..b5c0ebef67
--- /dev/null
+++ b/tools/ui/src/lib/components/app/models/discover/ModelsDiscoverDetails/ModelsDiscoverDetailsDownloadOptions/index.ts
@@ -0,0 +1,40 @@
+/**
+ *
+ * MODELS DISCOVER - DETAILS - DOWNLOAD OPTIONS
+ *
+ * The download area of the detail pane: GGUF files grouped by bit depth, each an
+ * independent download action chip, plus the standalone terminal command preview.
+ *
+ */
+
+/**
+ * **ModelsDiscoverDetailsDownloadOptions** - GGUF download options
+ *
+ * Groups GGUF files by bit depth and renders one independent download
+ * action chip per file, plus the standalone terminal command preview.
+ */
+export { default as ModelsDiscoverDetailsDownloadOptions } from './ModelsDiscoverDetailsDownloadOptions.svelte';
+
+/**
+ * **ModelsDiscoverDetailsDownloadOptionsRow** - One bit-depth group of quants
+ *
+ * A single bit-depth row inside ModelsDiscoverDetailsDownloadOptions: the
+ * depth label with its memory hint and the quant chips of that depth.
+ */
+export { default as ModelsDiscoverDetailsDownloadOptionsRow } from './ModelsDiscoverDetailsDownloadOptionsRow.svelte';
+
+/**
+ * **ModelsDiscoverDetailsDownloadOptionsQuantDownloadButton** - One quant chip
+ *
+ * A single GGUF file as an independent action chip: download / retry when
+ * idle, pause / resume / cancel while in flight, delete when downloaded.
+ */
+export { default as ModelsDiscoverDetailsDownloadOptionsQuantDownloadButton } from './ModelsDiscoverDetailsDownloadOptionsQuantDownloadButton.svelte';
+
+/**
+ * **ModelsDiscoverDetailsDownloadOptionsDownloadCommand** - Terminal command
+ *
+ * The `llama serve -hf ...` command box with inline quant selects and a copy
+ * button; owns its picks, nothing two-way binds them to the quant chips.
+ */
+export { default as ModelsDiscoverDetailsDownloadOptionsDownloadCommand } from './ModelsDiscoverDetailsDownloadOptionsDownloadCommand.svelte';
diff --git a/tools/ui/src/lib/components/app/models/discover/ModelsDiscoverDetails/ModelsDiscoverDetailsHeader.svelte b/tools/ui/src/lib/components/app/models/discover/ModelsDiscoverDetails/ModelsDiscoverDetailsHeader.svelte
new file mode 100644
index 0000000000..a216283436
--- /dev/null
+++ b/tools/ui/src/lib/components/app/models/discover/ModelsDiscoverDetails/ModelsDiscoverDetailsHeader.svelte
@@ -0,0 +1,94 @@
+
+
+
+
diff --git a/tools/ui/src/lib/components/app/models/discover/ModelsDiscoverList/index.ts b/tools/ui/src/lib/components/app/models/discover/ModelsDiscoverList/index.ts
new file mode 100644
index 0000000000..d888343a15
--- /dev/null
+++ b/tools/ui/src/lib/components/app/models/discover/ModelsDiscoverList/index.ts
@@ -0,0 +1,38 @@
+/**
+ *
+ * MODELS DISCOVER - LIST
+ *
+ * The discover sidebar column: a debounced search field above a navigable list of
+ * HuggingFace GGUF models, with skeleton rows while loading.
+ *
+ */
+
+/**
+ * **ModelsDiscoverList** - Sidebar model list
+ *
+ * Renders the discover model list as a navigable column. Each row links to the
+ * model's detail and highlights the active one.
+ */
+export { default as ModelsDiscoverList } from './ModelsDiscoverList.svelte';
+
+/**
+ * **ModelsDiscoverListSearch** - Sidebar search input
+ *
+ * Debounced search field for the model list.
+ */
+export { default as ModelsDiscoverListSearch } from './ModelsDiscoverListSearch.svelte';
+
+/**
+ * **ModelsDiscoverListItem** - Single sidebar row
+ *
+ * One model entry in the discover sidebar list, selectable via `onSelect`.
+ */
+export { default as ModelsDiscoverListItem } from './ModelsDiscoverListItem.svelte';
+
+/**
+ * **ModelsDiscoverListItemSkeleton** - Skeleton sidebar row
+ *
+ * Pulsing placeholder matching a ModelsDiscoverListItem row, shown while the
+ * list is loading.
+ */
+export { default as ModelsDiscoverListItemSkeleton } from './ModelsDiscoverListItemSkeleton.svelte';
diff --git a/tools/ui/src/lib/components/app/models/discover/index.ts b/tools/ui/src/lib/components/app/models/discover/index.ts
new file mode 100644
index 0000000000..5926b75153
--- /dev/null
+++ b/tools/ui/src/lib/components/app/models/discover/index.ts
@@ -0,0 +1,39 @@
+/**
+ *
+ * MODELS DISCOVER
+ *
+ * Components for the Models Discover view: a sidebar search + list of
+ * HuggingFace GGUF models and a detail view for the selected model, used as the
+ * body of the discovery dialog. The list and detail trees live in their own
+ * subfolders; this barrel re-exports them alongside the shared leaves.
+ *
+ */
+
+/**
+ * **ModelsDiscover** - Models discover explorer
+ *
+ * The complete discovery layout: a sidebar search + model list on the left and a
+ * detail view for the selected model on the right. Used as the body of the
+ * discovery dialog.
+ */
+export { default as ModelsDiscover } from './ModelsDiscover.svelte';
+
+/**
+ * **ModelsDiscoverAvatar** - Org avatar for a model row
+ *
+ * Shows the org's avatar image, falling back to a monogram on a stable hue
+ * derived from the org name when the image fails to load. Shared by the list,
+ * the detail header and the model selector rows.
+ */
+export { default as ModelsDiscoverAvatar } from './ModelsDiscoverAvatar.svelte';
+
+/**
+ * **ModelsDiscoverDownloadProgressBar** - Thin download progress bar
+ *
+ * Normalizes bytes to a 0..100% bar; can pin to the bottom edge as an overlay.
+ * Shared by the quant chips and the model selector's download rows.
+ */
+export { default as ModelsDiscoverDownloadProgressBar } from './ModelsDiscoverDownloadProgressBar.svelte';
+
+export * from './ModelsDiscoverList';
+export * from './ModelsDiscoverDetails';
diff --git a/tools/ui/src/lib/components/ui/select/select-content.svelte b/tools/ui/src/lib/components/ui/select/select-content.svelte
index f8792f8a9d..a8263819a1 100644
--- a/tools/ui/src/lib/components/ui/select/select-content.svelte
+++ b/tools/ui/src/lib/components/ui/select/select-content.svelte
@@ -88,7 +88,7 @@
{@render children?.()}
diff --git a/tools/ui/src/lib/components/ui/select/select-trigger.svelte b/tools/ui/src/lib/components/ui/select/select-trigger.svelte
index 9f642819ec..de3abcdf21 100644
--- a/tools/ui/src/lib/components/ui/select/select-trigger.svelte
+++ b/tools/ui/src/lib/components/ui/select/select-trigger.svelte
@@ -11,20 +11,26 @@
variant = 'default',
...restProps
}: WithoutChild & {
- size?: 'sm' | 'default';
+ size?: 'xs' | 'sm' | 'default';
variant?: 'default' | 'plain';
} = $props();
+ // Super small trigger: fits its selected value, for dense inline use.
+ const xsClasses =
+ "group flex h-6 w-fit items-center justify-between gap-1 rounded-md border border-border/30 bg-muted/60 px-2 py-0 text-xs whitespace-nowrap outline-none select-none transition-colors hover:bg-muted/80 focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 data-[placeholder]:text-muted-foreground dark:border-border/20 dark:bg-muted/75 dark:hover:bg-muted [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='text-'])]:text-muted-foreground";
+
const baseClasses = $derived(
variant === 'plain'
? "group inline-flex w-full items-center justify-end gap-2 whitespace-nowrap px-0 py-0 text-sm font-medium text-muted-foreground transition-colors focus-visible:outline-none focus-visible:ring-0 focus-visible:ring-offset-0 disabled:cursor-not-allowed disabled:opacity-50 data-[placeholder]:text-muted-foreground data-[size=default]:h-9 data-[size=sm]:h-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-3 [&_svg:not([class*='text-'])]:text-muted-foreground"
- : "flex w-fit items-center justify-between gap-2 rounded-md border border-input bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none select-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 data-[placeholder]:text-muted-foreground data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 dark:bg-input/30 dark:hover:bg-input/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground"
+ : size === 'xs'
+ ? xsClasses
+ : "flex w-fit items-center justify-between gap-2 rounded-md border border-border/30 bg-muted/60 px-3 py-2 text-sm whitespace-nowrap shadow-sm transition-colors outline-none select-none hover:bg-muted/80 focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 data-[placeholder]:text-muted-foreground data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 dark:border-border/20 dark:bg-muted/75 dark:hover:bg-muted dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-3.5 [&_svg:not([class*='text-'])]:text-muted-foreground"
);
const chevronClasses = $derived(
- variant === 'plain'
+ variant === 'plain' || size === 'xs'
? 'size-3 opacity-60 transition-transform group-data-[state=open]:-rotate-180'
- : 'size-4 opacity-50'
+ : 'size-3.5 opacity-60 transition-transform group-data-[state=open]:-rotate-180'
);
diff --git a/tools/ui/src/lib/stores/models-discover/index.svelte.ts b/tools/ui/src/lib/stores/models-discover/index.svelte.ts
index 91a5cb6783..28cc66f00d 100644
--- a/tools/ui/src/lib/stores/models-discover/index.svelte.ts
+++ b/tools/ui/src/lib/stores/models-discover/index.svelte.ts
@@ -2,7 +2,7 @@
* modelsDiscoverStore - Models Discover browse state
*
* Owns the HuggingFace GGUF model list shown in the discover sidebar
- * (DialogModelsDiscover). By default the list is the curated catalog set;
+ * (the Discover tab of DialogManageModels). By default the list is the curated catalog set;
* search replaces it with matches across all of HuggingFace. Both paths fetch
* the same fields, so a row renders the same badges and sizes either way.
*/
@@ -80,6 +80,7 @@ class ModelsDiscoverStore {
this.catalog = catalog;
const builds = this.catalogBuilds();
+ const failedBuilds: HfCatalogBuild[] = [];
this.defaultModels = [];
@@ -98,7 +99,11 @@ class ModelsDiscoverStore {
);
for (const { build, info, tree } of batch) {
- if (!info) continue;
+ if (!info) {
+ failedBuilds.push(build);
+
+ continue;
+ }
this.catalogSizeRanges.set(build.repo, this.sizeRangeFor(build, tree));
@@ -112,7 +117,9 @@ class ModelsDiscoverStore {
}
}
- this.fetched = true;
+ // stay incomplete when some repos failed, so the next mount retries
+ // them; succeeded repos are served from the service cache for free
+ this.fetched = failedBuilds.length === 0;
} catch (err) {
this.error = err instanceof Error ? err.message : 'Failed to fetch models';
} finally {