From 48640314669c5b144498345db2dfc47a68f01202 Mon Sep 17 00:00:00 2001 From: Ali Waseem Date: Mon, 20 Jul 2026 10:25:42 -0600 Subject: [PATCH] fix(assistant): SQL approval buttons do nothing (#48102) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes FE-3954: clicking Skip/Run Query in the AI Assistant did nothing. ## Root cause `onFinish` synced the AI SDK `Chat` instance's live message array directly into valtio state (`chat.messages = messages`). valtio's `proxy()` mutates an object's nested properties in place instead of cloning them, so this corrupted the SDK's own array with Proxies. The next approval click hit the SDK's internal `structuredClone()` call and threw `DOMException: Proxy object could not be cloned` — an unhandled rejection before any network request, so the buttons silently did nothing. ## Fix Assign a sanitized copy of the message array instead of the SDK's live reference. ## Test plan - [x] `pnpm --filter studio exec vitest run state/ai-assistant-state.test.ts` — fails on old code with the exact DOMException, passes on the fix - [ ] Manual: approve/skip a suggested query in AI Assistant and confirm it runs/is skipped ## Summary by CodeRabbit * **Bug Fixes** * Improved AI assistant chat message synchronization to prevent message corruption. * Ensured chat messages remain safely cloneable after approval-related updates. * **Tests** * Added coverage verifying that synchronized AI assistant messages can be cloned successfully. --- apps/studio/state/ai-assistant-state.test.ts | 46 ++++++++++++++++++++ apps/studio/state/ai-assistant-state.tsx | 5 ++- 2 files changed, 49 insertions(+), 2 deletions(-) create mode 100644 apps/studio/state/ai-assistant-state.test.ts diff --git a/apps/studio/state/ai-assistant-state.test.ts b/apps/studio/state/ai-assistant-state.test.ts new file mode 100644 index 00000000000..4fb412ffe83 --- /dev/null +++ b/apps/studio/state/ai-assistant-state.test.ts @@ -0,0 +1,46 @@ +import { proxy, ref } from 'valtio/vanilla' +import { describe, expect, it } from 'vitest' + +import { sanitizeForCloning } from './ai-assistant-state' + +describe('AI assistant chat message sync', () => { + // FE-3954: syncing the live array into valtio corrupted it with Proxies, breaking structuredClone in addToolApprovalResponse + it('does not corrupt the live SDK message array when synced into valtio state', () => { + const state = proxy<{ chats: Record; chatInstances: Record }>({ + chats: {}, + chatInstances: {}, + }) + state.chats['chat-1'] = { id: 'chat-1', messages: [] } + + const liveSdkMessages = [ + { + id: 'message-1', + role: 'assistant', + parts: [ + { type: 'text', text: "Sure, here's a query" }, + { + type: 'tool-execute_sql', + toolCallId: 'tool-1', + state: 'approval-requested', + approval: { id: 'approval-1' }, + }, + ], + }, + ] + state.chatInstances['chat-1'] = ref({ messages: liveSdkMessages }) + + const chat = state.chats['chat-1'] + chat.messages = liveSdkMessages.map((message) => sanitizeForCloning(message)) + + // mirrors addToolApprovalResponse's own update logic + const lastMessage = liveSdkMessages[liveSdkMessages.length - 1] + const updatedParts = lastMessage.parts.map((part: any) => + part.state === 'approval-requested' && part.approval?.id === 'approval-1' + ? { ...part, state: 'approval-responded', approval: { id: 'approval-1', approved: true } } + : part + ) + const replacedMessage = { ...lastMessage, parts: updatedParts } + + expect(() => structuredClone(replacedMessage)).not.toThrow() + }) +}) diff --git a/apps/studio/state/ai-assistant-state.tsx b/apps/studio/state/ai-assistant-state.tsx index c083fa10e52..9f9599b32f9 100644 --- a/apps/studio/state/ai-assistant-state.tsx +++ b/apps/studio/state/ai-assistant-state.tsx @@ -152,7 +152,7 @@ async function clearStorage(): Promise { // Helper function to sanitize objects to ensure they're cloneable // Issue due to addToolResult -function sanitizeForCloning(obj: any): any { +export function sanitizeForCloning(obj: any): any { if (obj === null || obj === undefined) return obj if (typeof obj !== 'object') return obj return JSON.parse(JSON.stringify(obj)) @@ -329,7 +329,8 @@ function createChatInstance( const messages = chatInstance.messages const chat = state.chats[options.id] if (chat) { - chat.messages = messages + // Clone first — valtio's proxy() mutates nested properties in place and would corrupt the SDK's live array + chat.messages = messages.map((message) => sanitizeForCloning(message)) chat.updatedAt = new Date() }