Files
supabase/apps/studio/components/ui/AIAssistantPanel/Confirm.utils.ts
Charis 144c2eadb6 fix(studio): expose notebook diff validation errors for auto-retry (#49331)
## Summary

- Automatically deny notebook tool proposals with specific error reasons
when client-side diff validation fails (e.g., unknown cell ID)
- Enables the AI Assistant to see the actual failure reason and retry
automatically instead of requiring manual user intervention
- Exposes the same `describeNotebookOperationError` helper used
server-side for consistent error messaging
- Adds `denyWithReason()` to manual tool approval handlers for flexible
denial messaging

## Test plan

- Run `pnpm --filter studio exec vitest run
apps/studio/components/ui/AIAssistantPanel/Confirm.utils.test.ts` to
verify denyWithReason tests
- Run `pnpm --filter studio exec vitest run
apps/studio/components/ui/AIAssistantPanel/NotebookProposalRenderer.test.tsx`
to verify auto-deny behavior, Skip fallback, and no re-fire after
approval is already handled
- Confirm no regressions in existing notebook tool approval flows

## Manual testing

- Get assistant to create a notebook.
- Open the notebook and manually delete a cell yourself.
- Ask the assistant to delete the cell you just deleted.
- Assistant should automatically recover from the error.

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

## Summary by CodeRabbit

* **New Features**
* Notebook proposals now display clear success, error, and denial
outcomes.
* Tool errors for SQL, Edge Functions, and notebooks now appear in their
respective result views.
  * Output links are supported in notebook proposal results.
* Approval panels remain visible after completed actions with
standardized status messages.

* **Bug Fixes**
* Specific denial reasons are preserved instead of showing a generic
skipped message.
* Unapplyable notebook updates are automatically denied with an
explanation.
  * Prevented duplicate denial responses after approval decisions.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-08-21 10:35:56 -04:00

107 lines
3.6 KiB
TypeScript

export type ConfirmFooterApprovalState =
| 'approval-requested'
| 'approval-responded'
| 'success'
| 'error'
| 'denied'
/** 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 loading or in a terminal state.
* Driven by the AI SDK tool approval state; any other state hides the bar.
*/
export function getConfirmFooterBar(state?: ConfirmFooterApprovalState): {
show: boolean
isLoading: boolean
outcome?: 'success' | 'error' | 'denied'
} {
if (state === 'approval-requested') return { show: true, isLoading: false }
if (state === 'approval-responded') return { show: true, isLoading: true }
if (state === 'success' || state === 'error' || state === 'denied') {
return { show: true, isLoading: false, outcome: state }
}
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. Completed manual
* approvals stay visible as a terminal outcome; automatic decisions never get a footer.
*
* @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'
if (state === 'output-available' && approval?.approved === true) return 'success'
if (state === 'output-error' && approval?.approved === true) return 'error'
if (state === 'output-denied' && approval?.approved === false) return 'denied'
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<void>
}): {
confirmState?: ConfirmFooterApprovalState
onApprove?: () => void
onDeny?: () => void
/** Deny with a specific reason instead of USER_SKIPPED_TOOL_REASON, e.g. for an automatic
* denial that should tell the model what went wrong rather than that the user skipped it. */
denyWithReason?: (reason: string) => 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,
}),
denyWithReason: (reason: string) =>
addToolApprovalResponse?.({ id: approvalId, approved: false, reason }),
}
}