From 8febe2ca8f3797094f9742882cb96a7f67ddde4c Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Sat, 13 Jun 2026 04:56:38 -0700 Subject: [PATCH 01/93] Bump install.sh / install.ps1 pin to unsloth>=2026.6.7 (#6301) --- install.ps1 | 10 +++++----- install.sh | 10 +++++----- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/install.ps1 b/install.ps1 index 88939ce627..dccd8e4fc1 100644 --- a/install.ps1 +++ b/install.ps1 @@ -1923,7 +1923,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.6.6" unsloth-zoo } + $baseInstallExit = Invoke-InstallCommand { uv pip install --python $VenvPython --no-deps --reinstall-package unsloth --reinstall-package unsloth-zoo "unsloth>=2026.6.7" unsloth-zoo } if ($baseInstallExit -eq 0) { # Resolve pydantic WITH deps so pip pins pydantic-core # to the matching version (no-torch-runtime.txt below @@ -1937,7 +1937,7 @@ shell.Run cmd, 0, False } } } else { - $baseInstallExit = Invoke-InstallCommand { uv pip install --python $VenvPython --reinstall-package unsloth --reinstall-package unsloth-zoo "unsloth>=2026.6.6" unsloth-zoo } + $baseInstallExit = Invoke-InstallCommand { uv pip install --python $VenvPython --reinstall-package unsloth --reinstall-package unsloth-zoo "unsloth>=2026.6.7" unsloth-zoo } } if ($baseInstallExit -ne 0) { Write-Host "[ERROR] Failed to install unsloth (exit code $baseInstallExit)" -ForegroundColor Red @@ -1984,7 +1984,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.6.6" unsloth-zoo } + $baseInstallExit = Invoke-InstallCommand { uv pip install --python $VenvPython --no-deps --upgrade-package unsloth --upgrade-package unsloth-zoo "unsloth>=2026.6.7" unsloth-zoo } if ($baseInstallExit -eq 0) { # Same pydantic-with-deps trick as the migrated branch. $baseInstallExit = Invoke-InstallCommand { uv pip install --python $VenvPython pydantic } @@ -1996,7 +1996,7 @@ shell.Run cmd, 0, False } } } elseif ($StudioLocalInstall) { - $baseInstallExit = Invoke-InstallCommand { uv pip install --python $VenvPython --upgrade-package unsloth "unsloth>=2026.6.6" unsloth-zoo } + $baseInstallExit = Invoke-InstallCommand { uv pip install --python $VenvPython --upgrade-package unsloth "unsloth>=2026.6.7" unsloth-zoo } } else { $baseInstallExit = Invoke-InstallCommand { uv pip install --python $VenvPython --upgrade-package unsloth -- "$PackageName" } } @@ -2024,7 +2024,7 @@ shell.Run cmd, 0, False Write-TauriLog "STEP" "Installing unsloth" substep "installing unsloth (this may take a few minutes)..." if ($StudioLocalInstall) { - $baseInstallExit = Invoke-InstallCommand { uv pip install --python $VenvPython unsloth-zoo "unsloth>=2026.6.6" --torch-backend=auto } + $baseInstallExit = Invoke-InstallCommand { uv pip install --python $VenvPython unsloth-zoo "unsloth>=2026.6.7" --torch-backend=auto } if ($baseInstallExit -ne 0) { Write-Host "[ERROR] Failed to install unsloth (exit code $baseInstallExit)" -ForegroundColor Red return (Exit-InstallFailure "Failed to install unsloth (exit code $baseInstallExit)" $baseInstallExit) diff --git a/install.sh b/install.sh index 05b8e1e92b..5e08680628 100755 --- a/install.sh +++ b/install.sh @@ -2422,7 +2422,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.6.6" unsloth-zoo + "unsloth>=2026.6.7" unsloth-zoo # Resolve pydantic WITH deps so pip pins pydantic-core to the # matching version (no-torch-runtime.txt below is --no-deps). # All transitive deps are torch-free. @@ -2435,7 +2435,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.6.6" unsloth-zoo + "unsloth>=2026.6.7" unsloth-zoo fi if [ "$STUDIO_LOCAL_INSTALL" = true ]; then substep "overlaying local repo (editable)..." @@ -2639,7 +2639,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.6.6" unsloth-zoo + "unsloth>=2026.6.7" unsloth-zoo # Same pydantic-with-deps trick as the migrated branch. run_install_cmd "install pydantic (with deps for compatible core)" \ uv pip install --python "$_VENV_PY" pydantic @@ -2657,7 +2657,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.6.6" unsloth-zoo + --upgrade-package unsloth "unsloth>=2026.6.7" unsloth-zoo substep "overlaying local repo (editable)..." run_install_cmd "overlay local repo" uv pip install --python "$_VENV_PY" -e "$_REPO_ROOT" --no-deps substep "overlaying unsloth-zoo from git main..." @@ -2689,7 +2689,7 @@ else tauri_log "STEP" "Installing Unsloth" 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.6.6" --torch-backend=auto + run_install_cmd "install unsloth (auto torch backend)" uv pip install --python "$_VENV_PY" unsloth-zoo "unsloth>=2026.6.7" --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 substep "overlaying unsloth-zoo from git main..." From a3ca5d2a5a1e261b7ac56df74237c03f934ce857 Mon Sep 17 00:00:00 2001 From: oobabooga Date: Sun, 14 Jun 2026 02:06:31 -0300 Subject: [PATCH 02/93] Studio: don't silently fall back to a CPU prebuilt on NVIDIA Linux GPU hosts (#6310) --- studio/install_llama_prebuilt.py | 12 +++-- tests/studio/install/test_selection_logic.py | 53 ++++++++++++++++++++ 2 files changed, 61 insertions(+), 4 deletions(-) diff --git a/studio/install_llama_prebuilt.py b/studio/install_llama_prebuilt.py index aab6d641fe..b33d0b6325 100644 --- a/studio/install_llama_prebuilt.py +++ b/studio/install_llama_prebuilt.py @@ -5932,9 +5932,9 @@ def resolve_install_attempts( def _linux_published_attempts(host: HostInfo, bundle: PublishedReleaseBundle) -> list[AssetChoice]: """Build the install attempts for a fork Linux host from a manifest-described - bundle: CUDA (with a CPU fallback), per-gfx ROCm, or CPU. Same selection the - upstream filename path used, just sourced from the manifest instead of - reconstructed from asset names.""" + bundle: CUDA, per-gfx ROCm, or (non-GPU) CPU. Same selection the upstream + filename path used, just sourced from the manifest instead of reconstructed + from asset names.""" attempts: list[AssetChoice] = [] if host.has_usable_nvidia: # Prefer the cudart major Studio loads at runtime (torch's bundled @@ -5949,7 +5949,7 @@ def _linux_published_attempts(host: HostInfo, bundle: PublishedReleaseBundle) -> ) if selection is not None: attempts.extend(selection.attempts) - if host.has_rocm and not host.has_usable_nvidia: + elif host.has_rocm: # Use the fork's own per-gfx ROCm bundle (hash-approved, ships the full # ROCm runtime). Do NOT append the CPU asset for ROCm-only hosts: if no # bundle covers the GPU we want validate_prebuilt_attempts to raise @@ -5959,6 +5959,10 @@ def _linux_published_attempts(host: HostInfo, bundle: PublishedReleaseBundle) -> if published_rocm is not None: attempts.append(published_rocm) else: + # CPU-only host. A usable-NVIDIA host never reaches here -- if its CUDA + # selection produced nothing we want an empty attempt list so the caller + # source-builds with CUDA, not a CPU-only binary silently installed on a + # GPU host (mirrors the ROCm branch, and Windows NVIDIA). cpu_choice = published_asset_choice_for_kind(bundle, "linux-cpu") if cpu_choice is not None: attempts.append(cpu_choice) diff --git a/tests/studio/install/test_selection_logic.py b/tests/studio/install/test_selection_logic.py index 4e8beeb888..0282370fe0 100644 --- a/tests/studio/install/test_selection_logic.py +++ b/tests/studio/install/test_selection_logic.py @@ -2611,6 +2611,59 @@ class TestDirectLinuxNvidiaCpuGate: assert [a.install_kind for a in plan.attempts] == ["linux-cpu"] +class TestLinuxPublishedAttemptsNvidiaCpuGate: + """Live fork-manifest path (_linux_published_attempts): an NVIDIA host whose + CUDA selection finds nothing must NOT be handed the manifest's CPU bundle -- + the attempt list stays empty so the caller source-builds with CUDA instead of + silently installing a CPU-only binary on a GPU host. CPU-only hosts still get + the CPU bundle. Mirrors the ROCm policy and TestDirectLinuxNvidiaCpuGate (the + latter covers direct_linux_release_plan, which is off the live path, this the + live path).""" + + def _cpu_only_bundle(self): + return make_release( + [ + make_artifact( + "app-b8508-linux-x64-cpu.tar.gz", + install_kind = "linux-cpu", + runtime_line = None, + coverage_class = None, + supported_sms = [], + min_sm = None, + max_sm = None, + bundle_profile = None, + rank = 1000, + ), + ] + ) + + def test_nvidia_host_without_cuda_line_gets_no_cpu_attempt(self, monkeypatch): + monkeypatch.setattr( + INSTALL_LLAMA_PREBUILT, + "detect_torch_cuda_runtime_preference", + lambda host: CudaRuntimePreference(runtime_line = None, selection_log = []), + ) + monkeypatch.setattr( + INSTALL_LLAMA_PREBUILT, + "detected_linux_runtime_lines", + lambda: (["cuda13"], {"cuda13": ["/usr/local/cuda/lib64"]}), + ) + host = make_host(driver_cuda_version = (13, 1), compute_caps = ["100"]) + attempts = INSTALL_LLAMA_PREBUILT._linux_published_attempts(host, self._cpu_only_bundle()) + assert attempts == [] + + def test_cpu_host_gets_cpu_attempt(self): + host = make_host( + nvidia_smi = None, + driver_cuda_version = None, + compute_caps = [], + has_physical_nvidia = False, + has_usable_nvidia = False, + ) + attempts = INSTALL_LLAMA_PREBUILT._linux_published_attempts(host, self._cpu_only_bundle()) + assert [a.install_kind for a in attempts] == ["linux-cpu"] + + # =========================================================================== # N.1d. published_windows_cuda_attempts -- version-dynamic ordering seed # =========================================================================== From 19ae0737360ce90b9d64717f36a5755269494057 Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Sun, 14 Jun 2026 04:48:37 -0700 Subject: [PATCH 03/93] Studio: clarify llama.cpp update banner copy (#6313) Rename the banner heading from "New llama.cpp version" to "New llama.cpp update", and add a small hint under the version line ("No restart needed after update"). Tighten the gap above the action row so the hint and the buttons read as one group. --- studio/frontend/src/components/llama-update-banner.tsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/studio/frontend/src/components/llama-update-banner.tsx b/studio/frontend/src/components/llama-update-banner.tsx index 1aa9fc4e60..8c8fcc3646 100644 --- a/studio/frontend/src/components/llama-update-banner.tsx +++ b/studio/frontend/src/components/llama-update-banner.tsx @@ -172,7 +172,7 @@ export function LlamaUpdateBanner({ />

- {applying ? "Updating llama.cpp..." : "New llama.cpp version"} + {applying ? "Updating llama.cpp..." : "New llama.cpp update"}

{status?.installed_tag ?? "unknown"} →{" "} @@ -180,6 +180,9 @@ export function LlamaUpdateBanner({ {status?.latest_tag ?? ""}

+

+ No restart needed after update +

@@ -203,7 +206,7 @@ export function LlamaUpdateBanner({ /> ) : ( -
+
+ {incognito && ( +

+ This chat won't appear in your history and isn't saved. It + disappears when you leave. +

+ )} {!hideComposer && }
@@ -2089,6 +2096,7 @@ const ComposerToolsMenu: FC<{ side?: "top" | "bottom" }> = ({ const [newProjectOpen, setNewProjectOpen] = useState(false); const [promptStorageOpen, setPromptStorageOpen] = useState(false); const activeThreadId = useChatRuntimeStore((s) => s.activeThreadId); + const incognito = useChatRuntimeStore((s) => s.incognito); const aui = useAui(); const composerCanAddAttachments = useAuiState( ({ composer }) => composer.isEditing, @@ -2124,8 +2132,9 @@ const ComposerToolsMenu: FC<{ side?: "top" | "bottom" }> = ({ }; input.click(); }, [aui, audioAttachmentsEnabled]); - // Disable Export chat until the thread has content. + // Exports are storage-backed; temporary chats intentionally never write there. const messageCount = useAuiState(({ thread }) => thread.messages.length); + const exportDisabled = incognito || !activeThreadId || messageCount === 0; const { startQueue } = useContext(PromptQueueContext); const plusPins = usePlusMenuPrefsStore((s) => s.pins); @@ -2214,7 +2223,7 @@ const ComposerToolsMenu: FC<{ side?: "top" | "bottom" }> = ({ ), exportChat: ( - + Export chat diff --git a/studio/frontend/src/features/chat/chat-page.tsx b/studio/frontend/src/features/chat/chat-page.tsx index 0a2809c9d4..8262a52eaa 100644 --- a/studio/frontend/src/features/chat/chat-page.tsx +++ b/studio/frontend/src/features/chat/chat-page.tsx @@ -31,6 +31,7 @@ import { GuidedTour, useGuidedTourController } from "@/features/tour"; import { isTauri } from "@/lib/api-base"; import { cn } from "@/lib/utils"; import { + BubbleChatTemporaryIcon, Folder02Icon, LayoutAlignRightIcon, } from "@hugeicons/core-free-icons"; @@ -111,6 +112,7 @@ import { listStoredChatMessages, listStoredChatThreads, } from "./utils/chat-history-storage"; +import { isAssistantLocalThreadId } from "./utils/thread-ids"; type LoraCandidate = { id: string; @@ -160,12 +162,6 @@ function pickBestLoraForBase( return partial ?? sorted[0] ?? null; } -function isAssistantLocalThreadId( - threadId: string | null | undefined, -): boolean { - return Boolean(threadId?.startsWith("__LOCALID_")); -} - function messageHasImage(message: MessageRecord): boolean { const contentParts = Array.isArray(message.content) ? message.content : []; if (contentParts.some((part) => part.type === "image")) { @@ -1028,6 +1024,30 @@ export function ChatPage(): ReactElement { const settingsOpen = useChatRuntimeStore((s) => s.settingsPanelOpen); const setSettingsOpen = useChatRuntimeStore((s) => s.setSettingsPanelOpen); + const incognito = useChatRuntimeStore((s) => s.incognito); + const setIncognito = useChatRuntimeStore((s) => s.setIncognito); + const incognitoLabel = incognito + ? "Turn off temporary chat" + : "Turn on temporary chat"; + const toggleIncognito = useCallback(() => { + const store = useChatRuntimeStore.getState(); + store.setIncognito(!store.incognito); + // On an empty scratch chat there's nothing to abandon, so flip in + // place: navigating would remount the thread and bounce the composer + // (it docks to the bottom before the welcome state re-centers it). + // Otherwise start a clean chat so the temporary session can't inherit + // or leave behind a persisted thread (matches ChatGPT / Gemini). + const onEmptyScratchChat = + !search.thread && + !search.compare && + !search.project && + store.activeThreadId == null; + if (onEmptyScratchChat) return; + // setActiveThreadId already clears contextUsage. + store.setActiveThreadId(null); + store.setActiveProjectId(null); + navigate({ to: "/chat", search: { new: crypto.randomUUID() } }); + }, [navigate, search]); const hydratePersistedSettings = useChatRuntimeStore( (s) => s.hydratePersistedSettings, ); @@ -1441,6 +1461,17 @@ export function ChatPage(): ReactElement { currentProjectId, ]); + // Temporary chat only applies to a fresh single-view chat. Exit incognito + // when we land on anything else (compare, a project, or an existing thread + // via sidebar/deep link/back), so the toggle isn't stranded and the UI + // never implies a saved thread is temporary. + useEffect(() => { + const onFreshSingleChat = view.mode === "single" && !view.threadId; + if (incognito && !onFreshSingleChat) { + setIncognito(false); + } + }, [view, incognito, setIncognito]); + const selectedArtifact = useSelectedChatArtifact(); const artifactSurface = useChatArtifactsStore((state) => state.surface); const closeArtifactSurface = useChatArtifactsStore( @@ -2121,6 +2152,16 @@ export function ChatPage(): ReactElement { className="max-w-[62vw] !pr-3 sm:max-w-none !h-[34px]" /> )} + {incognito && view.mode === "single" && ( +
+ + Temporary +
+ )} {view.mode !== "compare" && currentProjectId && (