mirror of
https://github.com/supabase/supabase.git
synced 2026-09-08 10:59:38 +08:00
## Summary - Adds a `create_notebook` AI assistant tool (`needsApproval: true`) that lets the assistant create a new notebook after explicit user approval. - Cell SQL is promoted from untrusted to safe via `acceptUntrustedSql`/`acceptUntrustedLogsSql` inside `execute`, using the approval gate as the confirming user gesture (same pattern as `execute_sql`). - Input is validated against the existing agent-writable notebook schema, which rejects any agent-supplied cell `id` at the schema level. - Threads an optional auth-headers param through `upsertContent`/`createNotebook`/`updateNotebook` so the tool can pass its own bearer token server-side. - Registers the tool in the tool-filter (`SCHEMA` category, alongside `list_notebooks`/`get_notebook`) and adds a `## Notebooks` prompt section guiding the assistant on when to use `create_notebook` vs. one-off `execute_sql`. Resolves FE-4082 ## Test plan - [x] `notebook-tools.test.ts` covers: tool registration, `needsApproval`, cell-id rejection, valid input, PUT body shape, and the returned id — all passing - [x] Typecheck clean - [x] Lint clean (no new warnings) <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added AI-assisted notebook creation for saving multi-step investigations. * Added support for database and log SQL cells in newly created notebooks. * Notebook creation requires approval before saving and returns the notebook’s name and identifier. * Added support for custom request headers during notebook and content operations. * Added guidance for choosing between one-time SQL execution and reusable notebooks when Explorer is enabled. * **Improvements** * Improved validation and normalization of notebook content before saving. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
80 lines
2.4 KiB
TypeScript
80 lines
2.4 KiB
TypeScript
import { useMutation, useQueryClient } from '@tanstack/react-query'
|
|
import { toast } from 'sonner'
|
|
|
|
import type { Content } from './content-query'
|
|
import { remapWireSnippet, unmapSqlContentField } from './content-remap'
|
|
import { contentKeys } from './keys'
|
|
import type { SnippetWithContent } from './sql-folders-query'
|
|
import type { components } from '@/data/api'
|
|
import { handleError, put } from '@/data/fetchers'
|
|
import type { ResponseError, UseCustomMutationOptions } from '@/types'
|
|
|
|
export type UpsertContentPayload = Omit<components['schemas']['UpsertContentBody'], 'content'> & {
|
|
id: string
|
|
content: Partial<Content['content']>
|
|
favorite?: boolean
|
|
}
|
|
|
|
export type UpsertContentVariables = {
|
|
projectRef: string
|
|
payload: UpsertContentPayload
|
|
}
|
|
|
|
export async function upsertContent(
|
|
{ projectRef, payload }: UpsertContentVariables,
|
|
signal?: AbortSignal,
|
|
headersInit?: HeadersInit
|
|
): Promise<SnippetWithContent | null> {
|
|
const headers = new Headers(headersInit)
|
|
headers.set('Version', '2')
|
|
|
|
const { data, error } = await put('/platform/projects/{ref}/content', {
|
|
params: { path: { ref: projectRef } },
|
|
body: unmapSqlContentField(payload),
|
|
headers,
|
|
signal,
|
|
})
|
|
if (error) handleError(error)
|
|
if (!data) return null
|
|
|
|
return remapWireSnippet(data, 'saved')
|
|
}
|
|
|
|
export type UpsertContentData = Awaited<ReturnType<typeof upsertContent>>
|
|
|
|
export const useContentUpsertMutation = ({
|
|
onError,
|
|
onSuccess,
|
|
invalidateQueriesOnSuccess = true,
|
|
...options
|
|
}: Omit<
|
|
UseCustomMutationOptions<UpsertContentData, ResponseError, UpsertContentVariables>,
|
|
'mutationFn'
|
|
> & {
|
|
invalidateQueriesOnSuccess?: boolean
|
|
} = {}) => {
|
|
const queryClient = useQueryClient()
|
|
|
|
return useMutation<UpsertContentData, ResponseError, UpsertContentVariables>({
|
|
mutationFn: (args) => upsertContent(args),
|
|
async onSuccess(data, variables, context) {
|
|
const { projectRef } = variables
|
|
if (invalidateQueriesOnSuccess) {
|
|
await Promise.all([
|
|
queryClient.invalidateQueries({ queryKey: contentKeys.allContentLists(projectRef) }),
|
|
queryClient.invalidateQueries({ queryKey: contentKeys.infiniteList(projectRef) }),
|
|
])
|
|
}
|
|
await onSuccess?.(data, variables, context)
|
|
},
|
|
async onError(data, variables, context) {
|
|
if (onError === undefined) {
|
|
toast.error(`Failed to insert content: ${data.message}`)
|
|
} else {
|
|
onError(data, variables, context)
|
|
}
|
|
},
|
|
...options,
|
|
})
|
|
}
|