mirror of
https://github.com/supabase/supabase.git
synced 2026-10-09 11:25:06 +03:00
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 <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## 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. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
47 lines
1.7 KiB
TypeScript
47 lines
1.7 KiB
TypeScript
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<string, any>; chatInstances: Record<string, any> }>({
|
|
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()
|
|
})
|
|
})
|