diff --git a/apps/studio/components/ui/AIAssistantPanel/Message.utils.ts b/apps/studio/components/ui/AIAssistantPanel/Message.utils.ts index 9867cc27511..1f901c2ccfe 100644 --- a/apps/studio/components/ui/AIAssistantPanel/Message.utils.ts +++ b/apps/studio/components/ui/AIAssistantPanel/Message.utils.ts @@ -2,7 +2,7 @@ import { untrustedSql } from '@supabase/pg-meta' import { z, type SafeParseReturnType } from 'zod' import { notebookOperationsSchema } from '@/data/content/notebooks/notebook-operations' -import { agentNotebookSchema } from '@/data/content/notebooks/notebook-schema' +import { agentNotebookSchema, notebookSchema } from '@/data/content/notebooks/notebook-schema' // Splits markdown into alternating [plain, code, plain, code, ...] segments. // Odd-indexed segments are already inside code spans/fences and should be left alone. @@ -143,6 +143,10 @@ export const updateNotebookInputSchema = z.object({ export const notebookToolOutputSchema = z.object({ id: z.string(), name: z.string() }) +export const updateNotebookToolOutputSchema = notebookToolOutputSchema.extend({ + previous_content: notebookSchema.optional(), +}) + export const rateMessageResponseSchema = z.object({ category: z.enum([ 'sql_generation', diff --git a/apps/studio/components/ui/AIAssistantPanel/NotebookProposalRenderer.test.tsx b/apps/studio/components/ui/AIAssistantPanel/NotebookProposalRenderer.test.tsx index f48bef2b8c7..f445dec37d2 100644 --- a/apps/studio/components/ui/AIAssistantPanel/NotebookProposalRenderer.test.tsx +++ b/apps/studio/components/ui/AIAssistantPanel/NotebookProposalRenderer.test.tsx @@ -339,6 +339,141 @@ describe('NotebookProposalRenderer', () => { expect(screen.queryByText("This update can't be applied as written")).not.toBeInTheDocument() }) + it('renders the snapshot-derived diff for a completed delete_cell update, without fetching the notebook', () => { + render( + + ) + + expect(screen.getByText('−1')).toBeInTheDocument() + expect(screen.getByRole('link', { name: 'Open notebook' })).toHaveAttribute( + 'href', + `/project/default/explorer/notebook/${NOTEBOOK_ID}` + ) + }) + + it('renders a single added cell for a completed insert_cell update, not a phantom duplicate', () => { + render( + + ) + + expect(screen.getByText('+1')).toBeInTheDocument() + expect(screen.getAllByRole('button', { name: 'Added Markdown cell' })).toHaveLength(1) + }) + + it('falls back to the compact completed body when previous_content is absent', () => { + render( + + ) + + expect(screen.getByText('Notebook updated: Signup funnel')).toBeInTheDocument() + expect(screen.queryByText("This update can't be applied as written")).not.toBeInTheDocument() + expect(screen.getByRole('link', { name: 'Open notebook' })).toBeInTheDocument() + }) + + it('falls back to the compact completed body when the snapshot no longer matches the operations', () => { + render( + + ) + + expect(screen.getByText('Notebook updated: Signup funnel')).toBeInTheDocument() + expect(screen.queryByText("This update can't be applied as written")).not.toBeInTheDocument() + expect(screen.queryByText('Preview unavailable')).not.toBeInTheDocument() + }) + + it('falls back to the compact completed body when the output id does not match the requested notebook', () => { + render( + + ) + + expect(screen.getByText('Notebook updated: Signup funnel')).toBeInTheDocument() + expect(screen.queryByText('−1')).not.toBeInTheDocument() + }) + it('derives the diff against live content for a denied update', async () => { mockContentItem(mockNotebookRow()) diff --git a/apps/studio/components/ui/AIAssistantPanel/NotebookProposalRenderer.tsx b/apps/studio/components/ui/AIAssistantPanel/NotebookProposalRenderer.tsx index d2634c6a948..af710fbf15b 100644 --- a/apps/studio/components/ui/AIAssistantPanel/NotebookProposalRenderer.tsx +++ b/apps/studio/components/ui/AIAssistantPanel/NotebookProposalRenderer.tsx @@ -13,6 +13,7 @@ import { createNotebookInputSchema, notebookToolOutputSchema, updateNotebookInputSchema, + updateNotebookToolOutputSchema, } from './Message.utils' import { AlertError } from '@/components/ui/AlertError' import { @@ -340,8 +341,31 @@ function UpdateNotebookProposal({ } if (isCompleted) { - const parsedOutput = notebookToolOutputSchema.safeParse(output) + const parsedOutput = updateNotebookToolOutputSchema.safeParse(output) const notebookName = parsedOutput.success ? parsedOutput.data.name : undefined + const isOutputForRequestedNotebook = + parsedOutput.success && parsedOutput.data.id === parsedInput.data.id + const previousContent = isOutputForRequestedNotebook + ? parsedOutput.data.previous_content + : undefined + const diff = previousContent + ? deriveNotebookDiff(previousContent, parsedInput.data.operations) + : undefined + + if (diff?.success) { + return ( + + + + ) + } return (