diff --git a/frontend/app/[locale]/newchat/adapter/conversation-thread-list-adapter.tsx b/frontend/app/[locale]/newchat/adapter/conversation-thread-list-adapter.tsx index eee8573277..68b46f9550 100644 --- a/frontend/app/[locale]/newchat/adapter/conversation-thread-list-adapter.tsx +++ b/frontend/app/[locale]/newchat/adapter/conversation-thread-list-adapter.tsx @@ -1377,6 +1377,16 @@ export const conversationThreadListAdapter: RemoteThreadListAdapter = { }; }, + // New conversations do not have a backend ID until their first agent run. + // Accept metadata updates so assistant-ui can retain the selected agent in + // its local thread state while users switch between conversations. + async updateCustom( + _remoteId: string, + _custom: Record | undefined + ): Promise { + return; + }, + async rename(remoteId: string, newTitle: string): Promise { const candidateId = await waitForServerConversationId(remoteId); const conversationId = Number(candidateId); diff --git a/frontend/app/[locale]/newchat/page.tsx b/frontend/app/[locale]/newchat/page.tsx index 3f60dd1a5a..c577ab0eff 100644 --- a/frontend/app/[locale]/newchat/page.tsx +++ b/frontend/app/[locale]/newchat/page.tsx @@ -700,6 +700,11 @@ const HomeContent: FC<{ async (agent: Agent) => { shouldRestoreAgentRef.current = true; await runtime.threads.switchToNewThread(); + const thread = runtime.threads.getItemById( + runtime.threads.getState().mainThreadId + ); + await thread.initialize(); + await thread.updateCustom({ agentId: agent.id }); onAgentSelected(agent); }, [runtime, onAgentSelected] diff --git a/frontend/services/storageService.ts b/frontend/services/storageService.ts index e64e1c2bf9..a2bdb34150 100644 --- a/frontend/services/storageService.ts +++ b/frontend/services/storageService.ts @@ -168,9 +168,6 @@ export async function fetchImageBlob(url: string): Promise { const apiUrl = convertImageUrlToApiUrl(url); log.info(`[fetchImageBlob] input=${url}, apiUrl=${apiUrl}`); const response = await fetch(apiUrl); - log.info( - `[fetchImageBlob] status=${response.status}, contentType=${response.headers.get("content-type")}` - ); if (!response.ok) { throw new Error( `Failed to fetch image: ${response.status} ${response.statusText}` diff --git a/frontend/tests/newchatThreadAgentBinding.test.ts b/frontend/tests/newchatThreadAgentBinding.test.ts new file mode 100644 index 0000000000..9a3100aa10 --- /dev/null +++ b/frontend/tests/newchatThreadAgentBinding.test.ts @@ -0,0 +1,36 @@ +import assert from "node:assert/strict"; +import { readFile } from "node:fs/promises"; +import test from "node:test"; + +const pagePath = new URL("../app/[locale]/newchat/page.tsx", import.meta.url); +const adapterPath = new URL( + "../app/[locale]/newchat/adapter/conversation-thread-list-adapter.tsx", + import.meta.url +); + +test("binds an agent selected for a new thread to that thread's metadata", async () => { + const page = await readFile(pagePath, "utf8"); + + assert.match( + page, + /thread\.updateCustom\(\{\s*agentId:\s*agent\.id\s*\}\)/ + ); +}); + +test("initializes a new thread before storing its selected agent", async () => { + const page = await readFile(pagePath, "utf8"); + + assert.match( + page, + /const thread = runtime\.threads\.getItemById\([\s\S]*await thread\.initialize\(\);[\s\S]*await thread\.updateCustom\(/ + ); +}); + +test("supports storing an agent on a new thread before the conversation exists on the server", async () => { + const adapter = await readFile(adapterPath, "utf8"); + + assert.match( + adapter, + /async updateCustom\(\s*_remoteId: string,\s*_custom: Record \| undefined\s*\): Promise/ + ); +});