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 (