From 19ae0737360ce90b9d64717f36a5755269494057 Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Sun, 14 Jun 2026 04:48:37 -0700 Subject: [PATCH 001/233] 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 && (