From 8a33c094b4ab52d3a93c3d3ba26cdf2eb5bba034 Mon Sep 17 00:00:00 2001
From: "claude[bot]" <209825114+claude[bot]@users.noreply.github.com>
Date: Wed, 19 Aug 2026 23:15:29 +0100
Subject: [PATCH 1/4] chore(www): add /evals to the sitemap (#49226)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
_Requested by **Sean Oliver** · [Slack
thread](https://supabase.slack.com/archives/C07P3AU3J2D/p1787036390117589?thread_ts=1787036390.117589&cid=C07P3AU3J2D)_
## I have read the
[CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md)
file.
YES
## What kind of change does this PR introduce?
Chore. One entry added to the www sitemap generator.
## What is the current behavior?
`https://supabase.com/evals` is missing from `sitemap_www.xml`, so
search crawlers are never told the page exists. `robots.txt` doesn't
block it, they just have no way to find it from the sitemap.
The reason is that `/evals` is served by a separate Vercel project and
only reaches supabase.com through a proxy rewrite in
`apps/www/lib/rewrites.js`:
```js
{
source: '/evals',
destination: 'https://supabase-evals.vercel.app',
},
```
`apps/www/internals/generate-sitemap.mjs` builds its URL list by
globbing local route source files (`pages/**`, `_blog/*.mdx`,
prerendered `.next/server/pages/**`, etc.) and never resolves rewrites.
There is no page file behind `/evals`, so the globs can't discover it.
Closes GROWTH-1113.
## What is the new behavior?
`https://supabase.com/evals` appears once in the generated
`sitemap_www.xml`, with the same `weekly` and
`0.5` as every other entry in the file (no entry in
this sitemap carries a ``).
The entry is a small named const spread into the final `urlset` join,
next to `changelogDetailUrls` — the existing precedent in this file for
URLs with no page file behind them. Nothing else in the script changed,
and the sitemap index output (`sitemap.xml`) is byte-identical.
```diff
+ // /evals is a separate app proxied onto supabase.com via a rewrite in lib/rewrites.js,
+ // so it has no page file for the globs above to find. Hardcode it here.
+ const proxiedAppUrls = [
+ `
+
+ https://supabase.com/evals
+ weekly
+ 0.5
+
+ `,
+ ]
+
const sitemap = `
- ${[...staticUrls, ...changelogDetailUrls].join('')}
+ ${[...staticUrls, ...changelogDetailUrls, ...proxiedAppUrls].join('')}
`
```
This only makes the URL discoverable. Whether the page content itself is
crawlable is separate work, tracked in the evals repo.
## Additional context
Verification, run locally against this branch. The generator runs
standalone (`node ./internals/generate-sitemap.mjs` from `apps/www`); a
missing `.next` just means the globs match fewer pages, and the missing
changelog RSS is caught internally.
I generated `sitemap_www.xml` from `master` and from this branch and
diffed the two. The added entry is the only difference:
```
3271a3272,3277
>
>
> https://supabase.com/evals
> weekly
> 0.5
>
```
Exactly one occurrence, with its neighbouring entry for context:
```
$ grep -c 'https://supabase.com/evals' public/sitemap_www.xml
1
https://supabase.com/termsweekly0.5https://supabase.com/evalsweekly0.5
```
Other checks:
- Both outputs parse as well-formed XML (Python `xml.dom.minidom`):
`sitemap_www.xml` has 545 `` elements, `sitemap.xml` parses OK.
- `sitemap.xml` (the sitemap index) is identical to the pre-change
output; `diff` reports no changes.
- `npx prettier --check internals/generate-sitemap.mjs` → "All matched
files use Prettier code style!"
- Both generated sitemaps are gitignored (`apps/www/.gitignore` lines
29-30), confirmed with `git check-ignore`. `git status` shows only
`apps/www/internals/generate-sitemap.mjs`, so no generated file is in
the commit.
- No test, snapshot, or fixture anywhere in the repo references the
sitemap generator, so there was nothing to run. Its only caller is
`apps/www`'s `postbuild` script.
Not run: `pnpm --filter=www build`. It fails during "Collecting page
data" on a clean `master` checkout in this environment too, so the
failure is pre-existing and unrelated, and this change needs no build to
verify.
Co-authored-by: Claude
---
apps/www/internals/generate-sitemap.mjs | 14 +++++++++++++-
1 file changed, 13 insertions(+), 1 deletion(-)
diff --git a/apps/www/internals/generate-sitemap.mjs b/apps/www/internals/generate-sitemap.mjs
index af7aa31cd73dd..939c6be54d1d6 100644
--- a/apps/www/internals/generate-sitemap.mjs
+++ b/apps/www/internals/generate-sitemap.mjs
@@ -139,10 +139,22 @@ async function generate() {
}
})()
+ // /evals is a separate app proxied onto supabase.com via a rewrite in lib/rewrites.js,
+ // so it has no page file for the globs above to find. Hardcode it here.
+ const proxiedAppUrls = [
+ `
+
+ https://supabase.com/evals
+ weekly
+ 0.5
+
+ `,
+ ]
+
const sitemap = `
- ${[...staticUrls, ...changelogDetailUrls].join('')}
+ ${[...staticUrls, ...changelogDetailUrls, ...proxiedAppUrls].join('')}
`
From bd76d7fc3460dd6a5665e0d04432ebb00f24235e Mon Sep 17 00:00:00 2001
From: Saxon Fletcher
Date: Thu, 20 Aug 2026 11:35:34 +1000
Subject: [PATCH 2/4] feat(studio): wrap assistant Edge Function approval in a
Confirm card (#49168)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
## I have read the
[CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md)
file.
YES
## What kind of change does this PR introduce?
Feature / UI refactor.
## What is the current behavior?
Assistant Edge Function approval nests `ConfirmFooter` under the
function block. `addToolApprovalResponse` is wired whenever state is
`approval-requested`, including automatic approvals.
## What is the new behavior?
Introduces a `Confirm` card that owns the frame, with the footer
attached below the body. Edge Function approval uses that card.
Interactive Approve/Deny only runs for manual `approval-requested` parts
(`!approval.isAutomatic`), matching the [AI SDK tool-approvals `useChat`
guidelines](https://ai-sdk.dev/docs/agents/tool-approvals).
SQL still uses `DisplayBlockRenderer` until #49170. `ConfirmFooter` is
inlined into `Confirm` so SQL can keep importing the named footer until
that PR.
## Additional context
Part of stack #49171. Base: `chore/ai-sdk-7` (#49167).
Notebook proposal Confirm wrapping is **not** in this stack — that file
lives on [#49159](https://github.com/supabase/supabase/pull/49159).
Follow up after that stack merges.
## Test plan
- [ ] Deploy-edge-function tool part shows Confirm with Skip / Deploy
- [ ] Existing-function replace warning still requires the second
confirm
- [ ] After approve, footer morphs to loading and buttons disable
- [ ] `Confirm.utils.test.ts` and `EdgeFunctionRenderer.test.tsx` pass
## Summary by CodeRabbit
* **New Features**
* Added confirmation cards for AI-assisted actions, including approve
and cancel controls.
* Improved handling of manual approval requests for SQL execution,
notebook changes, and Edge Function deployment.
* Added support for customizing report and Edge Function block styling.
* **Bug Fixes**
* Automatic approvals no longer appear as pending manual confirmations.
* Skipped SQL actions now provide clearer messaging.
* **Tests**
* Expanded coverage for approval states, confirmation controls, and
automatic decisions.
---------
Co-authored-by: Cursor
Co-authored-by: Claude Opus 5
---
.../ReportBlock/ReportBlockContainer.tsx | 7 +-
.../AIAssistant.utils.test.ts | 12 ++
.../ui/AIAssistantPanel/AIAssistant.utils.ts | 3 +-
.../ui/AIAssistantPanel/AssistantChat.tsx | 2 +-
.../ui/AIAssistantPanel/Confirm.tsx | 126 +++++++++++++++++
.../ui/AIAssistantPanel/Confirm.utils.test.ts | 132 ++++++++++++++++++
.../ui/AIAssistantPanel/Confirm.utils.ts | 86 ++++++++++++
.../ui/AIAssistantPanel/ConfirmFooter.tsx | 42 ------
.../AIAssistantPanel/DisplayBlockRenderer.tsx | 2 +-
.../EdgeFunctionRenderer.test.tsx | 18 ++-
.../AIAssistantPanel/EdgeFunctionRenderer.tsx | 45 +++---
.../ui/AIAssistantPanel/Message.Parts.tsx | 28 ++--
.../NotebookProposalRenderer.tsx | 9 +-
.../EdgeFunctionBlock/EdgeFunctionBlock.tsx | 3 +
apps/studio/lib/ai/message-utils.test.ts | 58 ++++++--
apps/studio/lib/ai/message-utils.ts | 36 ++++-
apps/studio/lib/ai/prompts.ts | 2 +-
17 files changed, 508 insertions(+), 103 deletions(-)
create mode 100644 apps/studio/components/ui/AIAssistantPanel/Confirm.tsx
create mode 100644 apps/studio/components/ui/AIAssistantPanel/Confirm.utils.test.ts
create mode 100644 apps/studio/components/ui/AIAssistantPanel/Confirm.utils.ts
delete mode 100644 apps/studio/components/ui/AIAssistantPanel/ConfirmFooter.tsx
diff --git a/apps/studio/components/interfaces/Reports/ReportBlock/ReportBlockContainer.tsx b/apps/studio/components/interfaces/Reports/ReportBlock/ReportBlockContainer.tsx
index 44e7ed8722c5c..d14e2862687f1 100644
--- a/apps/studio/components/interfaces/Reports/ReportBlock/ReportBlockContainer.tsx
+++ b/apps/studio/components/interfaces/Reports/ReportBlock/ReportBlockContainer.tsx
@@ -11,6 +11,7 @@ interface ReportBlockContainerProps {
draggable?: boolean
showDragHandle?: boolean
tooltip?: ReactNode
+ className?: string
onDragStart?: (e: DragEvent) => void
}
@@ -23,6 +24,7 @@ export const ReportBlockContainer = ({
draggable = false,
showDragHandle = false,
tooltip,
+ className,
onDragStart,
children,
}: PropsWithChildren) => {
@@ -35,7 +37,10 @@ export const ReportBlockContainer = ({
draggable={draggable}
unselectable={draggable ? 'on' : undefined}
onDragStart={onDragStart}
- className="h-full flex flex-col overflow-hidden bg-surface-100 border-overlay relative rounded-sm border shadow-xs"
+ className={cn(
+ 'h-full flex flex-col overflow-hidden bg-surface-100 border-overlay relative rounded-sm border shadow-xs',
+ className
+ )}
>
diff --git a/apps/studio/components/ui/AIAssistantPanel/AIAssistant.utils.test.ts b/apps/studio/components/ui/AIAssistantPanel/AIAssistant.utils.test.ts
index 2f28dda6bdb94..e7c9eaa7d25ef 100644
--- a/apps/studio/components/ui/AIAssistantPanel/AIAssistant.utils.test.ts
+++ b/apps/studio/components/ui/AIAssistantPanel/AIAssistant.utils.test.ts
@@ -127,6 +127,18 @@ describe('AIAssistant.utils.ts:hasPendingToolApproval', () => {
expect(hasPendingToolApproval(messages)).toBe(true)
})
+
+ test('Should ignore automatic approvals', () => {
+ const messages = createMessageWithPart({
+ type: 'tool-execute_sql',
+ toolCallId: 'call-1',
+ state: 'approval-requested',
+ input: { sql: 'select 1', label: 'Test query' },
+ approval: { id: 'approval-1', isAutomatic: true },
+ } as UIMessage['parts'][number])
+
+ expect(hasPendingToolApproval(messages)).toBe(false)
+ })
})
describe('AIAssistant.utils.ts:resolvePendingToolApprovalsAsDenied', () => {
diff --git a/apps/studio/components/ui/AIAssistantPanel/AIAssistant.utils.ts b/apps/studio/components/ui/AIAssistantPanel/AIAssistant.utils.ts
index 92d4b1d694eaa..a3cb9b63a3f47 100644
--- a/apps/studio/components/ui/AIAssistantPanel/AIAssistant.utils.ts
+++ b/apps/studio/components/ui/AIAssistantPanel/AIAssistant.utils.ts
@@ -15,6 +15,7 @@ import { databaseKeys } from '@/data/database/keys'
import { enumeratedTypesKeys } from '@/data/enumerated-types/keys'
import { handleError } from '@/data/fetchers'
import { tableKeys } from '@/data/tables/keys'
+import { isManualApprovalRequested } from '@/lib/ai/message-utils'
import { tryParseJson } from '@/lib/helpers'
import type { SqlSnippet } from '@/state/ai-assistant-state'
import { ResponseError } from '@/types'
@@ -85,7 +86,7 @@ export const hasPendingToolApproval = (messages: Pick {
if (message.role !== 'assistant') return false
- return message.parts?.some((part) => isToolUIPart(part) && part.state === 'approval-requested')
+ return message.parts?.some((part) => isManualApprovalRequested(part))
})
}
diff --git a/apps/studio/components/ui/AIAssistantPanel/AssistantChat.tsx b/apps/studio/components/ui/AIAssistantPanel/AssistantChat.tsx
index 5bdb8363df2f0..25d7f4cad6e53 100644
--- a/apps/studio/components/ui/AIAssistantPanel/AssistantChat.tsx
+++ b/apps/studio/components/ui/AIAssistantPanel/AssistantChat.tsx
@@ -194,7 +194,7 @@ export const AssistantChat = ({
addToolApprovalResponse,
stop,
regenerate,
- } = useChat({
+ } = useChat({
id: chatId,
...(chatInstance ? { chat: chatInstance } : {}),
sendAutomaticallyWhen: lastAssistantMessageIsCompleteWithApprovalResponses,
diff --git a/apps/studio/components/ui/AIAssistantPanel/Confirm.tsx b/apps/studio/components/ui/AIAssistantPanel/Confirm.tsx
new file mode 100644
index 0000000000000..8282bdce2ae3c
--- /dev/null
+++ b/apps/studio/components/ui/AIAssistantPanel/Confirm.tsx
@@ -0,0 +1,126 @@
+import { type PropsWithChildren } from 'react'
+import { Button, cn } from 'ui'
+
+import { getConfirmFooterBar } from './Confirm.utils'
+
+interface ConfirmFooterProps {
+ message: string
+ cancelLabel?: string
+ confirmLabel?: string
+ confirmLabelLoading?: string
+ isLoading?: boolean
+ isDisabled?: boolean
+ /** Escape hatch for consumers that attach the bar directly under their own frame. */
+ className?: string
+ onCancel?: () => void | Promise
+ onConfirm?: () => void | Promise
+}
+
+/** Action bar that sits at the bottom of `Confirm`. */
+export const ConfirmFooter = ({
+ message,
+ cancelLabel = 'Cancel',
+ confirmLabel = 'Confirm',
+ confirmLabelLoading = 'Working...',
+ isLoading = false,
+ isDisabled = false,
+ className,
+ onCancel,
+ onConfirm,
+}: ConfirmFooterProps) => {
+ const isInactive = isLoading || isDisabled
+
+ return (
+
+
{message}
+
+
+
+
+
+ )
+}
+
+interface ConfirmProps {
+ /**
+ * Result of `getManualToolApprovalConfirmState`. Interactive buttons only for
+ * `approval-requested`; `approval-responded` is the post-approve loading morph.
+ */
+ state?: string
+ message: string
+ cancelLabel?: string
+ confirmLabel?: string
+ confirmLabelLoading?: string
+ extraLoading?: boolean
+ isLoading?: boolean
+ /**
+ * Children fill the remaining height of the card (e.g. `QueryEditor` in viewport
+ * mode). Omit for content-sized bodies like notebook previews.
+ */
+ fill?: boolean
+ className?: string
+ onCancel?: () => void | Promise
+ onConfirm?: () => void | Promise
+}
+
+/**
+ * Card that wraps an assistant tool preview and optionally attaches a confirm footer
+ * below it. The card owns the frame; nested surfaces (QueryEditor viewport, unframed
+ * edge-function blocks) fill the body.
+ */
+export const Confirm = ({
+ children,
+ state,
+ message,
+ cancelLabel = 'Skip',
+ confirmLabel = 'Confirm',
+ confirmLabelLoading = 'Working...',
+ extraLoading = false,
+ isLoading = false,
+ fill = false,
+ className,
+ onCancel,
+ onConfirm,
+}: PropsWithChildren) => {
+ const bar = getConfirmFooterBar(state)
+ const showLoading = bar.isLoading || extraLoading || isLoading
+ const isApprovalRequested = state === 'approval-requested'
+
+ return (
+
+
+ {children}
+
+ {bar.show && (
+
+ )}
+
+ )
+}
diff --git a/apps/studio/components/ui/AIAssistantPanel/Confirm.utils.test.ts b/apps/studio/components/ui/AIAssistantPanel/Confirm.utils.test.ts
new file mode 100644
index 0000000000000..933e5c0c64c79
--- /dev/null
+++ b/apps/studio/components/ui/AIAssistantPanel/Confirm.utils.test.ts
@@ -0,0 +1,132 @@
+import { describe, expect, it, vi } from 'vitest'
+
+import {
+ getConfirmFooterBar,
+ getManualToolApprovalConfirmState,
+ getManualToolApprovalHandlers,
+ getManualToolApprovalId,
+ USER_SKIPPED_TOOL_REASON,
+} from './Confirm.utils'
+
+describe('getConfirmFooterBar', () => {
+ it('hides the bar when no approval state is provided', () => {
+ expect(getConfirmFooterBar()).toEqual({ show: false, isLoading: false })
+ })
+
+ it('shows the bar while approval is requested', () => {
+ expect(getConfirmFooterBar('approval-requested')).toEqual({ show: true, isLoading: false })
+ })
+
+ it('shows a loading bar after the user approves', () => {
+ expect(getConfirmFooterBar('approval-responded')).toEqual({ show: true, isLoading: true })
+ })
+
+ it('hides the bar for every other tool state', () => {
+ expect(getConfirmFooterBar('input-available')).toEqual({ show: false, isLoading: false })
+ expect(getConfirmFooterBar('output-available')).toEqual({ show: false, isLoading: false })
+ expect(getConfirmFooterBar('output-denied')).toEqual({ show: false, isLoading: false })
+ })
+})
+
+describe('getManualToolApprovalConfirmState', () => {
+ it('shows an interactive footer for a manual approval request', () => {
+ expect(
+ getManualToolApprovalConfirmState({
+ state: 'approval-requested',
+ approval: { id: 'approval-1' },
+ })
+ ).toBe('approval-requested')
+ })
+
+ it('keeps a loading footer after a manual approve', () => {
+ expect(
+ getManualToolApprovalConfirmState({
+ state: 'approval-responded',
+ approval: { id: 'approval-1', approved: true },
+ })
+ ).toBe('approval-responded')
+ })
+
+ it('hides the footer for automatic approvals', () => {
+ expect(
+ getManualToolApprovalConfirmState({
+ state: 'approval-requested',
+ approval: { id: 'approval-1', isAutomatic: true },
+ })
+ ).toBeUndefined()
+ expect(
+ getManualToolApprovalConfirmState({
+ state: 'approval-responded',
+ approval: { id: 'approval-1', approved: true, isAutomatic: true },
+ })
+ ).toBeUndefined()
+ })
+
+ it('hides the footer when the user denied the request', () => {
+ expect(
+ getManualToolApprovalConfirmState({
+ state: 'approval-responded',
+ approval: { id: 'approval-1', approved: false },
+ })
+ ).toBeUndefined()
+ })
+
+ it('ignores non-approval tool states', () => {
+ expect(getManualToolApprovalConfirmState({ state: 'input-available' })).toBeUndefined()
+ expect(getManualToolApprovalConfirmState({ state: 'output-available' })).toBeUndefined()
+ expect(getManualToolApprovalConfirmState({ state: 'output-denied' })).toBeUndefined()
+ })
+})
+
+describe('getManualToolApprovalId', () => {
+ it('returns the approval id only for a manual approval-requested part', () => {
+ expect(
+ getManualToolApprovalId({ state: 'approval-requested', approval: { id: 'approval-1' } })
+ ).toBe('approval-1')
+ expect(
+ getManualToolApprovalId({
+ state: 'approval-requested',
+ approval: { id: 'approval-1', isAutomatic: true },
+ })
+ ).toBeUndefined()
+ expect(
+ getManualToolApprovalId({
+ state: 'approval-responded',
+ approval: { id: 'approval-1', approved: true },
+ })
+ ).toBeUndefined()
+ })
+})
+
+describe('getManualToolApprovalHandlers', () => {
+ it('wires approve and deny only while a manual approval is requested', () => {
+ const addToolApprovalResponse = vi.fn()
+ const { confirmState, onApprove, onDeny } = getManualToolApprovalHandlers({
+ state: 'approval-requested',
+ approval: { id: 'approval-1' },
+ addToolApprovalResponse,
+ })
+
+ expect(confirmState).toBe('approval-requested')
+ onApprove?.()
+ onDeny?.()
+ expect(addToolApprovalResponse).toHaveBeenCalledWith({ id: 'approval-1', approved: true })
+ expect(addToolApprovalResponse).toHaveBeenCalledWith({
+ id: 'approval-1',
+ approved: false,
+ reason: USER_SKIPPED_TOOL_REASON,
+ })
+ })
+
+ it('does not call addToolApprovalResponse for automatic approvals', () => {
+ const addToolApprovalResponse = vi.fn()
+ const handlers = getManualToolApprovalHandlers({
+ state: 'approval-requested',
+ approval: { id: 'approval-1', isAutomatic: true },
+ addToolApprovalResponse,
+ })
+
+ expect(handlers).toEqual({ confirmState: undefined })
+ expect(addToolApprovalResponse).not.toHaveBeenCalled()
+ })
+})
diff --git a/apps/studio/components/ui/AIAssistantPanel/Confirm.utils.ts b/apps/studio/components/ui/AIAssistantPanel/Confirm.utils.ts
new file mode 100644
index 0000000000000..9ce728c929417
--- /dev/null
+++ b/apps/studio/components/ui/AIAssistantPanel/Confirm.utils.ts
@@ -0,0 +1,86 @@
+export type ConfirmFooterApprovalState = 'approval-requested' | 'approval-responded'
+
+/** Sent with Skip so the model sees a user choice, not the SDK default "Tool execution denied." */
+export const USER_SKIPPED_TOOL_REASON = 'The user skipped this action.'
+
+export type ToolApprovalFields = {
+ id?: string
+ approved?: boolean
+ /** AI SDK v7: automatic policy decisions must not get a confirm footer or response. */
+ isAutomatic?: boolean
+}
+
+/**
+ * Whether the confirm bar should render, and whether it is in the post-approve loading
+ * morph. Driven by the AI SDK tool approval state; any other state hides the bar.
+ */
+export function getConfirmFooterBar(state?: string): { show: boolean; isLoading: boolean } {
+ if (state === 'approval-requested') return { show: true, isLoading: false }
+ if (state === 'approval-responded') return { show: true, isLoading: true }
+ return { show: false, isLoading: false }
+}
+
+/**
+ * Maps a tool part onto the confirm footer. Follows the AI SDK `useChat` rule:
+ * interactive Approve/Deny only for `approval-requested` when `!approval.isAutomatic`.
+ * `approval-responded` keeps a loading morph after a manual approve; denials and
+ * automatic decisions hide the bar.
+ *
+ * @see https://ai-sdk.dev/docs/agents/tool-approvals
+ */
+export function getManualToolApprovalConfirmState({
+ state,
+ approval,
+}: {
+ state: string
+ approval?: ToolApprovalFields
+}): ConfirmFooterApprovalState | undefined {
+ if (approval?.isAutomatic) return undefined
+ if (state === 'approval-requested') return 'approval-requested'
+ if (state === 'approval-responded' && approval?.approved !== false) return 'approval-responded'
+ return undefined
+}
+
+export function getManualToolApprovalId({
+ state,
+ approval,
+}: {
+ state: string
+ approval?: ToolApprovalFields
+}): string | undefined {
+ if (state !== 'approval-requested' || approval?.isAutomatic) return undefined
+ return approval?.id
+}
+
+export function getManualToolApprovalHandlers({
+ state,
+ approval,
+ addToolApprovalResponse,
+}: {
+ state: string
+ approval?: ToolApprovalFields
+ addToolApprovalResponse?: (args: {
+ id: string
+ approved: boolean
+ reason?: string
+ }) => void | PromiseLike
+}): {
+ confirmState?: ConfirmFooterApprovalState
+ onApprove?: () => void
+ onDeny?: () => void
+} {
+ const confirmState = getManualToolApprovalConfirmState({ state, approval })
+ const approvalId = getManualToolApprovalId({ state, approval })
+ if (!approvalId) return { confirmState }
+
+ return {
+ confirmState,
+ onApprove: () => addToolApprovalResponse?.({ id: approvalId, approved: true }),
+ onDeny: () =>
+ addToolApprovalResponse?.({
+ id: approvalId,
+ approved: false,
+ reason: USER_SKIPPED_TOOL_REASON,
+ }),
+ }
+}
diff --git a/apps/studio/components/ui/AIAssistantPanel/ConfirmFooter.tsx b/apps/studio/components/ui/AIAssistantPanel/ConfirmFooter.tsx
deleted file mode 100644
index cd0c61966ee2e..0000000000000
--- a/apps/studio/components/ui/AIAssistantPanel/ConfirmFooter.tsx
+++ /dev/null
@@ -1,42 +0,0 @@
-import { PropsWithChildren } from 'react'
-import { Button, cn } from 'ui'
-
-interface ConfirmFooterProps {
- message: string
- cancelLabel?: string
- confirmLabel?: string
- confirmLabelLoading?: string
- isLoading?: boolean
- onCancel?: () => void | Promise
- onConfirm?: () => void | Promise
-}
-
-export const ConfirmFooter = ({
- message,
- cancelLabel = 'Cancel',
- confirmLabel = 'Confirm',
- confirmLabelLoading = 'Working...',
- isLoading = false,
- onCancel,
- onConfirm,
-}: PropsWithChildren) => {
- return (
-
-
{message}
-
-
-
-
-
- )
-}
diff --git a/apps/studio/components/ui/AIAssistantPanel/DisplayBlockRenderer.tsx b/apps/studio/components/ui/AIAssistantPanel/DisplayBlockRenderer.tsx
index 908b947dc095f..bd2eb1620709a 100644
--- a/apps/studio/components/ui/AIAssistantPanel/DisplayBlockRenderer.tsx
+++ b/apps/studio/components/ui/AIAssistantPanel/DisplayBlockRenderer.tsx
@@ -8,7 +8,7 @@ import { useRef, useState, type DragEvent, type PropsWithChildren } from 'react'
import { DEFAULT_CHART_CONFIG, QueryBlock } from '../QueryBlock/QueryBlock'
import { identifyQueryType } from './AIAssistant.utils'
-import { ConfirmFooter } from './ConfirmFooter'
+import { ConfirmFooter } from './Confirm'
import { ChartConfig } from '@/components/interfaces/SQLEditor/UtilityPanel/ChartConfig'
import { entityTypeKeys } from '@/data/entity-types/keys'
import { lintKeys } from '@/data/lint/keys'
diff --git a/apps/studio/components/ui/AIAssistantPanel/EdgeFunctionRenderer.test.tsx b/apps/studio/components/ui/AIAssistantPanel/EdgeFunctionRenderer.test.tsx
index 0d22406570bce..bae71ec86c3d5 100644
--- a/apps/studio/components/ui/AIAssistantPanel/EdgeFunctionRenderer.test.tsx
+++ b/apps/studio/components/ui/AIAssistantPanel/EdgeFunctionRenderer.test.tsx
@@ -1,5 +1,6 @@
import { screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
+import { type ReactNode } from 'react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { EdgeFunctionRenderer } from './EdgeFunctionRenderer'
@@ -70,17 +71,22 @@ vi.mock('../EdgeFunctionBlock/EdgeFunctionBlock', () => ({
),
}))
-vi.mock('./ConfirmFooter', () => ({
- ConfirmFooter: ({
+vi.mock('./Confirm', () => ({
+ Confirm: ({
+ children,
confirmLabel,
onConfirm,
}: {
+ children?: ReactNode
confirmLabel?: string
onConfirm?: () => void
}) => (
-
+