From c73fb954a703eef88e085b8fa675a2b02892202e Mon Sep 17 00:00:00 2001 From: sneakr Date: Fri, 10 Jul 2026 07:28:04 +0200 Subject: [PATCH] Fix model picker review findings Chat template editor never seeded its draft. Radix only calls onOpenChange from internal events, so the seed in the nextOpen branch was dead and a model with a saved override opened empty. Saving then cleared the override. Drop the dead branch, treat draft as an untouched sentinel, and reset it on every close. Uncached Hub picks could auto load a model after the user left the chat. Main detached the staged pick on route exit and on chat context change. Carry the context key on the pending pick and skip the load when it no longer matches. Also clear configTarget when the picker closes, restore the onUpdated ref so variant rows stop resubscribing on every parent render, skip the LRU write when the entry is already most recent, import NumericValueInput relatively, and drop the unused ModelUpdateAction barrel export. --- .../frontend/src/features/chat/chat-page.tsx | 36 ++++++++++++------ .../chat-template-editor-dialog.tsx | 37 ++++++++----------- .../components/model-config-page.tsx | 2 +- .../components/model-selector.tsx | 10 +++-- .../model-selector/model-update-action.tsx | 16 ++++++-- .../src/features/model-picker/index.ts | 1 - .../model-config/per-model-config.ts | 7 +++- 7 files changed, 67 insertions(+), 42 deletions(-) diff --git a/studio/frontend/src/features/chat/chat-page.tsx b/studio/frontend/src/features/chat/chat-page.tsx index 3b2f043255..8e79b32f27 100644 --- a/studio/frontend/src/features/chat/chat-page.tsx +++ b/studio/frontend/src/features/chat/chat-page.tsx @@ -1108,6 +1108,11 @@ export function validateChatSearch(search: Record): ChatSearch }; } +type PendingHubAutoLoad = { + selection: SelectedModelInput; + contextKey: string; +}; + // `search` comes from RootLayout (not useSearch) so ChatPage stays mounted off-route // (keeping an in-flight generation alive), frozen to the last /chat search. `active` // is false off-route: close body-portaled surfaces and stop route-specific listeners @@ -1680,8 +1685,9 @@ export function ChatPage({ }, [activeThreadId, closeArtifactSurface, selectedArtifact, view]); const hasActiveModel = Boolean(inferenceParams.checkpoint); + const chatContextKey = `${view.mode}|${activeThreadId ?? ""}|${search.new ?? ""}|${search.project ?? ""}`; const [pendingHubAutoLoad, setPendingHubAutoLoad] = - useState(null); + useState(null); const stageOrLoad = useCallback( async (selection: SelectedModelInput) => { const store = useChatRuntimeStore.getState(); @@ -1733,7 +1739,7 @@ export function ChatPage({ hasGgufSource(selection) && !selection.isDownloaded); if (wantManagerStage) { - setPendingHubAutoLoad(selection); + setPendingHubAutoLoad({ selection, contextKey: chatContextKey }); return; } const previousConfig = currentRuntimePerModelConfig({ @@ -1748,24 +1754,30 @@ export function ChatPage({ previousConfig, }); }, - [selectModel, loadingModel, rememberedConfigFor], + [selectModel, loadingModel, rememberedConfigFor, chatContextKey], ); useRepoDownload({ kind: DOWNLOAD_KIND.MODEL, - repoId: pendingHubAutoLoad?.id ?? "__hub_autoload_idle__", - activeVariant: pendingHubAutoLoad?.ggufVariant ?? null, + repoId: pendingHubAutoLoad?.selection.id ?? "__hub_autoload_idle__", + activeVariant: pendingHubAutoLoad?.selection.ggufVariant ?? null, onComplete: (variant) => { const pending = pendingHubAutoLoad; - if (!pending || (pending.ggufVariant ?? null) !== (variant ?? null)) { + if ( + !pending || + (pending.selection.ggufVariant ?? null) !== (variant ?? null) + ) { return; } setPendingHubAutoLoad(null); - void stageOrLoad({ ...pending, isDownloaded: true }); + if (!active || pending.contextKey !== chatContextKey) { + return; + } + void stageOrLoad({ ...pending.selection, isDownloaded: true }); }, onError: (variant) => { if ( pendingHubAutoLoad && - (pendingHubAutoLoad.ggufVariant ?? null) === (variant ?? null) + (pendingHubAutoLoad.selection.ggufVariant ?? null) === (variant ?? null) ) { setPendingHubAutoLoad(null); } @@ -1773,7 +1785,7 @@ export function ChatPage({ onCancelled: (variant) => { if ( pendingHubAutoLoad && - (pendingHubAutoLoad.ggufVariant ?? null) === (variant ?? null) + (pendingHubAutoLoad.selection.ggufVariant ?? null) === (variant ?? null) ) { setPendingHubAutoLoad(null); } @@ -1786,9 +1798,9 @@ export function ChatPage({ void (async () => { const outcome = await downloadManager.requestStart({ kind: DOWNLOAD_KIND.MODEL, - repoId: pending.id, - variant: pending.ggufVariant ?? null, - expectedBytes: pending.expectedBytes ?? 0, + repoId: pending.selection.id, + variant: pending.selection.ggufVariant ?? null, + expectedBytes: pending.selection.expectedBytes ?? 0, }); if (!active) return; if (outcome === "started") { diff --git a/studio/frontend/src/features/model-picker/components/chat-template-editor-dialog.tsx b/studio/frontend/src/features/model-picker/components/chat-template-editor-dialog.tsx index bfe4181e92..16ab776e61 100644 --- a/studio/frontend/src/features/model-picker/components/chat-template-editor-dialog.tsx +++ b/studio/frontend/src/features/model-picker/components/chat-template-editor-dialog.tsx @@ -12,7 +12,7 @@ import { } from "@/components/ui/dialog"; import { Spinner } from "@/components/ui/spinner"; import { Textarea } from "@/components/ui/textarea"; -import { useMemo, useState } from "react"; +import { useState } from "react"; import { validateChatTemplate } from "../api/templates"; import { MAX_CHAT_TEMPLATE_BYTES, @@ -39,27 +39,26 @@ export function ChatTemplateEditorDialog({ onSave, readOnly = false, }: ChatTemplateEditorDialogProps) { - const [draft, setDraft] = useState(""); + const [draft, setDraft] = useState(null); const [error, setError] = useState(null); const [validating, setValidating] = useState(false); - const initialDraft = value ?? defaultTemplate ?? ""; - const renderedDraft = useMemo( - () => - open && value == null && defaultTemplate != null && draft.length === 0 - ? defaultTemplate - : draft, - [open, value, defaultTemplate, draft], - ); + const renderedDraft = draft ?? value ?? defaultTemplate ?? ""; const byteLength = chatTemplateByteLength(renderedDraft); const overLimit = !isChatTemplateWithinLimit(renderedDraft); const matchesDefault = defaultTemplate != null && renderedDraft === defaultTemplate; + const handleClose = () => { + setDraft(null); + setError(null); + onOpenChange(false); + }; + const handleSave = async () => { if (renderedDraft.trim().length === 0 || matchesDefault) { onSave(null); - onOpenChange(false); + handleClose(); return; } if (overLimit) { @@ -74,7 +73,7 @@ export function ChatTemplateEditorDialog({ return; } onSave(renderedDraft); - onOpenChange(false); + handleClose(); } catch { setError("Could not validate the template."); } finally { @@ -87,10 +86,10 @@ export function ChatTemplateEditorDialog({ open={open} onOpenChange={(nextOpen) => { if (nextOpen) { - setDraft(initialDraft); - setError(null); + onOpenChange(true); + return; } - onOpenChange(nextOpen); + handleClose(); }} > @@ -135,7 +134,7 @@ export function ChatTemplateEditorDialog({ {readOnly ? (
-
@@ -157,11 +156,7 @@ export function ChatTemplateEditorDialog({ )}
-