diff --git a/src/app/api/chat/route.ts b/src/app/api/chat/route.ts index 53e7dc4f..75cb07c7 100644 --- a/src/app/api/chat/route.ts +++ b/src/app/api/chat/route.ts @@ -132,8 +132,10 @@ async function handlePOST(req: Request) { const system = body.system || ""; workspaceId = extractWorkspaceId(body); activeFolderId = body.activeFolderId; - // AssistantChatTransport passes thread remoteId as body.id (see assistant-ui react-ai-sdk) - const threadId = body.id ?? body.threadId ?? null; + // Prefer the explicit threadId bridge over AssistantChatTransport's + // default body.id fallback, which may be DEFAULT_THREAD_ID. + const rawThreadId = body.threadId ?? body.id ?? null; + const threadId = rawThreadId === "DEFAULT_THREAD_ID" ? null : rawThreadId; // Create tools using the modular factory (before convertToModelMessages so // toModelOutput can sanitize historical tool results for the model) diff --git a/src/components/assistant-ui/WorkspaceRuntimeProvider.tsx b/src/components/assistant-ui/WorkspaceRuntimeProvider.tsx index 8b76b7a8..95ea38cc 100644 --- a/src/components/assistant-ui/WorkspaceRuntimeProvider.tsx +++ b/src/components/assistant-ui/WorkspaceRuntimeProvider.tsx @@ -79,12 +79,7 @@ export function WorkspaceRuntimeProvider({ activePdfPageByItemId, viewingItemIds, ); - }, [ - workspaceState, - contextCardIds, - activePdfPageByItemId, - viewingItemIds, - ]); + }, [workspaceState, contextCardIds, activePdfPageByItemId, viewingItemIds]); // Per AI SDK, transport `body` is `Resolvable` — if it is a function, `resolve()` // calls it on every sendMessages (see @ai-sdk/provider-utils resolve()). That gives @@ -99,6 +94,7 @@ export function WorkspaceRuntimeProvider({ activeFolderId, selectedCardsContext: "", }); + const threadRemoteIdRef = useRef(null); chatApiPayloadRef.current.workspaceId = workspaceId; chatApiPayloadRef.current.modelId = selectedModelId; chatApiPayloadRef.current.activeFolderId = activeFolderId; @@ -175,7 +171,7 @@ export function WorkspaceRuntimeProvider({ }, []); const threadListAdapter = useMemo( - () => createThreadListAdapter(workspaceId), + () => createThreadListAdapter(workspaceId, threadRemoteIdRef), [workspaceId], ); @@ -183,7 +179,10 @@ export function WorkspaceRuntimeProvider({ () => new AssistantChatTransport({ api: "/api/chat", - body: () => ({ ...chatApiPayloadRef.current }), + body: () => ({ + ...chatApiPayloadRef.current, + threadId: threadRemoteIdRef.current, + }), }), // Body snapshot comes from the ref via Resolvable function above. // eslint-disable-next-line react-hooks/exhaustive-deps -- stable transport instance diff --git a/src/lib/chat/ThreadRemoteIdBridge.tsx b/src/lib/chat/ThreadRemoteIdBridge.tsx new file mode 100644 index 00000000..e80459c6 --- /dev/null +++ b/src/lib/chat/ThreadRemoteIdBridge.tsx @@ -0,0 +1,20 @@ +"use client"; + +import { useEffect, type MutableRefObject } from "react"; +import { useAuiState } from "@assistant-ui/react"; + +export function ThreadRemoteIdSync({ + remoteIdRef, +}: { + remoteIdRef: MutableRefObject; +}) { + const remoteId = useAuiState( + (state) => state.threadListItem.remoteId ?? null, + ); + + useEffect(() => { + remoteIdRef.current = remoteId; + }, [remoteId, remoteIdRef]); + + return null; +} diff --git a/src/lib/chat/custom-thread-list-adapter.tsx b/src/lib/chat/custom-thread-list-adapter.tsx index aff89d14..90489096 100644 --- a/src/lib/chat/custom-thread-list-adapter.tsx +++ b/src/lib/chat/custom-thread-list-adapter.tsx @@ -1,6 +1,11 @@ "use client"; -import { type FC, type PropsWithChildren, useMemo } from "react"; +import { + type FC, + type MutableRefObject, + type PropsWithChildren, + useMemo, +} from "react"; import { type ThreadMessage, type RemoteThreadListAdapter, @@ -8,20 +13,19 @@ import { } from "@assistant-ui/react"; import { createAssistantStream } from "assistant-stream"; import { useCustomThreadHistoryAdapter } from "./custom-thread-history-adapter"; +import { ThreadRemoteIdSync } from "./ThreadRemoteIdBridge"; import { SupabaseAttachmentAdapter } from "@/lib/attachments/supabase-attachment-adapter"; const attachmentsInstance = new SupabaseAttachmentAdapter(); -function CustomThreadListProviderInner({ - children, -}: PropsWithChildren) { +function CustomThreadListProviderInner({ children }: PropsWithChildren) { const history = useCustomThreadHistoryAdapter(); const adapters = useMemo( () => ({ history, attachments: attachmentsInstance, }), - [history] + [history], ); return ( @@ -31,13 +35,18 @@ function CustomThreadListProviderInner({ } export function createThreadListAdapter( - workspaceId: string + workspaceId: string, + threadRemoteIdRef: MutableRefObject, ): RemoteThreadListAdapter { - const unstable_Provider: FC = function CustomThreadListProvider({ - children, - }) { - return {children}; - }; + const unstable_Provider: FC = + function CustomThreadListProvider({ children }) { + return ( + + + {children} + + ); + }; return { async list() { @@ -47,12 +56,19 @@ export function createThreadListAdapter( if (!res.ok) throw new Error(`Failed to list threads: ${res.status}`); const data = await res.json(); return { - threads: (data.threads ?? []).map((t: { remoteId: string; status?: string; title?: string; externalId?: string }) => ({ - remoteId: t.remoteId, - status: t.status ?? "regular", - title: t.title, - externalId: t.externalId, - })), + threads: (data.threads ?? []).map( + (t: { + remoteId: string; + status?: string; + title?: string; + externalId?: string; + }) => ({ + remoteId: t.remoteId, + status: t.status ?? "regular", + title: t.title, + externalId: t.externalId, + }), + ), }; }, @@ -64,7 +80,10 @@ export function createThreadListAdapter( }); if (!res.ok) { const err = await res.json().catch(() => ({})); - throw new Error((err as { error?: string }).error ?? `Failed to create thread: ${res.status}`); + throw new Error( + (err as { error?: string }).error ?? + `Failed to create thread: ${res.status}`, + ); } const data = await res.json(); return { @@ -113,10 +132,7 @@ export function createThreadListAdapter( }; }, - async generateTitle( - remoteId: string, - messages: readonly ThreadMessage[] - ) { + async generateTitle(remoteId: string, messages: readonly ThreadMessage[]) { return createAssistantStream(async (controller) => { const res = await fetch(`/api/threads/${remoteId}/title`, { method: "POST",