From 0287240728be2f30dd0cb4d612ec64e40392d3fc Mon Sep 17 00:00:00 2001 From: Xinmin Zeng <135568692+fancyboi999@users.noreply.github.com> Date: Thu, 28 May 2026 15:27:38 +0800 Subject: [PATCH] fix(frontend): show new thread in sidebar immediately on creation (#3276) (#3283) When a user starts a new conversation, the sidebar list did not display it until the AI finished streaming and generated a title. This made it impossible to switch back to an in-progress conversation when working with multiple threads concurrently. Optimistically insert the new thread into the TanStack Query cache during the `onCreated` callback so the sidebar renders a placeholder entry ("New chat") as soon as the backend acknowledges thread creation. The existing `onUpdateEvent` title handler and `onFinish` query invalidation then update the entry in-place with the real title. --- frontend/src/core/threads/hooks.ts | 63 +++++++++++++++++++++++++++++- 1 file changed, 62 insertions(+), 1 deletion(-) diff --git a/frontend/src/core/threads/hooks.ts b/frontend/src/core/threads/hooks.ts index 1c927c5ff..aa0e4dfbd 100644 --- a/frontend/src/core/threads/hooks.ts +++ b/frontend/src/core/threads/hooks.ts @@ -1,7 +1,12 @@ import type { AIMessage, Message, Run } from "@langchain/langgraph-sdk"; import type { ThreadsClient } from "@langchain/langgraph-sdk/client"; import { useStream } from "@langchain/langgraph-sdk/react"; -import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { + type QueryClient, + useMutation, + useQuery, + useQueryClient, +} from "@tanstack/react-query"; import { useCallback, useEffect, useRef, useState } from "react"; import { toast } from "sonner"; @@ -149,6 +154,48 @@ export function getVisibleOptimisticMessages( return optimisticMessages; } +export function upsertThreadInSearchCache( + queryClient: QueryClient, + thread: AgentThread, +) { + queryClient.setQueriesData( + { + queryKey: ["threads", "search"], + exact: false, + }, + (oldData: Array | undefined) => { + if (!oldData) { + return [thread]; + } + + const existingIndex = oldData.findIndex( + (t) => t.thread_id === thread.thread_id, + ); + if (existingIndex === -1) { + return [thread, ...oldData]; + } + + return oldData.map((t, index) => { + if (index !== existingIndex) { + return t; + } + return { + ...thread, + ...t, + metadata: { + ...(thread.metadata ?? {}), + ...(t.metadata ?? {}), + }, + values: { + ...thread.values, + ...t.values, + }, + }; + }); + }, + ); +} + function getStreamErrorMessage(error: unknown): string { if (typeof error === "string" && error.trim()) { return error; @@ -241,6 +288,20 @@ export function useThreadStream({ fetchStateHistory: { limit: 1 }, onCreated(meta) { handleStreamStart(meta.thread_id, meta.run_id); + const now = new Date().toISOString(); + upsertThreadInSearchCache(queryClient, { + thread_id: meta.thread_id, + created_at: now, + updated_at: now, + metadata: context.agent_name ? { agent_name: context.agent_name } : {}, + status: "busy", + values: { + title: t.pages.newChat, + messages: [], + artifacts: [], + }, + interrupts: {}, + }); if (context.agent_name && !isMock) { void getAPIClient() .threads.update(meta.thread_id, {