Confirmation

An alert-based component for managing tool execution approval workflows with request, accept, and reject states.

The Confirmation component provides a flexible system for displaying tool approval requests and their outcomes. Perfect for showing users when AI tools require approval before execution, and displaying the approval status afterward.

Install using CLI

AI Elements Vue
shadcn-vue CLI
npx ai-elements-vue@latest add confirmation

Install Manually

Copy and paste the following code in the same folder.

Confirmation.vue
ConfirmationTitle.vue
ConfirmationRequest.vue
ConfirmationAccepted.vue
ConfirmationRejected.vue
ConfirmationActions.vue
ConfirmationAction.vue
context.ts
index.ts
<script setup lang="ts">
import type { ToolUIPart } from 'ai'
import type { HTMLAttributes } from 'vue'
import type { ToolUIPartApproval } from './context'
import { Alert } from '@repo/shadcn-vue/components/ui/alert'
import { cn } from '@repo/shadcn-vue/lib/utils'
import { provide, toRef } from 'vue'
import { ConfirmationKey } from './context'

const props = defineProps<{
  approval?: ToolUIPartApproval
  state: ToolUIPart['state']
  class?: HTMLAttributes['class']
}>()

provide(ConfirmationKey, {
  approval: toRef(props, 'approval'),
  state: toRef(props, 'state'),
})
</script>

<template>
  <Alert
    v-if="approval && state !== 'input-streaming' && state !== 'input-available'"
    :class="cn('flex flex-col gap-2', props.class)"
    v-bind="$attrs"
  >
    <slot />
  </Alert>
</template>

Usage with AI SDK

Build a chat UI with tool approval workflow where dangerous tools require user confirmation before execution.

Add the following component to your frontend:

pages/index.vue
<script setup lang="ts">
import type { ToolUIPart } from 'ai'
import { useChat } from '@ai-sdk/vue'
import { DefaultChatTransport } from 'ai'
import { CheckIcon, XIcon } from 'lucide-vue-next'
import { computed } from 'vue'
import {
  Confirmation,
  ConfirmationAccepted,
  ConfirmationAction,
  ConfirmationActions,
  ConfirmationRejected,
  ConfirmationRequest,
  ConfirmationTitle,
} from '@/components/ai-elements/confirmation'
import { MessageResponse } from '@/components/ai-elements/message'
import { Button } from '@/components/ui/button'

interface DeleteFileInput {
  filePath: string
  confirm: boolean
}

type DeleteFileToolUIPart = ToolUIPart<{
  delete_file: {
    input: DeleteFileInput
    output: { success: boolean, message: string }
  }
}>

const { messages, sendMessage, status, respondToConfirmationRequest } = useChat({
  transport: new DefaultChatTransport({
    api: '/api/chat',
  }),
})

function handleDeleteFile() {
  sendMessage({ text: 'Delete the file at /tmp/example.txt' })
}

const latestMessage = computed(() => {
  if (!messages.value || messages.value.length === 0) {
    return undefined
  }
  return messages.value[messages.value.length - 1]
})

const deleteTool = computed(() => {
  return latestMessage.value?.parts?.find(
    part => part.type === 'tool-delete_file'
  ) as DeleteFileToolUIPart | undefined
})
</script>

<template>
  <div
    class="max-w-4xl mx-auto p-6 relative size-full rounded-lg border h-[600px]"
  >
    <div class="flex flex-col h-full space-y-4">
      <Button
        :disabled="status !== 'ready'"
        @click="handleDeleteFile"
      >
        Delete Example File
      </Button>

      <Confirmation
        v-if="deleteTool?.approval"
        :approval="deleteTool.approval"
        :state="deleteTool.state"
      >
        <ConfirmationTitle>
          <ConfirmationRequest>
            This tool wants to delete: <code>{{ deleteTool.input?.filePath }}</code>
            <br>
            Do you approve this action?
          </ConfirmationRequest>
          <ConfirmationAccepted>
            <CheckIcon class="size-4" />
            <span>You approved this tool execution</span>
          </ConfirmationAccepted>
          <ConfirmationRejected>
            <XIcon class="size-4" />
            <span>You rejected this tool execution</span>
          </ConfirmationRejected>
        </ConfirmationTitle>
        <ConfirmationActions>
          <ConfirmationAction
            variant="outline"
            @click="
              respondToConfirmationRequest({
                approvalId: deleteTool!.approval!.id,
                approved: false,
              })
            "
          >
            Reject
          </ConfirmationAction>
          <ConfirmationAction
            variant="default"
            @click="
              respondToConfirmationRequest({
                approvalId: deleteTool!.approval!.id,
                approved: true,
              })
            "
          >
            Approve
          </ConfirmationAction>
        </ConfirmationActions>
      </Confirmation>

      <MessageResponse
        v-if="deleteTool?.output"
        :content="
          deleteTool.output.success
            ? deleteTool.output.message
            : `Error: ${deleteTool.output.message}`
        "
      />
    </div>
  </div>
</template>

Add the following route to your backend:

server/api/chat.ts
import { convertToModelMessages, streamText, UIMessage } from 'ai'
import { z } from 'zod'

// Allow streaming responses up to 30 seconds
export const maxDuration = 30

export default defineEventHandler(async (event) => {
  const body = await readBody<{ messages: UIMessage[] }>(event)

  const result = streamText({
    model: 'openai/gpt-4o',
    messages: convertToModelMessages(body.messages),
    tools: {
      delete_file: {
        description: 'Delete a file from the file system',
        parameters: z.object({
          filePath: z.string().describe('The path to the file to delete'),
          confirm: z
            .boolean()
            .default(false)
            .describe('Confirmation that the user wants to delete the file'),
        }),
        requireApproval: true, // Enables approval workflow
        execute: async ({ filePath, confirm }) => {
          if (!confirm) {
            return {
              success: false,
              message: 'Deletion not confirmed',
            }
          }

          // Simulate a file deletion delay
          await new Promise(resolve => setTimeout(resolve, 500))

          return {
            success: true,
            message: `Successfully deleted ${filePath}`,
          }
        },
      },
    },
  })

  // Stream back to the UI
  return result.toAIStreamResponse()
})

Features

  • Context-based state management for approval workflow
  • Conditional rendering based on approval state
  • Support for approval-requested, approval-responded, output-denied, and output-available states
  • Built on shadcn-vue Alert and Button components
  • TypeScript support with comprehensive type definitions
  • Customizable styling with Tailwind CSS
  • Keyboard navigation and accessibility support
  • Theme-aware with automatic dark mode support

Examples

Approval Request State

Shows the approval request with action buttons when state is approval-requested.

Approved State

Shows the accepted status when user approves and state is approval-responded or output-available.

Rejected State

Shows the rejected status when user rejects and state is output-denied.

Props

<Confirmation />

approvalToolUIPart['approval']
The approval object containing the approval ID and status. If not provided or undefined, the component will not render.
stateToolUIPart['state']
The current state of the tool (input-streaming, input-available, approval-requested, approval-responded, output-denied, or output-available). Will not render for input-streaming or input-available states.
classstring
Additional classes applied to the component.

<ConfirmationTitle />

classstring
Additional classes applied to the component.

<ConfirmationActions />

classstring
Additional classes applied to the component.