diff --git a/install.ps1 b/install.ps1
index 5ddb42ea7e..a2acd6c4ea 100644
--- a/install.ps1
+++ b/install.ps1
@@ -819,7 +819,7 @@ shell.Run cmd, 0, False
if ($SkipTorch) {
# No-torch: install unsloth + unsloth-zoo with --no-deps, then
# runtime deps (typer, safetensors, transformers, etc.) with --no-deps.
- $baseInstallExit = Invoke-InstallCommand { uv pip install --python $VenvPython --no-deps --reinstall-package unsloth --reinstall-package unsloth-zoo "unsloth>=2026.4.2" unsloth-zoo }
+ $baseInstallExit = Invoke-InstallCommand { uv pip install --python $VenvPython --no-deps --reinstall-package unsloth --reinstall-package unsloth-zoo "unsloth>=2026.4.4" unsloth-zoo }
if ($baseInstallExit -eq 0) {
$NoTorchReq = Find-NoTorchRuntimeFile
if ($NoTorchReq) {
@@ -827,7 +827,7 @@ shell.Run cmd, 0, False
}
}
} else {
- $baseInstallExit = Invoke-InstallCommand { uv pip install --python $VenvPython --reinstall-package unsloth --reinstall-package unsloth-zoo "unsloth>=2026.4.2" unsloth-zoo }
+ $baseInstallExit = Invoke-InstallCommand { uv pip install --python $VenvPython --reinstall-package unsloth --reinstall-package unsloth-zoo "unsloth>=2026.4.4" unsloth-zoo }
}
if ($baseInstallExit -ne 0) {
Write-Host "[ERROR] Failed to install unsloth (exit code $baseInstallExit)" -ForegroundColor Red
@@ -857,7 +857,7 @@ shell.Run cmd, 0, False
if ($SkipTorch) {
# No-torch: install unsloth + unsloth-zoo with --no-deps, then
# runtime deps (typer, safetensors, transformers, etc.) with --no-deps.
- $baseInstallExit = Invoke-InstallCommand { uv pip install --python $VenvPython --no-deps --upgrade-package unsloth --upgrade-package unsloth-zoo "unsloth>=2026.4.2" unsloth-zoo }
+ $baseInstallExit = Invoke-InstallCommand { uv pip install --python $VenvPython --no-deps --upgrade-package unsloth --upgrade-package unsloth-zoo "unsloth>=2026.4.4" unsloth-zoo }
if ($baseInstallExit -eq 0) {
$NoTorchReq = Find-NoTorchRuntimeFile
if ($NoTorchReq) {
@@ -865,7 +865,7 @@ shell.Run cmd, 0, False
}
}
} elseif ($StudioLocalInstall) {
- $baseInstallExit = Invoke-InstallCommand { uv pip install --python $VenvPython --upgrade-package unsloth "unsloth>=2026.4.2" unsloth-zoo }
+ $baseInstallExit = Invoke-InstallCommand { uv pip install --python $VenvPython --upgrade-package unsloth "unsloth>=2026.4.4" unsloth-zoo }
} else {
$baseInstallExit = Invoke-InstallCommand { uv pip install --python $VenvPython --upgrade-package unsloth "$PackageName" }
}
@@ -886,7 +886,7 @@ shell.Run cmd, 0, False
# Fallback: GPU detection failed to produce a URL -- let uv resolve torch
substep "installing unsloth (this may take a few minutes)..."
if ($StudioLocalInstall) {
- $baseInstallExit = Invoke-InstallCommand { uv pip install --python $VenvPython unsloth-zoo "unsloth>=2026.4.2" --torch-backend=auto }
+ $baseInstallExit = Invoke-InstallCommand { uv pip install --python $VenvPython unsloth-zoo "unsloth>=2026.4.4" --torch-backend=auto }
if ($baseInstallExit -ne 0) {
Write-Host "[ERROR] Failed to install unsloth (exit code $baseInstallExit)" -ForegroundColor Red
return
diff --git a/install.sh b/install.sh
index 053f334d2b..ea53ecc6d6 100755
--- a/install.sh
+++ b/install.sh
@@ -1040,7 +1040,7 @@ if [ "$_MIGRATED" = true ]; then
# to prevent transitive torch resolution.
run_install_cmd "install unsloth (migrated no-torch)" uv pip install --python "$_VENV_PY" --no-deps \
--reinstall-package unsloth --reinstall-package unsloth-zoo \
- "unsloth>=2026.4.2" unsloth-zoo
+ "unsloth>=2026.4.4" unsloth-zoo
_NO_TORCH_RT="$(_find_no_torch_runtime)"
if [ -n "$_NO_TORCH_RT" ]; then
run_install_cmd "install no-torch runtime deps" uv pip install --python "$_VENV_PY" --no-deps -r "$_NO_TORCH_RT"
@@ -1048,7 +1048,7 @@ if [ "$_MIGRATED" = true ]; then
else
run_install_cmd "install unsloth (migrated)" uv pip install --python "$_VENV_PY" \
--reinstall-package unsloth --reinstall-package unsloth-zoo \
- "unsloth>=2026.4.2" unsloth-zoo
+ "unsloth>=2026.4.4" unsloth-zoo
fi
if [ "$STUDIO_LOCAL_INSTALL" = true ]; then
substep "overlaying local repo (editable)..."
@@ -1070,7 +1070,7 @@ elif [ -n "$TORCH_INDEX_URL" ]; then
# runtime deps (typer, safetensors, transformers, etc.) with --no-deps.
run_install_cmd "install unsloth (no-torch)" uv pip install --python "$_VENV_PY" --no-deps \
--upgrade-package unsloth --upgrade-package unsloth-zoo \
- "unsloth>=2026.4.2" unsloth-zoo
+ "unsloth>=2026.4.4" unsloth-zoo
_NO_TORCH_RT="$(_find_no_torch_runtime)"
if [ -n "$_NO_TORCH_RT" ]; then
run_install_cmd "install no-torch runtime deps" uv pip install --python "$_VENV_PY" --no-deps -r "$_NO_TORCH_RT"
@@ -1081,7 +1081,7 @@ elif [ -n "$TORCH_INDEX_URL" ]; then
fi
elif [ "$STUDIO_LOCAL_INSTALL" = true ]; then
run_install_cmd "install unsloth (local)" uv pip install --python "$_VENV_PY" \
- --upgrade-package unsloth "unsloth>=2026.4.2" unsloth-zoo
+ --upgrade-package unsloth "unsloth>=2026.4.4" unsloth-zoo
substep "overlaying local repo (editable)..."
run_install_cmd "overlay local repo" uv pip install --python "$_VENV_PY" -e "$_REPO_ROOT" --no-deps
else
@@ -1092,7 +1092,7 @@ else
# Fallback: GPU detection failed to produce a URL -- let uv resolve torch
substep "installing unsloth (this may take a few minutes)..."
if [ "$STUDIO_LOCAL_INSTALL" = true ]; then
- run_install_cmd "install unsloth (auto torch backend)" uv pip install --python "$_VENV_PY" unsloth-zoo "unsloth>=2026.4.2" --torch-backend=auto
+ run_install_cmd "install unsloth (auto torch backend)" uv pip install --python "$_VENV_PY" unsloth-zoo "unsloth>=2026.4.4" --torch-backend=auto
substep "overlaying local repo (editable)..."
run_install_cmd "overlay local repo" uv pip install --python "$_VENV_PY" -e "$_REPO_ROOT" --no-deps
else
diff --git a/pyproject.toml b/pyproject.toml
index e391b4df3d..50bdf58b95 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -88,7 +88,7 @@ huggingfacenotorch = [
]
huggingface = [
"unsloth[huggingfacenotorch]",
- "unsloth_zoo>=2026.4.2",
+ "unsloth_zoo>=2026.4.3",
"torchvision",
"unsloth[triton]",
]
@@ -578,7 +578,7 @@ colab-ampere-torch220 = [
"flash-attn>=2.6.3 ; ('linux' in sys_platform)",
]
colab-new = [
- "unsloth_zoo>=2026.4.2",
+ "unsloth_zoo>=2026.4.3",
"packaging",
"tyro",
"transformers>=4.51.3,!=4.52.0,!=4.52.1,!=4.52.2,!=4.52.3,!=4.53.0,!=4.54.0,!=4.55.0,!=4.55.1,!=4.57.0,!=4.57.4,!=4.57.5,!=5.0.0,!=5.1.0,<=5.5.0",
diff --git a/studio/frontend/src/features/chat/chat-page.tsx b/studio/frontend/src/features/chat/chat-page.tsx
index 1dbff145ee..cf1ba11d7b 100644
--- a/studio/frontend/src/features/chat/chat-page.tsx
+++ b/studio/frontend/src/features/chat/chat-page.tsx
@@ -225,6 +225,7 @@ const LoraCompareContent = memo(function LoraCompareContent({
modelType="base"
pairId={pairId}
initialThreadId={baseThreadId}
+ syncActiveThreadId={false}
>
@@ -242,6 +243,7 @@ const LoraCompareContent = memo(function LoraCompareContent({
modelType="lora"
pairId={pairId}
initialThreadId={loraThreadId}
+ syncActiveThreadId={false}
>
@@ -343,6 +345,7 @@ const GeneralCompareContent = memo(function GeneralCompareContent({
modelType="model1"
pairId={pairId}
initialThreadId={model1ThreadId}
+ syncActiveThreadId={false}
>
@@ -376,6 +379,7 @@ const GeneralCompareContent = memo(function GeneralCompareContent({
modelType="model2"
pairId={pairId}
initialThreadId={model2ThreadId}
+ syncActiveThreadId={false}
>
@@ -479,11 +483,19 @@ function TopBarActions({
);
}
+function getInitialSingleChatView(): ChatView {
+ const id = useChatRuntimeStore.getState().activeThreadId;
+ if (typeof id === "string" && id.length > 0 && !id.startsWith("__LOCALID_")) {
+ return { mode: "single", threadId: id };
+ }
+ return { mode: "single" };
+}
+
export function ChatPage(): ReactElement {
- const [view, setView] = useState({
- mode: "single",
- newThreadNonce: crypto.randomUUID(),
- });
+ // Do not set newThreadNonce here: each /chat mount would run ThreadNewChatSwitch
+ // and create spurious threads when navigating (e.g. Recipes / Export). New Chat
+ // explicitly sets a nonce in handleNewThread.
+ const [view, setView] = useState(getInitialSingleChatView);
const [settingsOpen, setSettingsOpen] = useState(false);
const [modelSelectorOpen, setModelSelectorOpen] = useState(false);
const [modelSelectorLocked, setModelSelectorLocked] = useState(false);
@@ -587,9 +599,20 @@ export function ChatPage(): ReactElement {
void ejectModel();
}, [ejectModel]);
const handleNewThread = useCallback(() => {
+ // Skip if we are already on a fresh unsaved draft with no messages sent.
+ // Once the user sends a message, append() sets activeThreadId in the store,
+ // so we check the store to know whether the current draft has been sent.
+ if (
+ view.mode === "single" &&
+ !view.threadId &&
+ !useChatRuntimeStore.getState().activeThreadId
+ ) {
+ return;
+ }
+
useChatRuntimeStore.getState().setActiveThreadId(null);
setView({ mode: "single", newThreadNonce: crypto.randomUUID() });
- }, []);
+ }, [view]);
const handleNewCompare = useCallback(() => {
setView({ mode: "compare", pairId: crypto.randomUUID() });
// Clear activeThreadId so compare panes do not inherit the single-chat
@@ -922,7 +945,7 @@ export function ChatPage(): ReactElement {
{view.mode === "single" ? (
diff --git a/studio/frontend/src/features/chat/runtime-provider.tsx b/studio/frontend/src/features/chat/runtime-provider.tsx
index 7e6cd8e1dd..024543edb9 100644
--- a/studio/frontend/src/features/chat/runtime-provider.tsx
+++ b/studio/frontend/src/features/chat/runtime-provider.tsx
@@ -596,6 +596,15 @@ function ThreadHistoryProvider({
async append({ parentId, message }: ExportedMessageRepositoryItem) {
const { remoteId } = await aui.threadListItem().initialize();
+ // Keep single-chat runtime state in sync once a new chat is first
+ // persisted. Compare panes intentionally do not write global activeThreadId.
+ const thread = await db.threads.get(remoteId);
+ if (thread?.modelType === "base" && !thread.pairId) {
+ const store = useChatRuntimeStore.getState();
+ if (store.activeThreadId !== remoteId) {
+ store.setActiveThreadId(remoteId);
+ }
+ }
const content = cloneContent(message.content);
const attachments =
message.role === "user" ? cloneAttachments(message.attachments) : [];
@@ -658,7 +667,11 @@ function useRuntimeHook(): ReturnType {
function ThreadAutoSwitch({
threadId,
-}: { threadId: string }): ReactElement | null {
+ syncActiveThreadId = true,
+}: {
+ threadId: string;
+ syncActiveThreadId?: boolean;
+}): ReactElement | null {
const aui = useAui();
const isLoading = useAuiState(({ threads }) => threads.isLoading);
const mainThreadId = useAuiState(({ threads }) => threads.mainThreadId);
@@ -669,6 +682,13 @@ function ThreadAutoSwitch({
}
}, [aui, isLoading, mainThreadId, threadId]);
+ useEffect(() => {
+ if (!syncActiveThreadId || isLoading || mainThreadId !== threadId) {
+ return;
+ }
+ useChatRuntimeStore.getState().setActiveThreadId(threadId);
+ }, [isLoading, mainThreadId, syncActiveThreadId, threadId]);
+
return null;
}
@@ -682,30 +702,10 @@ function ThreadNewChatSwitch({
if (isLoading) {
return;
}
-
- let cancelled = false;
- // Clear immediately so the adapter never picks up a stale thread ID
- // from a previous chat while we initialize the new one.
+ // Switch to a fresh local thread without persisting it yet.
+ // Persistence still happens on first message append.
+ void aui.threads().switchToNewThread();
useChatRuntimeStore.getState().setActiveThreadId(null);
-
- void (async () => {
- try {
- aui.threads().switchToNewThread();
- const { remoteId } = await aui.threadListItem().initialize();
- if (!cancelled) {
- useChatRuntimeStore.getState().setActiveThreadId(remoteId);
- }
- } catch (error) {
- if (!cancelled) {
- useChatRuntimeStore.getState().setActiveThreadId(null);
- }
- console.error("Failed to initialize new chat thread", error);
- }
- })();
-
- return () => {
- cancelled = true;
- };
}, [aui, isLoading, nonce]);
return null;
@@ -733,12 +733,14 @@ export function ChatRuntimeProvider({
pairId,
initialThreadId,
newThreadNonce,
+ syncActiveThreadId = true,
}: {
children: ReactNode;
modelType?: ModelType;
pairId?: string;
initialThreadId?: string;
newThreadNonce?: string;
+ syncActiveThreadId?: boolean;
}): ReactElement {
const runtime = useRemoteThreadListRuntime({
runtimeHook: useRuntimeHook,
@@ -754,8 +756,15 @@ export function ChatRuntimeProvider({
return (
-
- {initialThreadId && }
+
+ {initialThreadId && (
+
+ )}
{!initialThreadId && newThreadNonce && (
)}
diff --git a/studio/frontend/src/features/chat/thread-sidebar.tsx b/studio/frontend/src/features/chat/thread-sidebar.tsx
index ba97d2ee6e..62246cdad5 100644
--- a/studio/frontend/src/features/chat/thread-sidebar.tsx
+++ b/studio/frontend/src/features/chat/thread-sidebar.tsx
@@ -22,6 +22,7 @@ import {
} from "@hugeicons/core-free-icons";
import { HugeiconsIcon } from "@hugeicons/react";
import { db, useLiveQuery } from "./db";
+import { useChatRuntimeStore } from "./stores/chat-runtime-store";
import type { ChatView, ThreadRecord } from "./types";
interface SidebarItem {
@@ -76,12 +77,17 @@ export function ThreadSidebar({
onNewCompare: () => void;
showCompare: boolean;
}) {
- const allThreads = useLiveQuery(
- () => db.threads.orderBy("createdAt").reverse().toArray(),
- [],
- );
+ const allThreads = useLiveQuery(async () => {
+ const threadIdsWithMessage = new Set(
+ (await db.messages.orderBy("threadId").uniqueKeys()) as string[],
+ );
+ const rows = await db.threads.orderBy("createdAt").reverse().toArray();
+ return rows.filter((t) => !t.archived && threadIdsWithMessage.has(t.id));
+ }, []);
const items = groupThreads(allThreads ?? []);
- const activeId = view.mode === "single" ? view.threadId : view.pairId;
+ const storeThreadId = useChatRuntimeStore((s) => s.activeThreadId);
+ const activeId =
+ view.mode === "single" ? (view.threadId ?? storeThreadId) : view.pairId;
function viewForItem(item: SidebarItem): ChatView {
return item.type === "single"
@@ -101,7 +107,11 @@ export function ThreadSidebar({
}
}
if (activeId === item.id) {
- onSelect({ mode: "single" });
+ // Directly set a new view with a nonce rather than going through
+ // onNewThread(), which may return early if the guard sees no
+ // threadId and no activeThreadId (after we just cleared it).
+ useChatRuntimeStore.getState().setActiveThreadId(null);
+ onSelect({ mode: "single", newThreadNonce: crypto.randomUUID() });
}
}
diff --git a/unsloth/models/_utils.py b/unsloth/models/_utils.py
index dcb7334417..d8fe92739a 100644
--- a/unsloth/models/_utils.py
+++ b/unsloth/models/_utils.py
@@ -12,7 +12,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
-__version__ = "2026.4.2"
+__version__ = "2026.4.4"
__all__ = [
"SUPPORTS_BFLOAT16",