From 4c83e3540ec878db055d35583fa83a3415acc957 Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Mon, 6 Apr 2026 09:20:17 -0700 Subject: [PATCH 1/4] Update --- pyproject.toml | 4 ++-- unsloth/models/_utils.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) 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/unsloth/models/_utils.py b/unsloth/models/_utils.py index dcb7334417..9ee2ade3db 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.3" __all__ = [ "SUPPORTS_BFLOAT16", From 8c89b84bb678659139e2530cc56ca291583828c7 Mon Sep 17 00:00:00 2001 From: Lee Jackson <130007945+Imagineer99@users.noreply.github.com> Date: Mon, 6 Apr 2026 17:32:54 +0100 Subject: [PATCH 2/4] Studio: Fix empty chat threads on navigation and stabilize new chat flow (#4872) * fix(chat): prevent implicit empty thread creation and stabilize new-chat flow * fix(chat): harden compare thread sync and simplify sidebar thread query * fix(chat): harden new-thread state sync and isolate compare active thread updates * fix(chat): stabilize new-thread state sync and prevent compare/session bleed * Fix thread restoration, handleNewThread guard, sidebar filter, and delete flow - Remove __LOCALID_ filter from getInitialSingleChatView: in this Dexie-backed adapter, AUI's __LOCALID_ prefixed IDs ARE the real persistent thread IDs stored by initialize(). Filtering them out breaks thread restoration on navigation. - Simplify handleNewThread to synchronous: the async Dexie message check is redundant (persistence is already deferred to first append) and strands users on legacy empty threads. Use a simple guard that checks the store's activeThreadId to detect unsent drafts. - Add message-count filter to sidebar: filter threads to only show those with at least one message, hiding legacy empty threads. - Add store-based sidebar highlighting fallback: use activeThreadId from the store when view.threadId is not set (nonce-backed chats). - Fix handleDelete to call onNewThread() instead of onSelect(), and clear activeThreadId, so the runtime properly resets after deleting the active thread. * Fix handleDelete nonce path and restore __LOCALID_ filter handleDelete was calling onNewThread() after clearing activeThreadId, but the handleNewThread guard sees !view.threadId && !activeThreadId and returns early, leaving the UI stuck on the deleted thread. Fix by directly calling onSelect with a new nonce instead. Restore __LOCALID_ filter in getInitialSingleChatView to prevent restoring unpersisted AUI local thread IDs on navigation. Without this filter, navigating away from /chat before sending a message would restore a non-existent thread that Dexie cannot fetch. --------- Co-authored-by: Daniel Han --- .../frontend/src/features/chat/chat-page.tsx | 35 +++++++++-- .../src/features/chat/runtime-provider.tsx | 61 +++++++++++-------- .../src/features/chat/thread-sidebar.tsx | 22 +++++-- 3 files changed, 80 insertions(+), 38 deletions(-) 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() }); } } From b295daf9323d2782fe7d1286b2e1bde89dc6cd29 Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Mon, 6 Apr 2026 09:39:06 -0700 Subject: [PATCH 3/4] Update _utils.py --- unsloth/models/_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/unsloth/models/_utils.py b/unsloth/models/_utils.py index 9ee2ade3db..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.3" +__version__ = "2026.4.4" __all__ = [ "SUPPORTS_BFLOAT16", From 1d8160376e169d13c386b7ef4bc1fdc8f855de68 Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Mon, 6 Apr 2026 09:46:35 -0700 Subject: [PATCH 4/4] Bump minimum unsloth version to 2026.4.4 in install scripts (#4876) --- install.ps1 | 10 +++++----- install.sh | 10 +++++----- 2 files changed, 10 insertions(+), 10 deletions(-) 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