diff --git a/studio/frontend/src/features/chat/index.ts b/studio/frontend/src/features/chat/index.ts
index bfaf29f519..dde3a75475 100644
--- a/studio/frontend/src/features/chat/index.ts
+++ b/studio/frontend/src/features/chat/index.ts
@@ -19,6 +19,7 @@ export {
export { useChatRuntimeStore } from "./stores/chat-runtime-store";
export { useChatSearchStore } from "./stores/chat-search-store";
export { useChatModelRuntime } from "./hooks/use-chat-model-runtime";
+export { isExternalModelId } from "./external-providers";
export { ChatSearchDialog } from "./components/chat-search-dialog";
export { setTrainingCompareHandoff } from "./lib/training-compare-handoff";
export type { ProjectRecord } from "./types";
diff --git a/studio/frontend/src/features/hub/catalog/gguf-download-card.tsx b/studio/frontend/src/features/hub/catalog/gguf-download-card.tsx
index 72743af722..e0178c3472 100644
--- a/studio/frontend/src/features/hub/catalog/gguf-download-card.tsx
+++ b/studio/frontend/src/features/hub/catalog/gguf-download-card.tsx
@@ -20,7 +20,7 @@ import {
import { type GgufVariantDetail, deleteCachedModel } from "../inventory";
import { formatBytes } from "../lib/format";
import { type GgufFitClass, classifyGgufFit } from "../lib/gguf-fit";
-import { HUB_POST_DOWNLOAD_ACTIONS_VISIBLE } from "../lib/hub-feature-flags";
+import { HUB_GGUF_RUN_ACTIONS_VISIBLE } from "../lib/hub-feature-flags";
import {
ggufVariantsMatch,
normalizeGgufVariantIdentity,
@@ -794,8 +794,7 @@ export function GgufDownloadCard({
downloadingThisVariant &&
!cancelling &&
"hover:bg-rose-500/10 hover:text-rose-600 dark:hover:text-rose-400",
- // Hide post-download CTAs (Run / New Chat) for this PR.
- !HUB_POST_DOWNLOAD_ACTIONS_VISIBLE &&
+ !HUB_GGUF_RUN_ACTIONS_VISIBLE &&
!downloadingThisVariant &&
!cancelling &&
!isLoadingThisModel &&
diff --git a/studio/frontend/src/features/hub/catalog/local-on-device-card.tsx b/studio/frontend/src/features/hub/catalog/local-on-device-card.tsx
index 9b3d401b99..0dd50f0a41 100644
--- a/studio/frontend/src/features/hub/catalog/local-on-device-card.tsx
+++ b/studio/frontend/src/features/hub/catalog/local-on-device-card.tsx
@@ -2,7 +2,10 @@
// Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0
import { TrainIcon } from "../components/train-icon";
-import { HUB_POST_DOWNLOAD_ACTIONS_VISIBLE } from "../lib/hub-feature-flags";
+import {
+ HUB_GGUF_RUN_ACTIONS_VISIBLE,
+ HUB_POST_DOWNLOAD_ACTIONS_VISIBLE,
+} from "../lib/hub-feature-flags";
import {
Popover,
PopoverContent,
@@ -295,6 +298,9 @@ export function LocalOnDeviceCard({
const formatTone =
modelFormat === "adapter" ? "adapter" : isGguf ? "gguf" : "checkpoint";
const showOldCacheHint = source === "hf_cache" && !!unsupportedReason;
+ const runActionsVisible = isGguf
+ ? HUB_GGUF_RUN_ACTIONS_VISIBLE
+ : HUB_POST_DOWNLOAD_ACTIONS_VISIBLE;
return (
@@ -440,11 +446,10 @@ export function LocalOnDeviceCard({
className="ml-1 mr-0 h-5 w-px shrink-0 bg-foreground/[0.06] opacity-100 transition-opacity duration-150 group-hover/dl:opacity-0 dark:bg-white/[0.04]"
/>
)}
- {/* Run + Train pair hidden until Hub->chat/train pickers ship. */}
{onTrain && (
diff --git a/studio/frontend/src/features/hub/catalog/model-inspector.tsx b/studio/frontend/src/features/hub/catalog/model-inspector.tsx
index 54165aa5e6..a3a73f2714 100644
--- a/studio/frontend/src/features/hub/catalog/model-inspector.tsx
+++ b/studio/frontend/src/features/hub/catalog/model-inspector.tsx
@@ -725,8 +725,6 @@ export const ModelInspector = memo(function ModelInspector({
/>
)}
-
-
);
});
diff --git a/studio/frontend/src/features/hub/hub-page.tsx b/studio/frontend/src/features/hub/hub-page.tsx
index 5b7fc4ad77..7324bed262 100644
--- a/studio/frontend/src/features/hub/hub-page.tsx
+++ b/studio/frontend/src/features/hub/hub-page.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
-// Hub page: browse + download surface only. Train CTA hidden until Hub->train picker ships.
import { useHubInventory } from "@/features/hub/inventory";
import { useDebouncedValue } from "@/hooks/use-debounced-value";
import { useGpuInfo } from "@/hooks/use-gpu-info";
@@ -12,9 +11,15 @@ import {
import { useOnlineStatus } from "@/features/hub/hooks/use-online-status";
import { useIsHubDesktop } from "@/features/hub/hooks/use-is-hub-desktop";
import { useHubInfiniteScroll } from "@/features/hub/hooks/use-hub-infinite-scroll";
-import { modelIdsMatch } from "@/features/hub/lib/model-identity";
+import { ggufVariantsMatch, modelIdsMatch } from "@/features/hub/lib/model-identity";
import { cn } from "@/lib/utils";
import { useHfTokenStore } from "@/features/hub/stores/hf-token-store";
+import {
+ getInferenceStatus,
+ isExternalModelId,
+ useChatModelRuntime,
+ useChatRuntimeStore,
+} from "@/features/chat";
import { ArrowLeft01Icon } from "@hugeicons/core-free-icons";
import { HugeiconsIcon } from "@hugeicons/react";
import { useNavigate, useSearch } from "@tanstack/react-router";
@@ -179,14 +184,35 @@ export function ModelsPage() {
const gpu = useGpuInfo();
const online = useOnlineStatus();
- // "Use in chat" just navigates to /chat; checkpoint/variant handoff lands with the Hub-aware picker.
- const activeCheckpoint: string | null = null;
- const activeGgufVariant: string | null = null;
- // Stubs so the inspector + load-status memo still typecheck; cast keeps the shape from narrowing to `never`.
- const loadingModel = null as { id: string } | null;
- const loadProgress = null as {
- phase?: "downloading" | "starting";
- } | null;
+ const { selectModel, loadingModel, loadProgress, ejectModel } =
+ useChatModelRuntime();
+ const checkpoint = useChatRuntimeStore((s) => s.params.checkpoint);
+ const activeCheckpoint =
+ checkpoint && !isExternalModelId(checkpoint) ? checkpoint : null;
+ const activeGgufVariant = useChatRuntimeStore((s) => s.activeGgufVariant);
+
+ useEffect(() => {
+ let cancelled = false;
+ void getInferenceStatus()
+ .then((status) => {
+ if (cancelled || !status.active_model) return;
+ const store = useChatRuntimeStore.getState();
+ if (
+ !isExternalModelId(store.params.checkpoint) &&
+ (!modelIdsMatch(store.params.checkpoint, status.active_model) ||
+ !ggufVariantsMatch(
+ store.activeGgufVariant,
+ status.gguf_variant ?? null,
+ ))
+ ) {
+ store.setCheckpoint(status.active_model, status.gguf_variant ?? null);
+ }
+ })
+ .catch(() => undefined);
+ return () => {
+ cancelled = true;
+ };
+ }, []);
const { tab, setTab: setModelsTab } = useModelsTabState();
const [query, setQuery] = useState("");
@@ -522,8 +548,8 @@ export function ModelsPage() {
);
const isLoadingThisModel = useMemo(() => {
- if (!loadingModel) return false;
- return selectedRepoMatchesRuntime(selectedModel, loadingModel.id, null);
+ if (!loadingModel || !selectedModel) return false;
+ return modelIdsMatch(loadingModel.id, selectedModel.resource.runId);
}, [loadingModel, selectedModel]);
const { vramInfo, minMemory } = useHubModelVram(selectedModel, gpu);
@@ -536,22 +562,41 @@ export function ModelsPage() {
? `${Math.floor(gpu.systemRamAvailableGb)} GB`
: "Unavailable";
- // Load / use-in-chat / train handlers are stubs; they wire in once the
- // Hub-aware chat and train pickers ship.
+ const openNewChat = useCallback(() => {
+ void navigate({ to: "/chat", search: { new: crypto.randomUUID() } });
+ }, [navigate]);
+ const runSelectedModel = useCallback(
+ (opts: ModelLoadOptions, isDownloaded: boolean) => {
+ if (!selectedModel) return;
+ const runId = selectedModel.resource.runId;
+ void selectModel({
+ id: runId,
+ ggufVariant: opts.ggufVariant,
+ isDownloaded,
+ expectedBytes: opts.expectedBytes,
+ throwOnError: true,
+ })
+ .then(() => {
+ const store = useChatRuntimeStore.getState();
+ if (!modelIdsMatch(store.params.checkpoint, runId)) {
+ store.setCheckpoint(runId, opts.ggufVariant ?? null);
+ }
+ })
+ .catch(() => undefined);
+ openNewChat();
+ },
+ [openNewChat, selectModel, selectedModel],
+ );
const handleLoad = useCallback(
- (_opts: ModelLoadOptions) => undefined,
- [],
+ (opts: ModelLoadOptions) =>
+ runSelectedModel(opts, selectedModel?.isDownloaded ?? true),
+ [runSelectedModel, selectedModel],
);
const handleLoadLocal = useCallback(
- (_opts: ModelLoadOptions = {}) => undefined,
- [],
+ (opts: ModelLoadOptions = {}) => runSelectedModel(opts, true),
+ [runSelectedModel],
);
- const handleUseInChat = useCallback(() => {
- void navigate({ to: "/chat" });
- }, [navigate]);
- const handleTrain = useCallback(() => {
- // Train CTA hidden until Hub->train picker ships.
- }, []);
+ const handleTrain = useCallback(() => undefined, []);
const inspectorRuntime = useMemo(
() => ({
@@ -582,14 +627,14 @@ export function ModelsPage() {
() => ({
onLoad: handleLoad,
onLoadLocal: handleLoadLocal,
- onUseInChat: handleUseInChat,
+ onUseInChat: openNewChat,
onTrain: handleTrain,
onInventoryChange: refreshInventory,
}),
[
handleLoad,
handleLoadLocal,
- handleUseInChat,
+ openNewChat,
handleTrain,
refreshInventory,
],
@@ -685,7 +730,7 @@ export function ModelsPage() {
ramLabel={ramLabel}
activeCheckpoint={activeCheckpoint}
activeGgufVariant={activeGgufVariant}
- onEject={() => undefined}
+ onEject={() => void ejectModel()}
/>
diff --git a/studio/frontend/src/features/hub/lib/hub-feature-flags.ts b/studio/frontend/src/features/hub/lib/hub-feature-flags.ts
index cf28403725..d0d527b720 100644
--- a/studio/frontend/src/features/hub/lib/hub-feature-flags.ts
+++ b/studio/frontend/src/features/hub/lib/hub-feature-flags.ts
@@ -1,8 +1,5 @@
// SPDX-License-Identifier: AGPL-3.0-only
// Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0
-// Hub feature flags for staged rollout. Each gates a CTA whose integration ships
-// in a later PR; JSX/wiring stay in place so flipping here is the only re-enable edit.
-
-// Post-download Run / chat / Train CTAs, hidden until the Hub-aware chat and train pickers ship.
export const HUB_POST_DOWNLOAD_ACTIONS_VISIBLE = false;
+export const HUB_GGUF_RUN_ACTIONS_VISIBLE = true;