feat(chat): eject current model and start fresh thread on model switch

This commit is contained in:
Roland Tannous 2026-02-22 13:17:03 +00:00
commit 536a735acc
2 changed files with 106 additions and 95 deletions

View file

@ -218,9 +218,9 @@ function InlineSidebar({
className={cn(
"bg-sidebar text-sidebar-foreground h-full overflow-hidden rounded-2xl corner-squircle transition-[width] duration-200 ease-linear",
!collapsed &&
(side === "left"
? "border-r border-0 border-sidebar-border"
: "border-l border-0 border-sidebar-border"),
(side === "left"
? "border-r border-0 border-sidebar-border"
: "border-l border-0 border-sidebar-border"),
collapsed ? "w-0" : "w-(--sidebar-width)",
)}
>
@ -301,9 +301,18 @@ export function ChatPage(): ReactElement {
const handleCheckpointChange = useCallback(
(value: string, meta?: { isLora: boolean }) => {
void selectModel({ id: value, isLora: meta?.isLora });
const currentCheckpoint =
useChatRuntimeStore.getState().params.checkpoint;
if (!value || value === currentCheckpoint) return;
setView({ mode: "single", newThreadNonce: crypto.randomUUID() });
void (async () => {
if (currentCheckpoint) {
await ejectModel();
}
await selectModel({ id: value, isLora: meta?.isLora });
})();
},
[selectModel],
[selectModel, ejectModel],
);
const handleEject = useCallback(() => {
void ejectModel();
@ -475,99 +484,99 @@ export function ChatPage(): ReactElement {
return (
<div className="h-[calc(100dvh-4rem)] bg-background overflow-hidden">
<GuidedTour {...tour.tourProps} />
<SidebarProvider
defaultOpen={true}
open={sidebarOpen}
onOpenChange={setSidebarOpen}
className="!min-h-0 h-full w-full max-w-7xl mx-auto px-2 sm:px-4"
style={
{
"--sidebar-width": "14rem",
"--sidebar-width-icon": "3rem",
} as CSSProperties
}
>
<InlineSidebar>
<ThreadSidebar
view={view}
onSelect={setView}
onNewThread={handleNewThread}
onNewCompare={handleNewCompare}
showCompare={canCompare}
/>
</InlineSidebar>
<GuidedTour {...tour.tourProps} />
<SidebarProvider
defaultOpen={true}
open={sidebarOpen}
onOpenChange={setSidebarOpen}
className="!min-h-0 h-full w-full max-w-7xl mx-auto px-2 sm:px-4"
style={
{
"--sidebar-width": "14rem",
"--sidebar-width-icon": "3rem",
} as CSSProperties
}
>
<InlineSidebar>
<ThreadSidebar
view={view}
onSelect={setView}
onNewThread={handleNewThread}
onNewCompare={handleNewCompare}
showCompare={canCompare}
/>
</InlineSidebar>
<div className="flex min-h-0 min-w-0 flex-1 flex-col">
<div className="flex h-11 shrink-0 items-center px-1.5 sm:px-2">
<div className="flex items-center gap-1">
<SidebarTrigger />
<TopBarActions
onNewThread={handleNewThread}
onNewCompare={handleNewCompare}
showCompare={canCompare}
/>
<ModelSelector
models={models}
loraModels={loraModels}
value={inferenceParams.checkpoint}
onValueChange={handleCheckpointChange}
onEject={handleEject}
variant="ghost"
open={modelSelectorOpen}
onOpenChange={handleModelSelectorOpenChange}
triggerDataTour="chat-model-selector"
contentDataTour="chat-model-selector-popover"
className="max-w-[62vw] sm:max-w-none"
/>
{loadingModel ? (
<div
className="flex items-center gap-1.5 text-muted-foreground"
title={`Loading ${loadingModel.displayName}. This may include downloading.`}
>
<Spinner className="size-3.5 shrink-0" />
<span className="text-xs">
Downloading model
</span>
</div>
) : null}
</div>
{modelsError && (
<div className="ml-2 text-xs text-destructive truncate max-w-[28rem]">
{modelsError}
<div className="flex min-h-0 min-w-0 flex-1 flex-col">
<div className="flex h-11 shrink-0 items-center px-1.5 sm:px-2">
<div className="flex items-center gap-1">
<SidebarTrigger />
<TopBarActions
onNewThread={handleNewThread}
onNewCompare={handleNewCompare}
showCompare={canCompare}
/>
<ModelSelector
models={models}
loraModels={loraModels}
value={inferenceParams.checkpoint}
onValueChange={handleCheckpointChange}
onEject={handleEject}
variant="ghost"
open={modelSelectorOpen}
onOpenChange={handleModelSelectorOpenChange}
triggerDataTour="chat-model-selector"
contentDataTour="chat-model-selector-popover"
className="max-w-[62vw] sm:max-w-none"
/>
{loadingModel ? (
<div
className="flex items-center gap-1.5 text-muted-foreground"
title={`Loading ${loadingModel.displayName}. This may include downloading.`}
>
<Spinner className="size-3.5 shrink-0" />
<span className="text-xs">
Downloading model
</span>
</div>
) : null}
</div>
{modelsError && (
<div className="ml-2 text-xs text-destructive truncate max-w-[28rem]">
{modelsError}
</div>
)}
<div className="flex-1" />
<button
type="button"
onClick={() => setSettingsOpen((o) => !o)}
className="flex h-9 w-9 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-accent hover:text-foreground"
title="Inference settings"
data-tour="chat-settings"
>
<HugeiconsIcon icon={Settings04Icon} className="size-5" />
</button>
</div>
{view.mode === "single" ? (
<SingleContent
key={view.threadId ?? view.newThreadNonce ?? "new"}
threadId={view.threadId}
newThreadNonce={view.newThreadNonce}
/>
) : (
<CompareContent key={view.pairId} pairId={view.pairId} />
)}
<div className="flex-1" />
<button
type="button"
onClick={() => setSettingsOpen((o) => !o)}
className="flex h-9 w-9 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-accent hover:text-foreground"
title="Inference settings"
data-tour="chat-settings"
>
<HugeiconsIcon icon={Settings04Icon} className="size-5" />
</button>
</div>
{view.mode === "single" ? (
<SingleContent
key={view.threadId ?? view.newThreadNonce ?? "new"}
threadId={view.threadId}
newThreadNonce={view.newThreadNonce}
/>
) : (
<CompareContent key={view.pairId} pairId={view.pairId} />
)}
</div>
<ChatSettingsPanel
open={settingsOpen}
params={inferenceParams}
onParamsChange={setInferenceParams}
autoTitle={autoTitle}
onAutoTitleChange={setAutoTitle}
/>
</SidebarProvider>
<ChatSettingsPanel
open={settingsOpen}
params={inferenceParams}
onParamsChange={setInferenceParams}
autoTitle={autoTitle}
onAutoTitleChange={setAutoTitle}
/>
</SidebarProvider>
</div>
);
}

View file

@ -165,8 +165,10 @@ export function useChatModelRuntime() {
setLoadingModel({ id: modelId, displayName });
try {
async function performLoad(): Promise<void> {
if (params.checkpoint) {
await unloadModel({ model_path: params.checkpoint });
const currentCheckpoint =
useChatRuntimeStore.getState().params.checkpoint;
if (currentCheckpoint) {
await unloadModel({ model_path: currentCheckpoint });
}
const loadResponse = await loadModel({