fix: lazy thread creation — only create thread on first message send
Trigger auto deployment for soc-langgraph / build-and-deploy (push) Failing after 20s
Deploy LangGraph UI to Azure Static Web Apps / build-and-deploy (push) Failing after 39s

Previously handleNewThread called client.threads.create() immediately,
leaving empty "新对话" threads in the sidebar when users clicked new
thread but never sent a message (ChatGPT-style behavior).

Now handleNewThread just resets UI state (setCurrentThreadId(null)).
A new ensureThread() helper in useConversation lazily creates the
backend thread only when the user actually submits a message.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
gongzhiyong
2026-04-15 02:46:52 +08:00
co-authored by Claude Opus 4.6
parent 361b576c6c
commit 902c8331be
2 changed files with 39 additions and 16 deletions
+25 -9
View File
@@ -15,23 +15,19 @@ export function useConversation(
const [historicalMessages, setHistoricalMessages] = useState<Message[]>([]);
const [historicalUi, setHistoricalUi] = useState<UIMsgLocal[]>([]);
const [resetLoading, setResetLoading] = useState(false);
const [threadLoadFailed, setThreadLoadFailed] = useState(false);
const handleNewThread = useCallback(async () => {
const handleNewThread = useCallback(() => {
if (currentThreadId) {
try { localStorage.setItem(`draft_${currentThreadId}`, input); } catch { /* ignore */ }
}
setInput("");
setHistoricalMessages([]);
setHistoricalUi([]);
try {
const t = await client.threads.create();
setThreads((prev) => [t as ThreadItem, ...prev]);
setCurrentThreadId(t.thread_id);
} catch {
setCurrentThreadId(null);
}
setThreadLoadFailed(false);
setCurrentThreadId(null);
setSidebarOpen(false);
}, [currentThreadId, input, client, setInput]);
}, [currentThreadId, input, setInput]);
const handleSelectThread = useCallback(async (threadId: string) => {
if (currentThreadId) {
@@ -48,6 +44,8 @@ export function useConversation(
const state = await loadThreadState(client, threadId);
setHistoricalMessages(state.messages.length ? state.messages : []);
setHistoricalUi(state.ui.length ? (state.ui as UIMsgLocal[]) : []);
// Track whether this is a selected thread with no loadable history
setThreadLoadFailed(state.messages.length === 0);
setCurrentThreadId(threadId);
setSidebarOpen(false);
}, [currentThreadId, input, client, setInput]);
@@ -105,6 +103,22 @@ export function useConversation(
}
}, [client]);
/**
* Ensures a thread exists, creating one lazily if currentThreadId is null.
* Returns the thread_id to use for submission.
*/
const ensureThread = useCallback(async (): Promise<string | null> => {
if (currentThreadId) return currentThreadId;
try {
const t = await client.threads.create();
setThreads((prev) => [t as ThreadItem, ...prev]);
setCurrentThreadId(t.thread_id);
return t.thread_id;
} catch {
return null;
}
}, [currentThreadId, client]);
return {
threads,
setThreads,
@@ -117,11 +131,13 @@ export function useConversation(
historicalUi,
setHistoricalUi,
resetLoading,
threadLoadFailed,
handleNewThread,
handleSelectThread,
handleDeleteThread,
handleResetThread,
applyThreadTitle,
updateThreadLastActive,
ensureThread,
};
}
+14 -7
View File
@@ -43,7 +43,7 @@ function App() {
historicalMessages, historicalUi,
resetLoading, threadLoadFailed,
handleNewThread, handleSelectThread, handleDeleteThread, handleResetThread,
applyThreadTitle,
applyThreadTitle, ensureThread,
} = useConversation(client, input, setInput);
// Canvas panel state
@@ -203,10 +203,17 @@ function App() {
const { text, attachedFile: file, activeTools: tools, modelMode: mode } = payload;
setInput("");
setSourceLabel(null);
if (currentThreadId) {
try { localStorage.removeItem(`draft_${currentThreadId}`); } catch { /* ignore */ }
updateThreadLastActive(currentThreadId);
// Lazy thread creation: create a real thread only when the user first sends a message
const activeThreadId = await ensureThread();
if (!activeThreadId) {
setConcurrentStatus("idle");
return;
}
thread.switchThread(activeThreadId);
try { localStorage.removeItem(`draft_${activeThreadId}`); } catch { /* ignore */ }
updateThreadLastActive(activeThreadId);
const enabledTools =
tools.size > 0
@@ -250,9 +257,9 @@ function App() {
// Scroll to bottom after submitting so user message stays visible
setTimeout(() => bottomRef.current?.scrollIntoView({ behavior: "smooth" }), 100);
if (currentThreadId && text) {
const existingTitle = threads.find((t) => t.thread_id === currentThreadId)?.metadata?.title as string | undefined;
applyThreadTitle(currentThreadId, text, activeMessages.length, existingTitle);
if (activeThreadId && text) {
const existingTitle = threads.find((t) => t.thread_id === activeThreadId)?.metadata?.title as string | undefined;
applyThreadTitle(activeThreadId, text, activeMessages.length, existingTitle);
}
}