diff --git a/apps/studio/components/ui/AIAssistantPanel/AssistantQueryCell.tsx b/apps/studio/components/ui/AIAssistantPanel/AssistantQueryCell.tsx
index 6369797c915..4c143f627d9 100644
--- a/apps/studio/components/ui/AIAssistantPanel/AssistantQueryCell.tsx
+++ b/apps/studio/components/ui/AIAssistantPanel/AssistantQueryCell.tsx
@@ -133,6 +133,9 @@ export const AssistantQueryCell = ({
cancelLabel="Skip"
confirmLabel="Run query"
confirmLabelLoading="Running..."
+ successMessage="Query executed"
+ errorMessage="Failed to execute SQL"
+ deniedMessage="Skipped query"
onCancel={onDeny}
onConfirm={onApprove}
>
diff --git a/apps/studio/components/ui/AIAssistantPanel/Confirm.test.tsx b/apps/studio/components/ui/AIAssistantPanel/Confirm.test.tsx
new file mode 100644
index 00000000000..79f628e686c
--- /dev/null
+++ b/apps/studio/components/ui/AIAssistantPanel/Confirm.test.tsx
@@ -0,0 +1,30 @@
+import { render, screen } from '@testing-library/react'
+import { describe, expect, it } from 'vitest'
+
+import { Confirm } from './Confirm'
+
+describe('Confirm', () => {
+ it('keeps its content visible and replaces actions with a success status', () => {
+ render(
+
+ Query preview
+
+ )
+
+ expect(screen.getByText('Query preview')).toBeInTheDocument()
+ expect(screen.getByText('Query executed')).toBeInTheDocument()
+ expect(screen.queryByRole('button')).not.toBeInTheDocument()
+ })
+
+ it('keeps its content visible and replaces actions with an error status', () => {
+ render(
+
+ Query preview
+
+ )
+
+ expect(screen.getByText('Query preview')).toBeInTheDocument()
+ expect(screen.getByText('Failed to execute SQL')).toBeInTheDocument()
+ expect(screen.queryByRole('button')).not.toBeInTheDocument()
+ })
+})
diff --git a/apps/studio/components/ui/AIAssistantPanel/Confirm.tsx b/apps/studio/components/ui/AIAssistantPanel/Confirm.tsx
index a3a7d26c975..fc02c4367de 100644
--- a/apps/studio/components/ui/AIAssistantPanel/Confirm.tsx
+++ b/apps/studio/components/ui/AIAssistantPanel/Confirm.tsx
@@ -1,7 +1,8 @@
-import { type PropsWithChildren } from 'react'
+import { Check, X } from 'lucide-react'
+import { type PropsWithChildren, type ReactNode } from 'react'
import { Button, cn } from 'ui'
-import { getConfirmFooterBar } from './Confirm.utils'
+import { getConfirmFooterBar, type ConfirmFooterApprovalState } from './Confirm.utils'
interface ConfirmFooterProps {
message: string
@@ -10,6 +11,9 @@ interface ConfirmFooterProps {
confirmLabelLoading?: string
isLoading?: boolean
isDisabled?: boolean
+ outcome?: 'success' | 'error' | 'denied'
+ showActions?: boolean
+ action?: ReactNode
/** Omit the confirm button so only Skip remains (unparseable / unapplyable previews). */
denyOnly?: boolean
/** Escape hatch for consumers that attach the bar directly under their own frame. */
@@ -26,6 +30,9 @@ export const ConfirmFooter = ({
confirmLabelLoading = 'Working...',
isLoading = false,
isDisabled = false,
+ outcome,
+ showActions = true,
+ action,
denyOnly = false,
className,
onCancel,
@@ -41,17 +48,28 @@ export const ConfirmFooter = ({
className
)}
>
-
{message}
-
-
- {!denyOnly && (
-
- )}
+
+ {outcome === 'success' && }
+ {outcome === 'error' && }
+ {message}
+ {(showActions || action) && (
+
+ {showActions && (
+ <>
+
+ {!denyOnly && (
+
+ )}
+ >
+ )}
+ {action}
+
+ )}
)
}
@@ -61,11 +79,15 @@ interface ConfirmProps {
* Result of `getManualToolApprovalConfirmState`. Interactive buttons only for
* `approval-requested`; `approval-responded` is the post-approve loading morph.
*/
- state?: string
+ state?: ConfirmFooterApprovalState
message: string
cancelLabel?: string
confirmLabel?: string
confirmLabelLoading?: string
+ successMessage?: string
+ errorMessage?: string
+ deniedMessage?: string
+ footerAction?: ReactNode
extraLoading?: boolean
isLoading?: boolean
/**
@@ -93,6 +115,10 @@ export const Confirm = ({
cancelLabel = 'Skip',
confirmLabel = 'Confirm',
confirmLabelLoading = 'Working...',
+ successMessage,
+ errorMessage,
+ deniedMessage,
+ footerAction,
extraLoading = false,
isLoading = false,
fill = false,
@@ -104,6 +130,14 @@ export const Confirm = ({
const bar = getConfirmFooterBar(state)
const showLoading = bar.isLoading || extraLoading || isLoading
const isApprovalRequested = state === 'approval-requested'
+ const footerMessage =
+ bar.outcome === 'success'
+ ? (successMessage ?? message)
+ : bar.outcome === 'error'
+ ? (errorMessage ?? message)
+ : bar.outcome === 'denied'
+ ? (deniedMessage ?? message)
+ : message
return (
{bar.show && (
{
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 })
+ it('shows a terminal outcome after a completed approval', () => {
+ expect(getConfirmFooterBar('success')).toEqual({
+ show: true,
+ isLoading: false,
+ outcome: 'success',
+ })
+ expect(getConfirmFooterBar('error')).toEqual({
+ show: true,
+ isLoading: false,
+ outcome: 'error',
+ })
+ expect(getConfirmFooterBar('denied')).toEqual({
+ show: true,
+ isLoading: false,
+ outcome: 'denied',
+ })
})
})
@@ -47,6 +59,27 @@ describe('getManualToolApprovalConfirmState', () => {
).toBe('approval-responded')
})
+ it('keeps a terminal footer after a manual tool completes', () => {
+ expect(
+ getManualToolApprovalConfirmState({
+ state: 'output-available',
+ approval: { id: 'approval-1', approved: true },
+ })
+ ).toBe('success')
+ expect(
+ getManualToolApprovalConfirmState({
+ state: 'output-error',
+ approval: { id: 'approval-1', approved: true },
+ })
+ ).toBe('error')
+ expect(
+ getManualToolApprovalConfirmState({
+ state: 'output-denied',
+ approval: { id: 'approval-1', approved: false },
+ })
+ ).toBe('denied')
+ })
+
it('hides the footer for automatic approvals', () => {
expect(
getManualToolApprovalConfirmState({
@@ -71,9 +104,10 @@ describe('getManualToolApprovalConfirmState', () => {
).toBeUndefined()
})
- it('ignores non-approval tool states', () => {
+ it('ignores terminal states that were not manually approved', () => {
expect(getManualToolApprovalConfirmState({ state: 'input-available' })).toBeUndefined()
expect(getManualToolApprovalConfirmState({ state: 'output-available' })).toBeUndefined()
+ expect(getManualToolApprovalConfirmState({ state: 'output-error' })).toBeUndefined()
expect(getManualToolApprovalConfirmState({ state: 'output-denied' })).toBeUndefined()
})
})
diff --git a/apps/studio/components/ui/AIAssistantPanel/Confirm.utils.ts b/apps/studio/components/ui/AIAssistantPanel/Confirm.utils.ts
index 9ce728c9294..d6061f4620e 100644
--- a/apps/studio/components/ui/AIAssistantPanel/Confirm.utils.ts
+++ b/apps/studio/components/ui/AIAssistantPanel/Confirm.utils.ts
@@ -1,4 +1,9 @@
-export type ConfirmFooterApprovalState = 'approval-requested' | 'approval-responded'
+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.'
@@ -11,20 +16,27 @@ export type ToolApprovalFields = {
}
/**
- * 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.
+ * 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?: string): { show: boolean; isLoading: boolean } {
+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; denials and
- * automatic decisions hide the bar.
+ * `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
*/
@@ -38,6 +50,9 @@ export function getManualToolApprovalConfirmState({
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
}
diff --git a/apps/studio/components/ui/AIAssistantPanel/EdgeFunctionRenderer.tsx b/apps/studio/components/ui/AIAssistantPanel/EdgeFunctionRenderer.tsx
index d22e46c9cd5..5b866559f45 100644
--- a/apps/studio/components/ui/AIAssistantPanel/EdgeFunctionRenderer.tsx
+++ b/apps/studio/components/ui/AIAssistantPanel/EdgeFunctionRenderer.tsx
@@ -16,6 +16,7 @@ interface EdgeFunctionRendererProps {
onDeny?: () => void
isDeploying?: boolean
initialIsDeployed?: boolean
+ errorText?: string
confirmState?: ConfirmFooterApprovalState
}
@@ -27,6 +28,7 @@ export const EdgeFunctionRenderer = ({
onDeny,
isDeploying = false,
initialIsDeployed,
+ errorText,
confirmState,
}: EdgeFunctionRendererProps) => {
const { ref } = useParams()
@@ -85,6 +87,9 @@ export const EdgeFunctionRenderer = ({
cancelLabel="Skip"
confirmLabel="Deploy"
confirmLabelLoading="Deploying..."
+ successMessage="Edge Function deployed"
+ errorMessage="Failed to deploy Edge Function"
+ deniedMessage="Skipped Edge Function deployment"
isLoading={isDeploying}
onCancel={onDeny}
onConfirm={handleDeploy}
@@ -97,6 +102,7 @@ export const EdgeFunctionRenderer = ({
disabled={isConfirming}
isDeploying={isDeploying}
isDeployed={initialIsDeployed}
+ errorText={errorText}
functionUrl={functionUrl}
deploymentDetailsUrl={deploymentDetailsUrl}
downloadCommand={downloadCommand}
diff --git a/apps/studio/components/ui/AIAssistantPanel/Message.Parts.tsx b/apps/studio/components/ui/AIAssistantPanel/Message.Parts.tsx
index b3b4fe25584..821f1d7cbfa 100644
--- a/apps/studio/components/ui/AIAssistantPanel/Message.Parts.tsx
+++ b/apps/studio/components/ui/AIAssistantPanel/Message.Parts.tsx
@@ -110,24 +110,17 @@ function ToolDisplayExecuteSqlLoading({ label = 'Writing SQL...' }: { label?: st
)
}
-function ToolDisplayExecuteSqlFailure() {
- return Failed to execute SQL.
-}
-
function MessagePartExecuteSql({ toolPart }: { toolPart: ToolUIPart }) {
const { id } = useMessageInfoContext()
const { addToolApprovalResponse } = useMessageActionsContext()
- const { toolCallId, state, input, output } = toolPart
+ const { toolCallId, state, input: submittedInput, output } = toolPart
+ const input = state === 'output-error' ? (submittedInput ?? toolPart.rawInput) : submittedInput
if (state === 'input-streaming') {
return
}
- if (state === 'output-error') {
- return
- }
-
const { data: chart, success } = parseExecuteSqlChartResult(input)
if (!success) return null
@@ -136,7 +129,8 @@ function MessagePartExecuteSql({ toolPart }: { toolPart: ToolUIPart }) {
state === 'approval-requested' ||
state === 'approval-responded' ||
state === 'output-denied' ||
- state === 'output-available'
+ state === 'output-available' ||
+ state === 'output-error'
) {
const { confirmState, onApprove, onDeny } = getManualToolApprovalHandlers({
state,
@@ -150,7 +144,11 @@ function MessagePartExecuteSql({ toolPart }: { toolPart: ToolUIPart }) {
id={`${id}-${toolCallId}`}
sql={chart.sql}
title={chart.label}
- initialResult={toAssistantQueryResult(output)}
+ initialResult={
+ state === 'output-error'
+ ? { rows: [], error: { message: toolPart.errorText ?? 'Failed to execute SQL' } }
+ : toAssistantQueryResult(output)
+ }
view={chart.view}
xAxis={chart.xAxis}
yAxis={chart.yAxis}
@@ -171,10 +169,12 @@ const TOOL_DEPLOY_EDGE_FUNCTION_STATES_WITH_INPUT = new Set([
'approval-responded',
'output-denied',
'output-available',
+ 'output-error',
])
function MessagePartDeployEdgeFunction({ toolPart }: { toolPart: ToolUIPart }) {
- const { state, input, output } = toolPart
+ const { state, input: submittedInput, output } = toolPart
+ const input = state === 'output-error' ? (submittedInput ?? toolPart.rawInput) : submittedInput
const { addToolApprovalResponse } = useMessageActionsContext()
if (state === 'input-streaming') {
@@ -186,10 +186,6 @@ function MessagePartDeployEdgeFunction({ toolPart }: { toolPart: ToolUIPart }) {
)
}
- if (state === 'output-error') {
- return Failed to deploy Edge Function.
- }
-
if (!TOOL_DEPLOY_EDGE_FUNCTION_STATES_WITH_INPUT.has(state)) return null
const parsedInput = deployEdgeFunctionInputSchema.safeParse(input)
@@ -213,6 +209,7 @@ function MessagePartDeployEdgeFunction({ toolPart }: { toolPart: ToolUIPart }) {
confirmState={confirmState}
isDeploying={confirmState === 'approval-responded'}
initialIsDeployed={isInitiallyDeployed}
+ errorText={state === 'output-error' ? toolPart.errorText : undefined}
onApprove={onApprove}
onDeny={onDeny}
/>
@@ -224,11 +221,6 @@ const NOTEBOOK_DRAFTING_LABEL: Record = {
update: 'Drafting notebook update...',
}
-const NOTEBOOK_FAILED_LABEL: Record = {
- create: 'Failed to create notebook.',
- update: 'Failed to update notebook.',
-}
-
function MessagePartNotebookProposal({
toolPart,
mode,
@@ -236,7 +228,8 @@ function MessagePartNotebookProposal({
toolPart: ToolUIPart
mode: NotebookProposalMode
}) {
- const { state, input, output } = toolPart
+ const { state, input: submittedInput, output } = toolPart
+ const input = state === 'output-error' ? (submittedInput ?? toolPart.rawInput) : submittedInput
const { addToolApprovalResponse } = useMessageActionsContext()
if (state === 'input-streaming') {
@@ -248,10 +241,6 @@ function MessagePartNotebookProposal({
)
}
- if (state === 'output-error') {
- return {NOTEBOOK_FAILED_LABEL[mode]}
- }
-
const { confirmState, onApprove, onDeny } = getManualToolApprovalHandlers({
state,
approval: toolPart.approval,
diff --git a/apps/studio/components/ui/AIAssistantPanel/MessagePartQueryLogs.tsx b/apps/studio/components/ui/AIAssistantPanel/MessagePartQueryLogs.tsx
index 9c3976f79d4..c3b2277a9ae 100644
--- a/apps/studio/components/ui/AIAssistantPanel/MessagePartQueryLogs.tsx
+++ b/apps/studio/components/ui/AIAssistantPanel/MessagePartQueryLogs.tsx
@@ -10,15 +10,11 @@ import {
toQueryLogsResult,
} from './MessagePartQueryLogs.utils'
-type QueryLogsToolPart = Pick
-
-function QueryLogsFailure() {
- return Failed to query logs.
-}
+type QueryLogsToolPart = Pick
export function MessagePartQueryLogs({ toolPart }: { toolPart: QueryLogsToolPart }) {
const { id } = useMessageInfoContext()
- const { toolCallId, state, input, output } = toolPart
+ const { toolCallId, state, input: submittedInput, output } = toolPart
if (state === 'input-streaming' || state === 'input-available') {
return (
@@ -29,14 +25,16 @@ export function MessagePartQueryLogs({ toolPart }: { toolPart: QueryLogsToolPart
)
}
- if (state === 'output-error') return
- if (state !== 'output-available') return null
+ if (state !== 'output-available' && state !== 'output-error') return null
- const parsedInput = parseQueryLogsInput(input)
- const result = toQueryLogsResult(output)
- if (!parsedInput.success || !result) {
- return
- }
+ const parsedInput = parseQueryLogsInput(submittedInput)
+ if (!parsedInput.success) return null
+
+ const result =
+ state === 'output-error'
+ ? { rows: [], error: { message: toolPart.errorText ?? 'Failed to query logs' } }
+ : toQueryLogsResult(output)
+ if (!result) return null
return (
@@ -52,6 +50,7 @@ export function MessagePartQueryLogs({ toolPart }: { toolPart: QueryLogsToolPart
),
}}
initialResult={result}
+ confirmState={state === 'output-error' ? 'error' : undefined}
/>
)
diff --git a/apps/studio/components/ui/AIAssistantPanel/NotebookProposalRenderer.test.tsx b/apps/studio/components/ui/AIAssistantPanel/NotebookProposalRenderer.test.tsx
index df86f4795e2..ac50ecb61d6 100644
--- a/apps/studio/components/ui/AIAssistantPanel/NotebookProposalRenderer.test.tsx
+++ b/apps/studio/components/ui/AIAssistantPanel/NotebookProposalRenderer.test.tsx
@@ -151,18 +151,53 @@ describe('NotebookProposalRenderer', () => {
expect(onApprove).not.toHaveBeenCalled()
})
- it('renders an Open notebook link once output is available', () => {
+ it('keeps the create preview and marks it successful once output is available', () => {
render(
)
- const link = screen.getByRole('link', { name: 'Open notebook' })
- expect(link).toHaveAttribute('href', `/project/default/explorer/notebook/${NOTEBOOK_ID}`)
+ expect(screen.getByRole('toolbar', { name: 'Notebook toolbar' })).toBeInTheDocument()
+ expect(screen.getByText('Signup funnel')).toBeInTheDocument()
+ expect(screen.getByText('Notebook created')).toBeInTheDocument()
+ expect(screen.queryByRole('button', { name: 'Create' })).not.toBeInTheDocument()
+ expect(screen.getByRole('link', { name: 'Open notebook' })).toHaveAttribute(
+ 'href',
+ `/project/default/explorer/notebook/${NOTEBOOK_ID}`
+ )
+ })
+
+ it('keeps the create preview and marks it failed when the tool errors', () => {
+ render(
+
+ )
+
+ expect(screen.getByRole('toolbar', { name: 'Notebook toolbar' })).toBeInTheDocument()
+ expect(screen.getByText('New notebook')).toBeInTheDocument()
+ expect(screen.getByText('Failed to create notebook')).toBeInTheDocument()
})
it('keeps the preview in the message after skip', () => {
diff --git a/apps/studio/components/ui/AIAssistantPanel/NotebookProposalRenderer.tsx b/apps/studio/components/ui/AIAssistantPanel/NotebookProposalRenderer.tsx
index e59c5d6aa06..b6ccfc94cf9 100644
--- a/apps/studio/components/ui/AIAssistantPanel/NotebookProposalRenderer.tsx
+++ b/apps/studio/components/ui/AIAssistantPanel/NotebookProposalRenderer.tsx
@@ -1,7 +1,7 @@
import { useParams } from 'common'
import { Loader2 } from 'lucide-react'
import Link from 'next/link'
-import { type PropsWithChildren } from 'react'
+import { type PropsWithChildren, type ReactNode } from 'react'
import { Button } from 'ui'
import { Admonition } from 'ui-patterns/Admonition'
import { CodeBlock } from 'ui-patterns/CodeBlock'
@@ -25,14 +25,13 @@ import { toWireNotebook } from '@/data/content/notebooks/notebook-schema'
export type NotebookProposalMode = 'create' | 'update'
-// input-streaming and output-error are handled by the caller before this component is
-// rendered — see MessagePartNotebookProposal in Message.Parts.tsx.
export type NotebookProposalState =
| 'input-available'
| 'approval-requested'
| 'approval-responded'
| 'output-denied'
| 'output-available'
+ | 'output-error'
export interface NotebookProposalRendererProps {
mode: NotebookProposalMode
@@ -45,7 +44,12 @@ export interface NotebookProposalRendererProps {
onDeny?: () => void
}
-type NotebookProposalStepProps = Omit
+type NotebookProposalStepProps = Omit<
+ NotebookProposalRendererProps,
+ 'mode' | 'output' | 'state'
+> & {
+ footerAction?: ReactNode
+}
const MODE_COPY = {
create: {
@@ -69,17 +73,23 @@ const MODE_COPY = {
* same way `AssistantQueryCell` wraps `QueryEditor`.
*/
export const NotebookProposalRenderer = (props: NotebookProposalRendererProps) => {
- const { mode, state, output } = props
+ const { ref } = useParams()
+ const { mode, state, input, output, confirmState, onApprove, onDeny } = props
+ const parsedOutput = notebookToolOutputSchema.safeParse(output)
+ const footerAction =
+ state === 'output-available' && parsedOutput.success && ref ? (
+
+ ) : undefined
- if (state === 'output-available') {
- return
- }
-
- const { input, confirmState, onApprove, onDeny } = props
return mode === 'create' ? (
@@ -87,33 +97,13 @@ export const NotebookProposalRenderer = (props: NotebookProposalRendererProps) =
)
}
-function NotebookOutputSummary({ mode, output }: { mode: NotebookProposalMode; output: unknown }) {
- const { ref } = useParams()
- const parsedOutput = notebookToolOutputSchema.safeParse(output)
- const label = MODE_COPY[mode].outputLabel
-
- return (
-
-
- {parsedOutput.success ? `${label}: ${parsedOutput.data.name}` : label}
-
- {parsedOutput.success && ref && (
-
- )}
-
- )
-}
-
interface NotebookConfirmProps {
mode: NotebookProposalMode
confirmState?: ConfirmFooterApprovalState
@@ -122,6 +112,7 @@ interface NotebookConfirmProps {
confirmLabelLoading?: string
extraLoading?: boolean
denyOnly?: boolean
+ footerAction?: ReactNode
onApprove?: () => void
onDeny?: () => void
}
@@ -135,6 +126,7 @@ function NotebookConfirm({
confirmLabelLoading,
extraLoading,
denyOnly,
+ footerAction,
onApprove,
onDeny,
children,
@@ -149,6 +141,10 @@ function NotebookConfirm({
cancelLabel="Skip"
confirmLabel={confirmLabel ?? copy.confirmLabel}
confirmLabelLoading={confirmLabelLoading ?? copy.confirmLabelLoading}
+ successMessage={copy.outputLabel}
+ errorMessage={`Failed to ${mode} notebook`}
+ deniedMessage={`Skipped notebook ${mode === 'create' ? 'creation' : 'update'}`}
+ footerAction={footerAction}
extraLoading={extraLoading}
denyOnly={denyOnly}
onCancel={onDeny}
@@ -188,6 +184,7 @@ function NotebookParseFailure({
function CreateNotebookProposal({
input,
confirmState,
+ footerAction,
onApprove,
onDeny,
}: NotebookProposalStepProps) {
@@ -216,6 +213,7 @@ function CreateNotebookProposal({
@@ -227,6 +225,7 @@ function CreateNotebookProposal({
function UpdateNotebookProposal({
input,
confirmState,
+ footerAction,
onApprove,
onDeny,
}: NotebookProposalStepProps) {
@@ -308,6 +307,7 @@ function UpdateNotebookProposal({