feat(hub): enable Run/New Chat for downloaded GGUF models, fix README (#6152)

* feat(hub): enable Run/New Chat for downloaded GGUF models, fix README bottom spacing

- enable the Run / New Chat action for GGUF models that are already downloaded, across the download card, on-device card, and hub page
- remove the extra bottom spacing under the model inspector README

* fix(hub): sync active GGUF variant on mount

* feat(hub): open a new chat immediately when Run is clicked

---------

Co-authored-by: Lee Jackson <130007945+Imagineer99@users.noreply.github.com>
This commit is contained in:
Eyera 2026-06-10 19:04:17 +02:00 committed by GitHub
commit cae12b793f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 84 additions and 39 deletions

View file

@ -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";

View file

@ -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 &&

View file

@ -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 (
<div className="flex w-full flex-col gap-2">
@ -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. */}
<div
className={cn(
"group/pair flex h-9 shrink-0 items-stretch gap-1.5",
!HUB_POST_DOWNLOAD_ACTIONS_VISIBLE && "hidden",
!runActionsVisible && "hidden",
)}
>
{onTrain && (

View file

@ -725,8 +725,6 @@ export const ModelInspector = memo(function ModelInspector({
/>
)}
</div>
<div aria-hidden="true" className="h-3 shrink-0" />
</div>
);
});

View file

@ -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()}
/>
<section className="elevated-card flex min-h-0 flex-1 flex-col overflow-hidden bg-card">

View file

@ -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;