diff --git a/studio/frontend/src/features/chat/chat-settings-sheet.tsx b/studio/frontend/src/features/chat/chat-settings-sheet.tsx
index a03ebb154c..fcbdecb6e0 100644
--- a/studio/frontend/src/features/chat/chat-settings-sheet.tsx
+++ b/studio/frontend/src/features/chat/chat-settings-sheet.tsx
@@ -27,6 +27,7 @@ import {
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
+import { InfoHint } from "@/components/ui/info-hint";
import {
InputGroup,
InputGroupAddon,
@@ -49,29 +50,27 @@ import {
import { Slider } from "@/components/ui/slider";
import { Switch } from "@/components/ui/switch";
import { Textarea } from "@/components/ui/textarea";
-import { InfoHint } from "@/components/ui/info-hint";
import { Tooltip, TooltipContent } from "@/components/ui/tooltip";
-import { useIsMobile } from "@/hooks/use-mobile";
+import { NumericValueInput, snapToStep } from "@/features/model-picker";
+import { RetrievalSettingsSection } from "@/features/rag";
import { useLlamaUpdateCheck } from "@/hooks/use-llama-update-check";
-import { cn } from "@/lib/utils";
-import {
- Edit03Icon,
- LayoutAlignRightIcon,
-} from "@hugeicons/core-free-icons";
+import { useIsMobile } from "@/hooks/use-mobile";
import { ChevronDownStandardIcon } from "@/lib/chevron-icons";
+import { toast } from "@/lib/toast";
+import { cn } from "@/lib/utils";
+import { Edit03Icon, LayoutAlignRightIcon } from "@hugeicons/core-free-icons";
import { HugeiconsIcon } from "@hugeicons/react";
import { Braces, ChevronDown, ExternalLink } from "lucide-react";
import { Tooltip as TooltipPrimitive } from "radix-ui";
import { Fragment, type ReactNode } from "react";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
-import { toast } from "@/lib/toast";
import { OpenAICodeExecSection } from "./components/openai-code-exec-section";
import {
type ExternalProviderConfig,
getExternalProviderApiKey,
parseExternalModelId,
- supportsProviderPromptCaching,
supportsProviderPromptCacheTtl,
+ supportsProviderPromptCaching,
} from "./external-providers";
import {
BUILTIN_PRESETS,
@@ -92,11 +91,6 @@ import {
providerSupportsFastMode,
} from "./provider-capabilities";
import { useChatRuntimeStore } from "./stores/chat-runtime-store";
-import {
- NumericValueInput,
- snapToStep,
-} from "@/features/model-picker/components/numeric-value-input";
-import { RetrievalSettingsSection } from "@/features/rag/components/retrieval-settings-section";
import type { InferenceParams } from "./types/runtime";
export { defaultInferenceParams, type Preset } from "./presets/preset-policy";
@@ -119,7 +113,7 @@ function getPromptVariablesError(raw: string): string | null {
return null;
}
} catch {
- return "Use valid JSON, for example { \"env\": \"staging\" }.";
+ return 'Use valid JSON, for example { "env": "staging" }.';
}
return "Variables must be a JSON object.";
}
@@ -266,8 +260,7 @@ function CollapsibleSection({
return (
{labelHref ? (
@@ -381,8 +374,7 @@ export function ChatSettingsPanel({
const showPresencePenalty =
!isExternalModel || Boolean(providerCapabilities?.presencePenalty);
const isMobile = useIsMobile();
- const isLoadedGguf =
- useChatRuntimeStore((s) => s.activeGgufVariant) != null;
+ const isLoadedGguf = useChatRuntimeStore((s) => s.activeGgufVariant) != null;
const isGguf = isLoadedGguf;
const currentCheckpoint = params.checkpoint;
const ggufContextLength = useChatRuntimeStore((s) => s.ggufContextLength);
@@ -410,7 +402,9 @@ export function ChatSettingsPanel({
`llama.cpp updated to ${result.tag ?? "the latest build"}.${reloadHint}`,
);
} else {
- toast.error(`llama.cpp update failed: ${result.error ?? "unknown error"}`);
+ toast.error(
+ `llama.cpp update failed: ${result.error ?? "unknown error"}`,
+ );
}
}, [applyLlamaUpdate]);
const loadedEffectiveContext = customContextLength ?? ggufContextLength;
@@ -465,8 +459,7 @@ export function ChatSettingsPanel({
BUILTIN_PRESETS.find((preset) => preset.name === activePreset) ?? null,
[activePreset],
);
- const hasUnsavedPresetChanges = useMemo(
- () => {
+ const hasUnsavedPresetChanges = useMemo(() => {
if (activePresetDefinition == null) {
return false;
}
@@ -474,9 +467,7 @@ export function ChatSettingsPanel({
return activePresetSource === "modified";
}
return !isSamePresetConfig(activePresetDefinition.params, params);
- },
- [activePresetDefinition, activePresetSource, params],
- );
+ }, [activePresetDefinition, activePresetSource, params]);
const presetSaveState = useMemo(
() =>
getPresetSaveState({
@@ -505,8 +496,7 @@ export function ChatSettingsPanel({
const externalSelection = currentCheckpoint
? parseExternalModelId(currentCheckpoint)
: null;
- const maxTokensMax =
- isExternalModel
+ const maxTokensMax = isExternalModel
? getExternalMaxOutputTokens(
externalProviderType,
externalSelection?.modelId,
@@ -596,8 +586,7 @@ export function ChatSettingsPanel({
return;
}
const fallbackPreset =
- BUILTIN_PRESETS.find((preset) => preset.name === "Default") ??
- null;
+ BUILTIN_PRESETS.find((preset) => preset.name === "Default") ?? null;
const next = customPresets.filter((preset) => preset.name !== name);
setCustomPresets(next);
if (activePreset === name) {
@@ -709,7 +698,7 @@ export function ChatSettingsPanel({
Run settings
-
+
onOpenChange?.(false)}
@@ -741,7 +730,7 @@ export function ChatSettingsPanel({
>
{(hasModelContent || modelConfig) && (
-
+
{modelConfig}
{showSpecFallback && (
@@ -775,8 +764,8 @@ export function ChatSettingsPanel({
{showContextVramWarning && (
Context length exceeds the estimated VRAM capacity (
- {ggufMaxContextLength?.toLocaleString()} tokens). The model may
- use system RAM.
+ {ggufMaxContextLength?.toLocaleString()} tokens). The
+ model may use system RAM.
)}
@@ -790,7 +779,7 @@ export function ChatSettingsPanel({
>
-
+
savePresetWithName(presetNameInput)}
disabled={!(settingsHydrated && presetSaveState.canSubmit)}
- variant={presetSaveState.isSaveReady ? "default" : "outline"}
+ variant={
+ presetSaveState.isSaveReady ? "default" : "outline"
+ }
size="sm"
className={cn(
"h-9 w-full rounded-full text-[13px] font-medium tracking-nav",
@@ -915,7 +906,8 @@ export function ChatSettingsPanel({
Prompt caching
- Reuse compatible prompt prefixes for lower latency and cost.
+ Reuse compatible prompt prefixes for lower latency and
+ cost.
Anthropic exposes a 5 minute and a 1 hour ephemeral
- cache pool. The 1 hour pool costs 2x base input on
- write vs 1.25x for 5 minute, but reads stay 0.1x for
- both, so a single read landing more than 5 minutes
- after the write pays off the premium.
+ cache pool. The 1 hour pool costs 2x base input on write
+ vs 1.25x for 5 minute, but reads stay 0.1x for both, so
+ a single read landing more than 5 minutes after the
+ write pays off the premium.
-
+
-
{showTemperature ? (
@@ -1136,7 +1127,9 @@ export function ChatSettingsPanel({
max={2}
step={0.1}
onChange={set("presencePenalty")}
- displayValue={params.presencePenalty === 0 ? "Off" : undefined}
+ displayValue={
+ params.presencePenalty === 0 ? "Off" : undefined
+ }
info="Penalizes any token that has already appeared at least once, encouraging the model to introduce new topics. 0 = off."
/>
) : null}
@@ -1165,7 +1158,7 @@ export function ChatSettingsPanel({
- {!isExternalModel ? (
+ {isExternalModel ? null : (
@@ -1176,13 +1169,13 @@ export function ChatSettingsPanel({
- ) : null}
+ )}
- {!isExternalModel ? (
+ {isExternalModel ? null : (
- ) : null}
+ )}
diff --git a/studio/frontend/src/features/chat/index.ts b/studio/frontend/src/features/chat/index.ts
index 7cd9611c71..9317329e76 100644
--- a/studio/frontend/src/features/chat/index.ts
+++ b/studio/frontend/src/features/chat/index.ts
@@ -3,11 +3,23 @@
export { ChatPage, validateChatSearch, type ChatSearch } from "./chat-page";
export {
+ addScanFolder,
+ browseFolders,
+ deleteCachedModel,
+ deleteFineTunedModel,
+ fetchGgufContextLength,
getInferenceStatus,
listGgufVariants,
listLocalModels,
+ listRecommendedFolders,
+ listScanFolders,
loadModel,
+ removeScanFolder,
+ type BrowseFoldersResponse,
+ type CachedGgufRepo,
+ type CachedModelRepo,
type LocalModelInfo,
+ type ScanFolderInfo,
} from "./api/chat-api";
export type { GgufVariantDetail } from "./types/api";
export {
@@ -17,6 +29,10 @@ export {
type Preset,
} from "./chat-settings-sheet";
export { useChatRuntimeStore } from "./stores/chat-runtime-store";
+export {
+ normalizeSpeculativeType,
+ readPersistedSpeculativeType,
+} from "./stores/chat-runtime-store";
export { useChatSearchStore } from "./stores/chat-search-store";
export { usePinnedChatsStore } from "./stores/pinned-chats-store";
export { useChatPreferencesStore } from "./stores/chat-preferences-store";
@@ -28,9 +44,11 @@ export {
export { useChatModelRuntime } from "./hooks/use-chat-model-runtime";
export {
customProviderDisplayName,
+ isCustomProviderType,
isExternalModelId,
parseExternalModelId,
} from "./external-providers";
+export { ApiProviderLogo } from "./api-provider-logo";
export { useExternalProvidersStore } from "./stores/external-providers-store";
export { ChatSearchDialog } from "./components/chat-search-dialog";
export { setTrainingCompareHandoff } from "./lib/training-compare-handoff";
diff --git a/studio/frontend/src/features/export/components/export-run-panel.tsx b/studio/frontend/src/features/export/components/export-run-panel.tsx
index 5c109deff5..6c2794420b 100644
--- a/studio/frontend/src/features/export/components/export-run-panel.tsx
+++ b/studio/frontend/src/features/export/components/export-run-panel.tsx
@@ -2,7 +2,6 @@
// Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0
import { Button } from "@/components/ui/button";
-import { FolderBrowser } from "@/features/model-picker/components/model-selector/folder-browser";
import { Input } from "@/components/ui/input";
import {
InputGroup,
@@ -17,6 +16,7 @@ import {
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip";
+import { FolderBrowser } from "@/features/model-picker";
import {
AlertCircleIcon,
ArrowRight01Icon,
@@ -28,17 +28,17 @@ import {
import { HugeiconsIcon } from "@hugeicons/react";
import { useEffect, useRef, useState } from "react";
import { useShallow } from "zustand/react/shallow";
+import type { ExportLogEntry } from "../api/export-api";
import {
EXPORT_METHODS,
type ExportMethod,
findMergedFormat,
} from "../constants";
-import type { ExportLogEntry } from "../api/export-api";
import { getExportLogLineClass } from "../lib/log-style";
import {
+ type ExportDestination,
selectExportProgressPercent,
useExportRuntimeStore,
- type ExportDestination,
} from "../stores/export-runtime-store";
function useElapsedSeconds(startedAt: number | null, running: boolean): number {
@@ -165,7 +165,9 @@ export function ExportRunPanel(props: ExportRunPanelProps) {
const isExporting = run.isExporting;
const isTerminal =
- run.phase === "success" || run.phase === "error" || run.phase === "canceled";
+ run.phase === "success" ||
+ run.phase === "error" ||
+ run.phase === "canceled";
const showConfig = run.phase === "idle";
// Gate the log area on the active run's method (from the store) as well as the
// local form selection, so it stays visible after navigating away and back
@@ -197,7 +199,9 @@ export function ExportRunPanel(props: ExportRunPanelProps) {
setFollowTail(nearBottom);
};
- const methodTitle = EXPORT_METHODS.find((m) => m.value === exportMethod)?.title;
+ const methodTitle = EXPORT_METHODS.find(
+ (m) => m.value === exportMethod,
+ )?.title;
const summary = run.summary;
const summaryBaseModel = summary?.baseModelName ?? baseModelName;
const summaryCheckpoint = summary?.checkpointLabel ?? checkpoint;
@@ -290,7 +294,10 @@ export function ExportRunPanel(props: ExportRunPanelProps) {
onClick={() => setFolderBrowserOpen(true)}
aria-label="Browse save folder"
>
-
+
Browse
@@ -301,8 +308,8 @@ export function ExportRunPanel(props: ExportRunPanelProps) {
<>Default: {defaultSaveDirectory}>
) : (
<>
- Paste an absolute path if the folder browser cannot reach the
- drive.
+ Paste an absolute path if the folder browser cannot reach
+ the drive.
>
)}
@@ -410,7 +417,10 @@ export function ExportRunPanel(props: ExportRunPanelProps) {
: [];
const showLabels = items.length > 1;
return items.map((o, i) => (
-
+
{showLabels && o.label ? (
{o.label}
@@ -431,14 +441,22 @@ export function ExportRunPanel(props: ExportRunPanelProps) {
{run.phase === "canceled" && (
-
- Export canceled. Training and inference were not affected.
+
+
+ Export canceled. Training and inference were not affected.
+
)}
{run.phase === "error" && run.error && (
-
+
{run.error}
)}
@@ -447,15 +465,21 @@ export function ExportRunPanel(props: ExportRunPanelProps) {
Base Model
- {summaryBaseModel}
+
+ {summaryBaseModel}
+
{isAdapter ? "Checkpoint" : "Model"}
- {summaryCheckpoint}
+
+ {summaryCheckpoint}
+
Export Method
- {summaryMethodLabel}
+
+ {summaryMethodLabel}
+
{summaryMethod === "merged" && summaryFormats.length > 0 && (
@@ -484,7 +508,12 @@ export function ExportRunPanel(props: ExportRunPanelProps) {
{summaryMethod === "gguf" && run.quantTotal > 1 && (
- Quant {Math.min(run.quantIndex + (isExporting ? 1 : 0), run.quantTotal)} of {run.quantTotal}
+ Quant{" "}
+ {Math.min(
+ run.quantIndex + (isExporting ? 1 : 0),
+ run.quantTotal,
+ )}{" "}
+ of {run.quantTotal}
)}
@@ -506,7 +535,10 @@ export function ExportRunPanel(props: ExportRunPanelProps) {
}
/>
{run.stage && (
-
+
{run.stage}
)}
@@ -556,10 +588,7 @@ export function ExportRunPanel(props: ExportRunPanelProps) {
) : (
{run.logLines.map((entry, idx) => (
-
+
{formatLogLine(entry)}
))}
diff --git a/studio/frontend/src/features/hub/catalog/models-catalog-rows.tsx b/studio/frontend/src/features/hub/catalog/models-catalog-rows.tsx
index a1db1baada..f6d696ab16 100644
--- a/studio/frontend/src/features/hub/catalog/models-catalog-rows.tsx
+++ b/studio/frontend/src/features/hub/catalog/models-catalog-rows.tsx
@@ -1,7 +1,6 @@
// SPDX-License-Identifier: AGPL-3.0-only
// Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0
-import { ModelDeleteAction } from "@/features/model-picker/components/model-selector/model-delete-action";
import {
Tooltip,
TooltipContent,
@@ -9,18 +8,22 @@ import {
} from "@/components/ui/tooltip";
import {
type GgufVariantDetail,
- deleteCachedModel,
deleteCachedDataset,
+ deleteCachedModel,
formatLocalUpdated,
listGgufVariants,
useGgufVariantsCacheVersion,
-} from "@/features/hub/inventory";
-import { classifyUnslothSupport } from "@/features/hub/hooks/use-hub-model-search";
-import { formatBytes, formatRelativeShort } from "@/features/hub/lib/format";
-import { ggufVariantDisplayLabel } from "@/features/hub/lib/gguf-variant-sort";
-import { modelIdsMatch } from "@/features/hub/lib/model-identity";
+} from "@/features/hub";
+import {
+ classifyUnslothSupport,
+ formatBytes,
+ formatRelativeShort,
+ ggufVariantDisplayLabel,
+ modelIdsMatch,
+ useHfTokenStore,
+} from "@/features/hub";
+import { ModelDeleteAction } from "@/features/model-picker";
import { cn, formatCompact } from "@/lib/utils";
-import { useHfTokenStore } from "@/features/hub/stores/hf-token-store";
import {
Download01Icon,
FavouriteIcon,
@@ -39,6 +42,7 @@ import {
useRef,
useState,
} from "react";
+import { paramLabelFromId } from "../lib/view-models";
import type {
CachedInventoryRow,
DiscoverRow,
@@ -46,7 +50,6 @@ import type {
} from "../types";
import { OwnerAvatar } from "./owner-avatar";
import { AccessGlyphs } from "./shared";
-import { paramLabelFromId } from "../lib/view-models";
const COARSE_POINTER =
typeof window !== "undefined" &&
@@ -142,15 +145,15 @@ function CachedSizeChipLive({
);
const rows: Array<{ label: string; size_bytes: number }> | null =
- !needsVariantFetch
- ? [{ label: repoId, size_bytes: totalBytes }]
- : currentVariantState.status === "loaded" &&
+ needsVariantFetch
+ ? currentVariantState.status === "loaded" &&
currentVariantState.variants.length > 0
? currentVariantState.variants.map((variant) => ({
label: ggufVariantDisplayLabel(variant),
size_bytes: variant.size_bytes,
}))
- : null;
+ : null
+ : [{ label: repoId, size_bytes: totalBytes }];
const variantMessage =
currentVariantState.status === "loading"
? "Loading downloaded variants..."
@@ -275,7 +278,9 @@ function CatalogRow({
)}
/>
-
+
{children}
@@ -653,7 +658,9 @@ export const InventoryRow = memo(function InventoryRow({
{/* Format already shows as the status dot, so the pill stays neutral. */}
{formatLabel &&
{formatLabel} }
- {paramLabel &&
{paramLabel} }
+ {paramLabel && (
+
{paramLabel}
+ )}
{quantLabel && (
{quantLabel}
@@ -697,9 +704,7 @@ export const InventoryRow = memo(function InventoryRow({
const compactMarkers =
partialRepoId || unsupported ? (
- {partialRepoId && (
-
- )}
+ {partialRepoId && }
{unsupported && (
)}
diff --git a/studio/frontend/src/features/hub/catalog/on-device-folders-dialog.tsx b/studio/frontend/src/features/hub/catalog/on-device-folders-dialog.tsx
index e1057e6187..86108bbd80 100644
--- a/studio/frontend/src/features/hub/catalog/on-device-folders-dialog.tsx
+++ b/studio/frontend/src/features/hub/catalog/on-device-folders-dialog.tsx
@@ -1,7 +1,6 @@
// SPDX-License-Identifier: AGPL-3.0-only
// Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0
-import { FolderBrowser } from "@/features/model-picker/components/model-selector/folder-browser";
import { Button } from "@/components/ui/button";
import {
Dialog,
@@ -22,9 +21,11 @@ import {
addScanFolder,
listScanFolders,
removeScanFolder,
-} from "@/features/hub/inventory";
-import { openModelsDir } from "@/features/native-intents/api";
+} from "@/features/hub";
+import { FolderBrowser } from "@/features/model-picker";
+import { openModelsDir } from "@/features/native-intents";
import { isTauri } from "@/lib/api-base";
+import { toast } from "@/lib/toast";
import { cn } from "@/lib/utils";
import {
Delete02Icon,
@@ -38,7 +39,6 @@ import {
} from "@hugeicons/core-free-icons";
import { HugeiconsIcon } from "@hugeicons/react";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
-import { toast } from "@/lib/toast";
function pathTail(path: string): string {
const parts = path.split(/[\\/]/).filter(Boolean);
@@ -123,7 +123,9 @@ export function OnDeviceFoldersDialog({
setPath("");
mutationVersionRef.current += 1;
setFolders((current) => {
- const withoutDuplicate = current.filter((row) => row.id !== folder.id);
+ const withoutDuplicate = current.filter(
+ (row) => row.id !== folder.id,
+ );
return [...withoutDuplicate, folder];
});
toast.success("Location added", {
@@ -184,9 +186,12 @@ export function OnDeviceFoldersDialog({
overlayClassName="bg-black/20 backdrop-blur-none"
>
- On-device locations
+
+ On-device locations
+
- Hugging Face model folders, GGUF files, and adapters are indexed here.
+ Hugging Face model folders, GGUF files, and adapters are indexed
+ here.
@@ -342,9 +347,7 @@ export function OnDeviceFoldersDialog({
-
+
{folder.path}
@@ -372,7 +375,10 @@ export function OnDeviceFoldersDialog({
/>
-
+
Open in file manager
@@ -397,7 +403,10 @@ export function OnDeviceFoldersDialog({
)}
-
+
Remove from list
diff --git a/studio/frontend/src/features/hub/hub-page.tsx b/studio/frontend/src/features/hub/hub-page.tsx
index 45f37bf623..92f7559fa6 100644
--- a/studio/frontend/src/features/hub/hub-page.tsx
+++ b/studio/frontend/src/features/hub/hub-page.tsx
@@ -1,35 +1,32 @@
// SPDX-License-Identifier: AGPL-3.0-only
// Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0
-import {
- applyModelLoadConfigToRuntime,
- currentRuntimePerModelConfig,
- resolveInitialConfig,
-} from "@/features/model-picker";
-import { hfModelFitsDevice } from "@/features/model-picker/components/model-selector/recommended-fit";
-import { useHubInventory } from "@/features/hub/inventory";
-import { useDebouncedValue } from "@/hooks/use-debounced-value";
-import { useGpuInfo } from "@/hooks/use-gpu-info";
-import {
- type HfModelSearchChannel,
- type HfSortDirection,
- type HfSortKey,
-} from "@/features/hub/hooks/use-hub-model-search";
-import { useOnlineStatus } from "@/features/hub/hooks/use-online-status";
-import { useHubInfiniteScroll } from "@/features/hub/hooks/use-hub-infinite-scroll";
-import { ggufVariantsMatch, modelIdsMatch } from "@/features/hub/lib/model-identity";
-import { cn } from "@/lib/utils";
import { usePlatformStore } from "@/config/env";
-import {
- hfApiToken,
- useHfTokenStore,
-} from "@/features/hub/stores/hf-token-store";
import {
getInferenceStatus,
isExternalModelId,
useChatModelRuntime,
useChatRuntimeStore,
} from "@/features/chat";
+import { useHubInventory } from "@/features/hub";
+import type {
+ HfModelSearchChannel,
+ HfSortDirection,
+ HfSortKey,
+} from "@/features/hub";
+import { useOnlineStatus } from "@/features/hub";
+import { useHubInfiniteScroll } from "@/features/hub";
+import { ggufVariantsMatch, modelIdsMatch } from "@/features/hub";
+import { hfApiToken, useHfTokenStore } from "@/features/hub";
+import {
+ applyModelLoadConfigToRuntime,
+ currentRuntimePerModelConfig,
+ hfModelFitsDevice,
+ resolveInitialConfig,
+} from "@/features/model-picker";
+import { useDebouncedValue } from "@/hooks/use-debounced-value";
+import { useGpuInfo } from "@/hooks/use-gpu-info";
+import { cn } from "@/lib/utils";
import { useNavigate, useSearch } from "@tanstack/react-router";
import {
useCallback,
@@ -39,17 +36,10 @@ import {
useRef,
useState,
} from "react";
+import { ExternalLinkConfirmDialog } from "./catalog/external-link-confirm-dialog";
import { HubDetailView } from "./catalog/hub-detail-view";
-import { HubTopBar } from "./catalog/hub-top-bar";
import { HubFeed } from "./catalog/hub-feed";
-import { OwnerScopeToggle } from "./catalog/owner-scope-toggle";
-import {
- type AllModelsView,
- HubListHeader,
- type InventorySort,
- InventorySortControl,
- ResultListHeader,
-} from "./catalog/models-table";
+import { HubTopBar } from "./catalog/hub-top-bar";
import {
ModelsCatalog,
type ModelsCatalogHandlers,
@@ -57,9 +47,16 @@ import {
type ModelsCatalogState,
} from "./catalog/models-catalog";
import { ModelsHeader } from "./catalog/models-header";
+import {
+ type AllModelsView,
+ HubListHeader,
+ type InventorySort,
+ InventorySortControl,
+ ResultListHeader,
+} from "./catalog/models-table";
import { ModelsToolbar } from "./catalog/models-toolbar";
-import { ExternalLinkConfirmDialog } from "./catalog/external-link-confirm-dialog";
import { OnDeviceFoldersDialog } from "./catalog/on-device-folders-dialog";
+import { OwnerScopeToggle } from "./catalog/owner-scope-toggle";
import { useDiscoverSearch } from "./hooks/use-discover-search";
import { useFeedWriteBack } from "./hooks/use-feed-write-back";
import { useHubFeed } from "./hooks/use-hub-feed";
@@ -568,15 +565,15 @@ export function ModelsPage() {
const deferredCapabilityFilter = useDeferredValue(capabilityFilter);
const hasQuery = deferredDebouncedQuery.trim() !== "";
- const mode: DiscoverMode = !isModelDiscover
- ? "search"
- : hasQuery
+ const mode: DiscoverMode = isModelDiscover
+ ? hasQuery
? "search"
: urlSection != null
? "channel-list"
: sortBrowseActive
? "search"
- : "feed";
+ : "feed"
+ : "search";
const isFeedMode = mode === "feed";
const isChannelListMode = mode === "channel-list";
const isSortBrowseMode =
@@ -700,7 +697,10 @@ export function ModelsPage() {
return discoverRows.filter(
(row) =>
!isHiddenModelId(row.id) &&
- matchesFormat(detectResultFormat(row.result), effectiveDiscoverFormat) &&
+ matchesFormat(
+ detectResultFormat(row.result),
+ effectiveDiscoverFormat,
+ ) &&
matchesCapability(row.capabilities, deferredCapabilityFilter) &&
(!activeChannel?.finetunableOnly || isUnslothFinetunable(row.result)) &&
// Models already on disk stay visible regardless of device fit,
@@ -763,7 +763,10 @@ export function ModelsPage() {
}
return merged;
}, [isFeedMode, feedTrendingRows, filteredDiscoverRows]);
- const feedResults = useMemo(() => feedRows.map((row) => row.result), [feedRows]);
+ const feedResults = useMemo(
+ () => feedRows.map((row) => row.result),
+ [feedRows],
+ );
const selectionDiscoverRows = isFeedMode ? feedRows : discoverRows;
const selectionFilteredDiscoverRows = isFeedMode
? feedRows
@@ -1343,16 +1346,18 @@ export function ModelsPage() {
);
}
- const ownerToggle = !isDatasetMode ? (
+ const ownerToggle = isDatasetMode ? undefined : (
- ) : undefined;
+ );
// Compact pill so it stays beside the view-mode tabs even in the narrow
// split pane instead of dropping to its own row.
return (
{isChannelListMode ? (
(null);
const [validating, setValidating] = useState(false);
- const wasOpen = useRef(false);
+ const initialDraft = value ?? defaultTemplate ?? "";
+ const renderedDraft = useMemo(
+ () =>
+ open && value == null && defaultTemplate != null && draft.length === 0
+ ? defaultTemplate
+ : draft,
+ [open, value, defaultTemplate, draft],
+ );
- useEffect(() => {
- const justOpened = open && !wasOpen.current;
- wasOpen.current = open;
- if (justOpened) {
- setDraft(value ?? defaultTemplate ?? "");
- setError(null);
- } else if (open && value == null && defaultTemplate != null) {
- setDraft((current) => (current.length === 0 ? defaultTemplate : current));
- }
- }, [open, value, defaultTemplate]);
-
- const byteLength = chatTemplateByteLength(draft);
- const overLimit = !isChatTemplateWithinLimit(draft);
+ const byteLength = chatTemplateByteLength(renderedDraft);
+ const overLimit = !isChatTemplateWithinLimit(renderedDraft);
const matchesDefault =
- defaultTemplate != null && draft === defaultTemplate;
+ defaultTemplate != null && renderedDraft === defaultTemplate;
const handleSave = async () => {
- if (draft.trim().length === 0 || matchesDefault) {
+ if (renderedDraft.trim().length === 0 || matchesDefault) {
onSave(null);
onOpenChange(false);
return;
@@ -72,12 +68,12 @@ export function ChatTemplateEditorDialog({
}
setValidating(true);
try {
- const result = await validateChatTemplate(draft);
+ const result = await validateChatTemplate(renderedDraft);
if (!result.valid) {
setError(result.error ?? "Invalid Jinja template.");
return;
}
- onSave(draft);
+ onSave(renderedDraft);
onOpenChange(false);
} catch {
setError("Could not validate the template.");
@@ -87,10 +83,21 @@ export function ChatTemplateEditorDialog({
};
return (
-
+ {
+ if (nextOpen) {
+ setDraft(initialDraft);
+ setError(null);
+ }
+ onOpenChange(nextOpen);
+ }}
+ >
- {readOnly ? "Chat Template" : "Edit Chat Template"}
+
+ {readOnly ? "Chat Template" : "Edit Chat Template"}
+
{readOnly
? "This is the model's chat template. Custom templates apply to GGUF models for now, so it is view only for safetensors models."
@@ -98,7 +105,7 @@ export function ChatTemplateEditorDialog({
@@ -3289,7 +3309,9 @@ export function HubModelPicker({
ariaLabel={`Inference settings for ${
m.model_id ?? m.display_name
}`}
- onConfigure={() => onConfigure(m.id, localModelMeta())}
+ onConfigure={() =>
+ onConfigure(m.id, localModelMeta())
+ }
/>
)}
@@ -3309,7 +3331,9 @@ export function HubModelPicker({
gpuGb={
gpu.available ? gpu.memoryTotalGb : undefined
}
- systemRamGb={gpu.systemRamAvailableGb || undefined}
+ systemRamGb={
+ gpu.systemRamAvailableGb || undefined
+ }
/>
)}
@@ -3384,7 +3408,9 @@ export function HubModelPicker({
ariaLabel={`Inference settings for ${
m.model_id ?? m.display_name
}`}
- onConfigure={() => onConfigure(m.id, localModelMeta())}
+ onConfigure={() =>
+ onConfigure(m.id, localModelMeta())
+ }
/>
)}
@@ -3404,7 +3430,9 @@ export function HubModelPicker({
gpuGb={
gpu.available ? gpu.memoryTotalGb : undefined
}
- systemRamGb={gpu.systemRamAvailableGb || undefined}
+ systemRamGb={
+ gpu.systemRamAvailableGb || undefined
+ }
/>
)}
@@ -3486,7 +3514,9 @@ export function HubModelPicker({
gpuGb={
gpu.available ? gpu.memoryTotalGb : undefined
}
- systemRamGb={gpu.systemRamAvailableGb || undefined}
+ systemRamGb={
+ gpu.systemRamAvailableGb || undefined
+ }
variantActions={{
onDelete: async (quant) => {
await deleteCachedModel(id, quant);
@@ -3546,10 +3576,14 @@ export function HubModelPicker({
}
}}
vramStatus={
- isKnownGgufRepo(id) ? null : (vram?.status ?? null)
+ isKnownGgufRepo(id)
+ ? null
+ : (vram?.status ?? null)
}
vramEst={isKnownGgufRepo(id) ? undefined : vram?.est}
- gpuGb={gpu.available ? gpu.memoryTotalGb : undefined}
+ gpuGb={
+ gpu.available ? gpu.memoryTotalGb : undefined
+ }
onArrowDownIntoChildren={
expandedGguf === id
? () => {
@@ -3576,7 +3610,9 @@ export function HubModelPicker({
gpuGb={
gpu.available ? gpu.memoryTotalGb : undefined
}
- systemRamGb={gpu.systemRamAvailableGb || undefined}
+ systemRamGb={
+ gpu.systemRamAvailableGb || undefined
+ }
variantActions={{
onDelete: async (quant) => {
await deleteCachedModel(id, quant);
@@ -3668,7 +3704,9 @@ export function HubModelPicker({
gpuGb={
gpu.available ? gpu.memoryTotalGb : undefined
}
- systemRamGb={gpu.systemRamAvailableGb || undefined}
+ systemRamGb={
+ gpu.systemRamAvailableGb || undefined
+ }
variantActions={{
onDelete: async (quant) => {
await deleteCachedModel(id, quant);
diff --git a/studio/frontend/src/features/model-picker/components/model-selector/pill-tabs.tsx b/studio/frontend/src/features/model-picker/components/model-selector/pill-tabs.tsx
index e6da8a7b74..fbc1d5ac91 100644
--- a/studio/frontend/src/features/model-picker/components/model-selector/pill-tabs.tsx
+++ b/studio/frontend/src/features/model-picker/components/model-selector/pill-tabs.tsx
@@ -78,7 +78,8 @@ export function PillTabs({
onValueChange(tabs[next].value);
e.currentTarget.parentElement
?.querySelectorAll
('button[role="tab"]')
- [next]?.focus();
+ .item(next)
+ ?.focus();
}}
onClick={() => onValueChange(tab.value)}
className={cn(
diff --git a/studio/frontend/src/features/model-picker/hooks/use-model-defaults.ts b/studio/frontend/src/features/model-picker/hooks/use-model-defaults.ts
index 57dab863cc..8cec99e942 100644
--- a/studio/frontend/src/features/model-picker/hooks/use-model-defaults.ts
+++ b/studio/frontend/src/features/model-picker/hooks/use-model-defaults.ts
@@ -1,8 +1,7 @@
// SPDX-License-Identifier: AGPL-3.0-only
// Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0
-import { looksLikeLocalPath } from "@/features/hub/lib/local-path";
-import { useHfTokenStore } from "@/features/hub/stores/hf-token-store";
+import { looksLikeLocalPath, useHfTokenStore } from "@/features/hub";
import { useEffect, useState } from "react";
import { fetchModelMaxPositionEmbeddings } from "../api/model-metadata";
import { fetchDefaultChatTemplate } from "../api/templates";
diff --git a/studio/frontend/src/features/model-picker/index.ts b/studio/frontend/src/features/model-picker/index.ts
index 057d0dc1a0..e76278dacc 100644
--- a/studio/frontend/src/features/model-picker/index.ts
+++ b/studio/frontend/src/features/model-picker/index.ts
@@ -2,6 +2,14 @@
// Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0
export { ModelSelector } from "./components/model-selector";
+export { FolderBrowser } from "./components/model-selector/folder-browser";
+export { ModelDeleteAction } from "./components/model-selector/model-delete-action";
+export { ModelUpdateAction } from "./components/model-selector/model-update-action";
+export { hfModelFitsDevice } from "./components/model-selector/recommended-fit";
+export {
+ NumericValueInput,
+ snapToStep,
+} from "./components/numeric-value-input";
export { SidebarModelConfig } from "./components/sidebar-model-config";
export type {
DeletedModelRef,
diff --git a/studio/frontend/src/features/model-picker/inventory/use-chat-picker-inventory.ts b/studio/frontend/src/features/model-picker/inventory/use-chat-picker-inventory.ts
index 72ec24f913..14a54d45e0 100644
--- a/studio/frontend/src/features/model-picker/inventory/use-chat-picker-inventory.ts
+++ b/studio/frontend/src/features/model-picker/inventory/use-chat-picker-inventory.ts
@@ -5,13 +5,13 @@ import type {
CachedGgufRepo,
CachedModelRepo,
LocalModelInfo,
-} from "@/features/chat/api/chat-api";
+} from "@/features/chat";
import {
type CachedInventoryRow,
type LocalInventoryRow,
type LocalSource,
useHubInventory,
-} from "@/features/hub/inventory";
+} from "@/features/hub";
import { useMemo } from "react";
const PICKER_LOCAL_SOURCES: ReadonlySet = new Set([
diff --git a/studio/frontend/src/features/model-picker/model-config/apply-per-model-config.ts b/studio/frontend/src/features/model-picker/model-config/apply-per-model-config.ts
index 5c8f582f45..e21233251a 100644
--- a/studio/frontend/src/features/model-picker/model-config/apply-per-model-config.ts
+++ b/studio/frontend/src/features/model-picker/model-config/apply-per-model-config.ts
@@ -5,11 +5,11 @@ import {
normalizeSpeculativeType,
readPersistedSpeculativeType,
useChatRuntimeStore,
-} from "@/features/chat/stores/chat-runtime-store";
+} from "@/features/chat";
import {
DEFAULT_PER_MODEL_CONFIG,
- normalizeMaxSeqLength,
type PerModelConfig,
+ normalizeMaxSeqLength,
} from "./per-model-config";
function cleanTemplate(value: string | null | undefined): string | null {
diff --git a/studio/frontend/src/features/model-picker/model-config/model-identity.ts b/studio/frontend/src/features/model-picker/model-config/model-identity.ts
index f0a46d2aaa..0caa7c1312 100644
--- a/studio/frontend/src/features/model-picker/model-config/model-identity.ts
+++ b/studio/frontend/src/features/model-picker/model-config/model-identity.ts
@@ -4,12 +4,12 @@
import {
normalizeGgufVariantIdentity,
normalizeModelIdentity,
-} from "@/features/hub/lib/model-identity";
+} from "@/features/hub";
export {
normalizeGgufVariantIdentity,
normalizeModelIdentity,
-} from "@/features/hub/lib/model-identity";
+} from "@/features/hub";
const MODEL_STORAGE_KEY_PREFIX = "v2:";