Files
supabase/apps/studio/data/content/content-delete-mutation.ts
Charis 89b4f1aca4 feat(studio): add delete_notebook tool to AI assistant (#49413)
## Summary

* Adds a `delete_notebook` AI assistant tool (`needsApproval: true`)
that lets the assistant delete a notebook with explicit user approval,
mirroring the existing `create_notebook`/`update_notebook` tools.
* Wires up a destructive-styled approval card in the AI Assistant Panel
(fetches the notebook to show its name, warns the deletion is permanent)
using the same `Confirm`/tool-approval plumbing as the other notebook
tools.
* Updates `tool-filter.ts` opt-in gating, the assistant system prompt,
the eval-harness mock tools, and the eval dataset with `delete_notebook`
coverage.
* Adds test coverage in `notebook-tools.test.ts`, `mock-tools.test.ts`,
and `NotebookProposalRenderer.test.tsx`.

Closes
[FE-4242](https://linear.app/supabase/issue/FE-4242/assistant-delete-notebook-tool).

## Test plan

- [X] `pnpm typecheck --filter=studio` passes
- [X] `pnpm --filter studio exec vitest run` for the touched files
(notebook-tools, mock-tools, NotebookProposalRenderer,
[Message.Parts](<http://Message.Parts>), and existing consumers of
`content-delete-mutation`) — all passing
- [X] `eslint` and `prettier --check` clean on all touched files
- [X] Manual verification of the approval UI in a running Studio
instance (not done in this session)

## Summary by CodeRabbit

* **New Features**
* Added AI-assisted notebook deletion with explicit confirmation and
irreversible-action warnings.
* Added safeguards to distinguish deleting an entire notebook from
removing individual panels.
* Completed deletions now display the deleted notebook’s name without an
option to reopen it.
* **Bug Fixes**
* Improved handling of missing notebooks and invalid deletion requests.
* **Tests**
* Added coverage for deletion approval, denial, errors, and successful
completion.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Added AI-assisted notebook deletion with explicit approval and
irreversible-action warnings.
* Added confirmation, loading, error, and completion states for notebook
deletion.
* Prevented accidental full-notebook deletion when only a panel or
section should be removed.
* Improved notebook update results by showing applied changes when
available.

* **Bug Fixes**
  * Notebook deletion now uses the required API version.
* Improved handling and validation of missing notebooks during deletion.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Joshen Lim <joshenlimek@gmail.com>
2026-08-24 13:18:12 -04:00

96 lines
3.2 KiB
TypeScript

import { useMutation, useQueryClient } from '@tanstack/react-query'
import type { QueryKey } from '@tanstack/react-query'
import { toast } from 'sonner'
import type { ContentData } from './content-query'
import { contentKeys } from './keys'
import { del, handleError } from '@/data/fetchers'
import type { ResponseError, UseCustomMutationOptions } from '@/types'
type DeleteContentVariables = { projectRef: string; ids: string[] }
type DeleteContext = { snapshots: [QueryKey, ContentData | undefined][] }
export async function deleteContents(
{ projectRef, ids }: DeleteContentVariables,
signal?: AbortSignal,
headersInit?: HeadersInit
) {
const headers = new Headers(headersInit)
headers.set('Version', '2')
const { data, error } = await del('/platform/projects/{ref}/content', {
headers,
params: {
path: { ref: projectRef },
query: { ids: ids.join(',') },
},
signal,
})
if (error) handleError(error)
return data.map((x) => x.id)
}
type DeleteContentData = Awaited<ReturnType<typeof deleteContents>>
/** Returns a copy of a cached content list with the given ids removed. Exported for testing. */
export function removeContentFromList(data: ContentData, ids: string[]): ContentData {
return { ...data, content: data.content.filter((item) => !ids.includes(item.id)) }
}
export const useContentDeleteMutation = ({
onSuccess,
onError,
...options
}: Omit<
UseCustomMutationOptions<DeleteContentData, ResponseError, DeleteContentVariables, DeleteContext>,
'mutationFn'
> = {}) => {
const queryClient = useQueryClient()
return useMutation<DeleteContentData, ResponseError, DeleteContentVariables, DeleteContext>({
mutationFn: (args) => deleteContents(args),
async onMutate({ projectRef, ids }) {
await queryClient.cancelQueries({ queryKey: contentKeys.allContentLists(projectRef) })
const snapshots = queryClient.getQueriesData<ContentData>({
queryKey: contentKeys.allContentLists(projectRef),
})
// allContentLists is a prefix of sibling caches (sql snippets, folders, counts) that
// don't share the { content: [...] } shape, so only optimistically update entries that do.
for (const [queryKey, data] of snapshots) {
if (data && Array.isArray(data.content)) {
queryClient.setQueryData<ContentData>(queryKey, removeContentFromList(data, ids))
}
}
return { snapshots }
},
async onSuccess(data, variables, context) {
const { projectRef } = variables
await Promise.all([
queryClient.invalidateQueries({ queryKey: contentKeys.allContentLists(projectRef) }),
queryClient.invalidateQueries({ queryKey: contentKeys.infiniteList(projectRef) }),
])
await onSuccess?.(data, variables, context)
},
async onError(error, variables, context) {
// Restore the snapshots captured in onMutate before surfacing the error.
if (context?.snapshots) {
for (const [queryKey, data] of context.snapshots) {
queryClient.setQueryData(queryKey, data)
}
}
if (onError === undefined) {
toast.error(`Failed to delete contents: ${error.message}`)
} else {
onError(error, variables, context)
}
},
...options,
})
}