diff --git a/studio/frontend/src/features/chat/runtime-provider.tsx b/studio/frontend/src/features/chat/runtime-provider.tsx index d01383b309..64d3f8eed0 100644 --- a/studio/frontend/src/features/chat/runtime-provider.tsx +++ b/studio/frontend/src/features/chat/runtime-provider.tsx @@ -1008,17 +1008,51 @@ function ThreadNewChatSwitch({ function ActiveThreadSync({ enabled, }: { enabled: boolean }): ReactElement | null { + const aui = useAui(); + const isLoading = useAuiState(({ threads }) => threads.isLoading); const mainThreadId = useAuiState(({ threads }) => threads.mainThreadId); const setActiveThreadId = useChatRuntimeStore( (state) => state.setActiveThreadId, ); + // One-shot guard: we only attempt to re-adopt a persisted draft on the + // first settled render after a page load, never again for the lifetime + // of this provider (so user-driven new-chat / switches aren't fought). + const restoreAttemptedRef = useRef(false); useEffect(() => { - if (!enabled) { + if (!enabled || isLoading || !mainThreadId) { return; } - setActiveThreadId(mainThreadId ?? null); - }, [enabled, mainThreadId, setActiveThreadId]); + const persisted = useChatRuntimeStore.getState().activeThreadId; + // On a fresh page load aui mints a new `__LOCALID_*` draft. If we have + // a different persisted draft id, ask aui to switch to it so any RAG + // docs uploaded under that id reattach. The thread was persisted to + // the backend when its first doc/message ran initialize(), so the + // adapter's fetch() can resolve it. If it can't (a draft that never + // got a doc/message), switchToThread rejects and we fall back to the + // fresh draft. + if ( + !restoreAttemptedRef.current + && persisted + && persisted !== mainThreadId + && persisted.startsWith("__LOCALID_") + && mainThreadId.startsWith("__LOCALID_") + ) { + restoreAttemptedRef.current = true; + const result = aui.threads().switchToThread(persisted) as unknown; + if ( + result + && typeof (result as Promise).catch === "function" + ) { + void (result as Promise).catch(() => { + setActiveThreadId(mainThreadId); + }); + } + return; + } + restoreAttemptedRef.current = true; + setActiveThreadId(mainThreadId); + }, [aui, enabled, isLoading, mainThreadId, setActiveThreadId]); return null; } diff --git a/studio/frontend/src/features/chat/stores/chat-runtime-store.ts b/studio/frontend/src/features/chat/stores/chat-runtime-store.ts index 1d1df695eb..ad89d52249 100644 --- a/studio/frontend/src/features/chat/stores/chat-runtime-store.ts +++ b/studio/frontend/src/features/chat/stores/chat-runtime-store.ts @@ -36,6 +36,7 @@ export const CHAT_RAG_TOOL_ENABLED_KEY = "unsloth_chat_rag_tool_enabled"; // localStorage persistence here the user's external pick is silently // reset to the default on every page refresh. const LAST_EXTERNAL_CHECKPOINT_KEY = "unsloth_chat_last_external_checkpoint"; +const CHAT_ACTIVE_THREAD_KEY = "unsloth_chat_active_thread_id"; function loadLastExternalCheckpoint(): string | null { if (typeof window === "undefined") return null; @@ -610,7 +611,11 @@ export const useChatRuntimeStore = create((set, get) => ({ defaultChatTemplate: null, chatTemplateOverride: null, loadedChatTemplateOverride: null, - activeThreadId: null, + // Persisted so a draft thread's RAG docs reattach after a page reload. + // On reload ActiveThreadSync asks assistant-ui to switch to this id + // (the thread was persisted to the backend when its first doc/message + // initialized it), keeping aui's mainThreadId and this value unified. + activeThreadId: loadString(CHAT_ACTIVE_THREAD_KEY, "") || null, settingsPanelOpen: false, pendingAudioBase64: null, pendingAudioName: null, @@ -755,8 +760,10 @@ export const useChatRuntimeStore = create((set, get) => ({ activeGgufVariant: ggufVariant ?? null, }; }), - setActiveThreadId: (activeThreadId) => - set({ activeThreadId, contextUsage: null }), + setActiveThreadId: (activeThreadId) => { + saveString(CHAT_ACTIVE_THREAD_KEY, activeThreadId ?? ""); + set({ activeThreadId, contextUsage: null }); + }, setSettingsPanelOpen: (settingsPanelOpen) => set({ settingsPanelOpen }), clearCheckpoint: () => { // Mirror setCheckpoint's persistence behavior: dropping the