fix(chat): preserve thread history across page reloads
Two bugs, one symptom (every reload silently spawned a blank thread, the previous conversation was orphaned in the sidebar): 1. Race in the auto-thread effect. On mount: threads = [], activeThread = null, /api/chat/threads fetch in flight. The auto-create effect re-ran the moment the workspace + auth resolved, saw threads.length === 0, and called newThread() before the fetch ever returned. When the historical threads finally landed, activeThread was already pinned to the new empty one. Gate on a `threadsLoaded` flag that flips true after the first loadThreads() resolves. Auto-create can no longer fire before history is known. 2. activeThread wasn't persisted. Even with the race fixed, refreshing the page would reset the sidebar to the top thread (most recently updated). After a deploy that's usually the brand-new empty thread we just spawned, not the conversation the user was actually in. Persist activeThread to localStorage keyed by workspace. Reload restores the same thread; switching workspaces resets cleanly. Made-with: Cursor
This commit is contained in:
@@ -143,6 +143,10 @@ export function ChatPanel() {
|
||||
return localStorage.getItem("vibn-chat-open") !== "false";
|
||||
});
|
||||
const [threads, setThreads] = useState<Thread[]>([]);
|
||||
// threadsLoaded flips to true after the FIRST loadThreads() resolves.
|
||||
// Used to gate the auto-create effect — without it we race the fetch
|
||||
// and spawn an empty thread before history loads.
|
||||
const [threadsLoaded, setThreadsLoaded] = useState(false);
|
||||
const [activeThread, setActiveThread] = useState<string | null>(null);
|
||||
const [messages, setMessages] = useState<Message[]>([]);
|
||||
const [toolEvents, setToolEvents] = useState<ToolEvent[]>([]);
|
||||
@@ -188,7 +192,9 @@ export function ChatPanel() {
|
||||
const res = await fetch(`/api/chat/threads?workspace=${encodeURIComponent(workspace)}`);
|
||||
const data = await res.json();
|
||||
setThreads(data.threads || []);
|
||||
} catch { /* silent */ }
|
||||
} catch { /* silent */ } finally {
|
||||
setThreadsLoaded(true);
|
||||
}
|
||||
}, [workspace, status]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -225,14 +231,34 @@ export function ChatPanel() {
|
||||
} catch { /* silent */ }
|
||||
}, []);
|
||||
|
||||
// Auto-create first thread
|
||||
// Auto-resume previous thread (or create a fresh one if the user has
|
||||
// never chatted in this workspace). We MUST wait for threadsLoaded
|
||||
// before deciding — otherwise we race the fetch and spawn an empty
|
||||
// thread before history arrives. Last-active thread is restored from
|
||||
// localStorage so a page reload (deploy, refresh) lands the user back
|
||||
// in the conversation they were in.
|
||||
useEffect(() => {
|
||||
if (open && status === "authenticated" && workspace && threads.length === 0 && !activeThread) {
|
||||
if (!open || status !== "authenticated" || !workspace) return;
|
||||
if (!threadsLoaded) return;
|
||||
if (activeThread) return;
|
||||
|
||||
if (threads.length === 0) {
|
||||
newThread();
|
||||
} else if (open && threads.length > 0 && !activeThread) {
|
||||
loadThread(threads[0].id);
|
||||
return;
|
||||
}
|
||||
}, [open, status, workspace, threads.length, activeThread, newThread, loadThread]);
|
||||
|
||||
const savedKey = `vibn-chat-active-thread:${workspace}`;
|
||||
const saved = typeof window !== "undefined" ? localStorage.getItem(savedKey) : null;
|
||||
const target = saved && threads.some((t) => t.id === saved) ? saved : threads[0].id;
|
||||
loadThread(target);
|
||||
}, [open, status, workspace, threadsLoaded, threads, activeThread, newThread, loadThread]);
|
||||
|
||||
// Persist active thread so reload re-opens the same conversation.
|
||||
useEffect(() => {
|
||||
if (typeof window === "undefined" || !workspace) return;
|
||||
const savedKey = `vibn-chat-active-thread:${workspace}`;
|
||||
if (activeThread) localStorage.setItem(savedKey, activeThread);
|
||||
}, [activeThread, workspace]);
|
||||
|
||||
useEffect(() => { scrollToBottom(); }, [messages, toolEvents, scrollToBottom]);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user