diff --git a/frontend/src/app/workspace/agents/[agent_name]/chats/[thread_id]/page.tsx b/frontend/src/app/workspace/agents/[agent_name]/chats/[thread_id]/page.tsx index d0b386153..7b288a40d 100644 --- a/frontend/src/app/workspace/agents/[agent_name]/chats/[thread_id]/page.tsx +++ b/frontend/src/app/workspace/agents/[agent_name]/chats/[thread_id]/page.tsx @@ -41,20 +41,22 @@ export default function AgentChatPage() { const { agent } = useAgent(agent_name); - const { threadId, isNewThread, setIsNewThread } = useThreadChat(); + const { threadId, setThreadId, isNewThread, setIsNewThread } = + useThreadChat(); const [settings, setSettings] = useThreadSettings(threadId); const { showNotification } = useNotification(); const [thread, sendMessage] = useThreadStream({ threadId: isNewThread ? undefined : threadId, context: { ...settings.context, agent_name: agent_name }, - onStart: () => { + onStart: (createdThreadId) => { + setThreadId(createdThreadId); setIsNewThread(false); // ! Important: Never use next.js router for navigation in this case, otherwise it will cause the thread to re-mount and lose all states. Use native history API instead. history.replaceState( null, "", - `/workspace/agents/${agent_name}/chats/${threadId}`, + `/workspace/agents/${agent_name}/chats/${createdThreadId}`, ); }, onFinish: (state) => { diff --git a/frontend/src/app/workspace/chats/[thread_id]/page.tsx b/frontend/src/app/workspace/chats/[thread_id]/page.tsx index 909ffbe07..c5ff83dec 100644 --- a/frontend/src/app/workspace/chats/[thread_id]/page.tsx +++ b/frontend/src/app/workspace/chats/[thread_id]/page.tsx @@ -32,7 +32,8 @@ import { cn } from "@/lib/utils"; export default function ChatPage() { const { t } = useI18n(); const [showFollowups, setShowFollowups] = useState(false); - const { threadId, isNewThread, setIsNewThread, isMock } = useThreadChat(); + const { threadId, setThreadId, isNewThread, setIsNewThread, isMock } = + useThreadChat(); const [settings, setSettings] = useThreadSettings(threadId); const [mounted, setMounted] = useState(false); useSpecificChatMode(); @@ -47,10 +48,11 @@ export default function ChatPage() { threadId: isNewThread ? undefined : threadId, context: settings.context, isMock, - onStart: () => { + onStart: (createdThreadId) => { + setThreadId(createdThreadId); setIsNewThread(false); // ! Important: Never use next.js router for navigation in this case, otherwise it will cause the thread to re-mount and lose all states. Use native history API instead. - history.replaceState(null, "", `/workspace/chats/${threadId}`); + history.replaceState(null, "", `/workspace/chats/${createdThreadId}`); }, onFinish: (state) => { if (document.hidden || !document.hasFocus()) { diff --git a/frontend/src/components/workspace/chats/use-thread-chat.ts b/frontend/src/components/workspace/chats/use-thread-chat.ts index b3164485e..85e7db1a8 100644 --- a/frontend/src/components/workspace/chats/use-thread-chat.ts +++ b/frontend/src/components/workspace/chats/use-thread-chat.ts @@ -22,8 +22,11 @@ export function useThreadChat() { if (pathname.endsWith("/new")) { setIsNewThread(true); setThreadId(uuid()); + return; } - }, [pathname]); + setIsNewThread(false); + setThreadId(threadIdFromPath); + }, [pathname, threadIdFromPath]); const isMock = searchParams.get("mock") === "true"; - return { threadId, isNewThread, setIsNewThread, isMock }; + return { threadId, setThreadId, isNewThread, setIsNewThread, isMock }; } diff --git a/frontend/src/core/threads/hooks.ts b/frontend/src/core/threads/hooks.ts index 395f15604..51ef05ca5 100644 --- a/frontend/src/core/threads/hooks.ts +++ b/frontend/src/core/threads/hooks.ts @@ -164,9 +164,11 @@ export function useThreadStream({ useEffect(() => { const normalizedThreadId = threadId ?? null; if (!normalizedThreadId) { - // Just reset for new thread creation when threadId becomes null/undefined + // Reset when the UI moves back to a brand new unsaved thread. startedRef.current = false; setOnStreamThreadId(normalizedThreadId); + } else { + setOnStreamThreadId(normalizedThreadId); } threadIdRef.current = normalizedThreadId; }, [threadId]); @@ -294,6 +296,16 @@ export function useThreadStream({ // Track message count before sending so we know when server has responded const prevMsgCountRef = useRef(thread.messages.length); + // Reset thread-local pending UI state when switching between threads so + // optimistic messages and in-flight guards do not leak across chat views. + useEffect(() => { + startedRef.current = false; + sendInFlightRef.current = false; + prevMsgCountRef.current = 0; + setOptimisticMessages([]); + setIsUploading(false); + }, [threadId]); + // Clear optimistic when server messages arrive (count increases) useEffect(() => { if ( @@ -357,7 +369,12 @@ export function useThreadStream({ } setOptimisticMessages(newOptimistic); - _handleOnStart(threadId); + // Only fire onStart immediately for an existing persisted thread. + // Brand-new chats should wait for onCreated(meta.thread_id) so URL sync + // uses the real server-generated thread id. + if (threadIdRef.current) { + _handleOnStart(threadId); + } let uploadedFileInfo: UploadedFileInfo[] = [];