Files
supabase/apps/studio/components/ui/EdgeFunctionBlock/EdgeFunctionBlock.tsx
Saxon Fletcher bd76d7fc34 feat(studio): wrap assistant Edge Function approval in a Confirm card (#49168)
<img width="1512" height="862" alt="image"
src="https://github.com/user-attachments/assets/79a6d4dc-dcd2-489f-97d7-3ee7a0196b7d"
/>


## 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

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## 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.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 11:35:34 +10:00

197 lines
6.1 KiB
TypeScript

import { Code } from 'lucide-react'
import Link from 'next/link'
import type { DragEvent, ReactNode } from 'react'
import { Button, cn } from 'ui'
import { Admonition } from 'ui-patterns/Admonition'
import { CodeBlock, type CodeBlockLang } from 'ui-patterns/CodeBlock'
import { ReportBlockContainer } from '@/components/interfaces/Reports/ReportBlock/ReportBlockContainer'
interface EdgeFunctionBlockProps {
/** Title of the EdgeFunctionBlock */
label: string
/** Function code to display */
code: string
/** Function name/slug */
functionName: string
/** Any other actions specific to the parent to be rendered in the header */
actions?: ReactNode
/** Toggle visiblity of code on render */
showCode?: boolean
/** Whether function block is draggable */
draggable?: boolean
/** Tooltip when hovering over the header of the block */
tooltip?: ReactNode
/** Optional callback on drag start */
onDragStart?: (e: DragEvent) => void
/** Hide the header deploy button (used when an external confirm footer is shown) */
hideDeployButton?: boolean
/** Disable interactive actions */
disabled?: boolean
/** Whether a deploy action is currently running */
isDeploying?: boolean
/** Whether a deploy action has completed */
isDeployed?: boolean
/** Optional message to show when deployment fails */
errorText?: string
/** URL to the deployed function */
functionUrl?: string
/** Link to the function details page */
deploymentDetailsUrl?: string
/** CLI command to download the function */
downloadCommand?: string
/** Show warning UI when replacing an existing function */
showReplaceWarning?: boolean
/** Cancel handler when replacing an existing function */
onCancelReplace?: () => void
/** Confirm handler when replacing an existing function */
onConfirmReplace?: () => void
/** Handler for triggering a deploy */
onDeploy?: () => void
className?: string
}
export const EdgeFunctionBlock = ({
label,
code,
functionName,
actions,
tooltip,
hideDeployButton = false,
disabled = false,
isDeploying = false,
isDeployed = false,
errorText,
functionUrl,
deploymentDetailsUrl,
downloadCommand,
showReplaceWarning = false,
onCancelReplace,
onConfirmReplace,
onDeploy,
draggable = false,
onDragStart,
className,
}: EdgeFunctionBlockProps) => {
const resolvedFunctionUrl = functionUrl ?? 'Function URL will be available after deployment'
const resolvedDownloadCommand = downloadCommand ?? `supabase functions download ${functionName}`
const hasStatusMessage = isDeploying || isDeployed || !!errorText
return (
<ReportBlockContainer
tooltip={tooltip}
icon={<Code size={16} strokeWidth={1.5} className="text-foreground-muted" />}
label={label}
loading={isDeploying}
draggable={draggable}
onDragStart={onDragStart}
className={className}
actions={
hideDeployButton || !onDeploy ? (
(actions ?? null)
) : (
<>
<Button
variant="outline"
size="tiny"
loading={isDeploying}
disabled={disabled || isDeploying}
onClick={onDeploy}
>
{isDeploying ? 'Deploying...' : 'Deploy'}
</Button>
{actions}
</>
)
}
>
{showReplaceWarning && (
<Admonition
type="warning"
className="rounded-none border-0 border-b shrink-0 bg-background-100"
>
<p>An edge function with the name "{functionName}" already exists.</p>
<p className="text-foreground-light">
Deploying will replace the existing function. Are you sure you want to proceed?
</p>
<div className="flex justify-stretch mt-2 gap-2">
<Button
variant="outline"
size="tiny"
className="w-full flex-1"
disabled={isDeploying}
onClick={onCancelReplace}
>
Cancel
</Button>
<Button
variant="danger"
size="tiny"
className="w-full flex-1"
loading={isDeploying}
disabled={isDeploying}
onClick={onConfirmReplace}
>
Replace function
</Button>
</div>
</Admonition>
)}
<div className="shrink-0 w-full max-h-96 overflow-y-auto">
<CodeBlock
hideLineNumbers
wrapLines={false}
value={code}
language={'typescript' as CodeBlockLang}
className={cn(
'max-w-none block bg-transparent! py-3! px-3.5! prose dark:prose-dark border-0 text-foreground rounded-none! w-full',
'[&>code]:m-0 [&>code>span]:text-foreground'
)}
/>
</div>
{hasStatusMessage && (
<div className="p-4 w-full border-t bg-surface-75 text-xs">
{isDeploying ? (
<p className="text-foreground-light">Deploying function...</p>
) : errorText ? (
<p className="text-danger">{errorText}</p>
) : (
<>
<p className="text-foreground-light mb-2">
The{' '}
{deploymentDetailsUrl ? (
<Link className="text-foreground" href={deploymentDetailsUrl}>
new function
</Link>
) : (
<span className="text-foreground">new function</span>
)}{' '}
is now live at:
</p>
<CodeBlock
language="bash"
hideLineNumbers
value={resolvedFunctionUrl}
className="text-xs p-2"
/>
<p className="text-foreground-light mt-4 mb-2">
To download and work on this function locally, use the CLI command:
</p>
<CodeBlock
hideLineNumbers
language="bash"
value={resolvedDownloadCommand}
className="text-xs p-2"
/>
</>
)}
</div>
)}
</ReportBlockContainer>
)
}