fix: load historical messages when switching threads via client.threads.getState
Deploy LangGraph Server to Azure Web App / build-and-deploy (push) Failing after 29s
Deploy LangGraph UI to Azure Static Web Apps / build-and-deploy (push) Failing after 47s

This commit is contained in:
gongzhiyong
2026-04-12 15:14:29 +08:00
parent 0d36c0fc0d
commit 964e0dd305
+35 -11
View File
@@ -112,6 +112,10 @@ function App() {
const [currentThreadId, setCurrentThreadId] = useState<string | null>(null);
const [sidebarOpen, setSidebarOpen] = useState(false);
// Historical messages fetched when switching to an existing thread
const [historicalMessages, setHistoricalMessages] = useState<Message[]>([]);
const [historicalUi, setHistoricalUi] = useState<UIMsgLocal[]>([]);
// Canvas panel state
const [canvasDoc, setCanvasDoc] = useState<CanvasDoc | null>(null);
@@ -243,7 +247,7 @@ function App() {
setSidebarOpen(false);
}, [currentThreadId, input]);
const handleSelectThread = useCallback((threadId: string) => {
const handleSelectThread = useCallback(async (threadId: string) => {
// Save draft for current thread
if (currentThreadId) {
try { localStorage.setItem(`draft_${currentThreadId}`, input); } catch { /* ignore */ }
@@ -255,8 +259,22 @@ function App() {
} catch {
setInput("");
}
// Clear historical messages before switching
setHistoricalMessages([]);
setHistoricalUi([]);
setCurrentThreadId(threadId);
setSidebarOpen(false);
// Fetch existing thread state so history is visible immediately
try {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const state = await (client.threads as any).getState(threadId);
if (state?.values?.messages) {
setHistoricalMessages(state.values.messages as Message[]);
}
if (state?.values?.ui) {
setHistoricalUi(state.values.ui as UIMsgLocal[]);
}
} catch { /* graceful degradation */ }
}, [currentThreadId, input]);
const handleDeleteThread = useCallback(async (threadId: string) => {
@@ -344,7 +362,7 @@ function App() {
}
// Auto-name thread on first message
const isFirstMessage = thread.messages.length === 0;
const isFirstMessage = activeMessages.length === 0;
// Consume pending retry tool hint (set by soc:retry-tool event)
const retryTool = pendingRetryToolRef.current;
@@ -418,7 +436,7 @@ function App() {
// ── Regenerate last AI message ───────────────────────────────────────────
function handleRegenerate() {
// Find last human message
const humanMsgs = thread.messages.filter((m) => m.type === "human");
const humanMsgs = activeMessages.filter((m) => m.type === "human");
const lastHuman = humanMsgs[humanMsgs.length - 1];
if (!lastHuman || thread.isLoading) return;
@@ -471,9 +489,12 @@ function App() {
reader.readAsDataURL(file);
}
// ── Active messages: prefer streaming messages, fall back to historical ──
const activeMessages: Message[] = thread.messages.length > 0 ? thread.messages : historicalMessages;
// ── Collect completed tool call IDs ─────────────────────────────────────
const completedToolIds = new Set(
thread.messages
activeMessages
.filter((m) => m.type === "tool")
.map((m) => (m as any).tool_call_id as string)
.filter(Boolean),
@@ -481,7 +502,7 @@ function App() {
// ── Collect failed tool call IDs ─────────────────────────────────────────
const failedToolIds = new Set(
thread.messages
activeMessages
.filter((m) => m.type === "tool" && (m as any).status === "error")
.map((m) => (m as any).tool_call_id as string)
.filter(Boolean),
@@ -500,7 +521,7 @@ function App() {
}>;
// ── Find last AI message index ──────────────────────────────────────────
const lastAiIdx = thread.messages.reduce((last, m, i) => (m.type === "ai" ? i : last), -1);
const lastAiIdx = activeMessages.reduce((last, m, i) => (m.type === "ai" ? i : last), -1);
return (
<div className="h-screen flex flex-row bg-background text-foreground overflow-hidden">
@@ -566,7 +587,7 @@ function App() {
setShowScrollBtn(el.scrollHeight - el.scrollTop - el.clientHeight > 200);
}}
>
{thread.messages.length === 0 && (
{activeMessages.length === 0 && (
<div className="flex flex-col items-center justify-center h-full gap-4 text-muted-foreground select-none">
<div className="flex flex-col items-center gap-2">
<p className="text-lg font-medium">你好,有什么可以帮你的?</p>
@@ -589,11 +610,14 @@ function App() {
</div>
)}
{thread.messages.map((message, idx) => {
{activeMessages.map((message, idx) => {
// Render UI cards attached to this message
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const allUi: UIMsgLocal[] = thread.messages.length > 0
? ((thread.values as any)?.ui ?? [])
: historicalUi;
const uiItemsRaw = deduplicateUiItems(
((thread.values as any)?.ui ?? []).filter(
allUi.filter(
(ui: UIMsgLocal) => ui.metadata?.message_id === message.id,
) as UIMsgLocal[]
);
@@ -779,7 +803,7 @@ function App() {
{/* Loading placeholder: show 3-dot bounce when waiting for first AI tokens after a human message */}
{(() => {
const lastMsg = thread.messages[thread.messages.length - 1];
const lastMsg = activeMessages[activeMessages.length - 1];
const showLoadingDots = thread.isLoading && lastMsg?.type === "human";
return showLoadingDots ? (
<div className="flex items-center gap-1.5 px-4 py-3 rounded-2xl rounded-bl-sm bg-muted text-muted-foreground w-fit">
@@ -794,7 +818,7 @@ function App() {
{thread.isLoading &&
deduplicateUiItems(
(thread.values?.ui ?? []).filter((ui: UIMsgLocal) => {
const attachedToExisting = thread.messages.some(
const attachedToExisting = activeMessages.some(
(m) => m.id === ui.metadata?.message_id,
);
return !attachedToExisting;