Skip to content
edmi-ui

Confirmation

Tool approval request with request, accepted and rejected states.

StackBlitz ↗StackBlitz ↗StackBlitz ↗
Approved · rebalance sent
Rejected · nothing was changed
Approved · rebalance sent
Rejected · nothing was changed
Approved · rebalance sent
Rejected · nothing was changed

Built on alert and button.

Terminal window
npx shadcn@latest add @edmi-ui/ai-confirmation
Terminal window
pnpm dlx shadcn@latest add @edmi-ui/ai-confirmation
Terminal window
yarn dlx shadcn@latest add @edmi-ui/ai-confirmation
Terminal window
bunx shadcn@latest add @edmi-ui/ai-confirmation

Pick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.

Terminal window
npx shadcn-vue@latest add @edmi-ui/ai-confirmation
Terminal window
pnpm dlx shadcn-vue@latest add @edmi-ui/ai-confirmation
Terminal window
yarn dlx shadcn-vue@latest add @edmi-ui/ai-confirmation
Terminal window
bunx shadcn-vue@latest add @edmi-ui/ai-confirmation

Pick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.

Terminal window
npx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/ai-confirmation.json
Terminal window
pnpm dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/ai-confirmation.json
Terminal window
yarn dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/ai-confirmation.json
Terminal window
bunx --bun shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/ai-confirmation.json

Pick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.

import {
Confirmation,
ConfirmationAccepted,
ConfirmationAction,
ConfirmationActions,
ConfirmationDescription,
ConfirmationRejected,
ConfirmationRequest,
ConfirmationTitle,
} from "@/components/ai/confirmation";
<Confirmation className="w-full max-w-md">
<ConfirmationTitle>
<ConfirmationRequest>Run rebalance on MAG4?</ConfirmationRequest>
<ConfirmationAccepted>
<span>Approved · rebalance sent</span>
</ConfirmationAccepted>
<ConfirmationRejected>
<span>Rejected · nothing was changed</span>
</ConfirmationRejected>
</ConfirmationTitle>
<ConfirmationDescription>
Sells 0.42 NVDAx and buys MSFTx + AAPLx. Max slippage 1%.
</ConfirmationDescription>
<ConfirmationActions>
<ConfirmationAction
>
Approve
</ConfirmationAction>
<ConfirmationAction
variant="outline"
>
Reject
</ConfirmationAction>
</ConfirmationActions>
</Confirmation>
<script setup lang="ts">
import {
Confirmation,
ConfirmationAccepted,
ConfirmationDescription,
ConfirmationRejected,
ConfirmationRequest,
ConfirmationTitle,
} from "@/components/ai/confirmation";
</script>
<template>
<div class="flex flex-col gap-4">
<Confirmation
:key="row.approval.id"
:approval="row.approval"
:state="row.state"
class="w-full max-w-md"
>
<ConfirmationTitle>
<ConfirmationRequest>Run rebalance on MAG4?</ConfirmationRequest>
<ConfirmationAccepted>
<CheckIcon class="size-4" />
<span>Approved · rebalance sent</span>
</ConfirmationAccepted>
<ConfirmationRejected>
<XIcon class="size-4" />
<span>Rejected · nothing was changed</span>
</ConfirmationRejected>
</ConfirmationTitle>
<ConfirmationDescription>
Sells 0.42 NVDAx and buys MSFTx + AAPLx. Max slippage 1%.
</ConfirmationDescription>
<!-- … -->
</Confirmation>
</div>
</template>
<script lang="ts">
import {
Confirmation,
ConfirmationAccepted,
ConfirmationAction,
ConfirmationActions,
ConfirmationDescription,
ConfirmationRejected,
ConfirmationRequest,
ConfirmationTitle,
} from "$lib/components/ai/confirmation";
</script>
<div class="flex flex-col gap-4">
{#each rows as row (row.approval.id)}
<Confirmation approval={row.approval} state={row.state} class="w-full max-w-md">
<ConfirmationTitle>
<ConfirmationRequest>Run rebalance on MAG4?</ConfirmationRequest>
<ConfirmationAccepted>
<CheckIcon class="size-4" />
<span>Approved · rebalance sent</span>
</ConfirmationAccepted>
<ConfirmationRejected>
<XIcon class="size-4" />
<span>Rejected · nothing was changed</span>
</ConfirmationRejected>
</ConfirmationTitle>
<ConfirmationDescription>
Sells 0.42 NVDAx and buys MSFTx + AAPLx. Max slippage 1%.
</ConfirmationDescription>
<ConfirmationActions>
<ConfirmationAction onclick={() => respond(row, true)}>Approve</ConfirmationAction>
<ConfirmationAction variant="outline" onclick={() => respond(row, false)}>
Reject
</ConfirmationAction>
</ConfirmationActions>
</Confirmation>
{/each}
</div>
registry/components/ai/confirmation.tsx
"use client";
import type { ToolUIPart } from "ai";
import { cn } from "cn";
import type { ComponentProps, ReactNode } from "react";
import { createContext, useContext, useMemo } from "react";
import { ShieldIcon } from "@phosphor-icons/react";
import { Alert } from "@/registry/edmi/ui/alert";
import { Button } from "@/registry/edmi/ui/button";
import type { Elevation } from "@/registry/edmi/ui/elevation";
type ToolUIPartApproval =
| {
id: string;
approved?: never;
reason?: never;
}
| {
id: string;
approved: boolean;
reason?: string;
}
| undefined;
interface ConfirmationContextValue {
approval: ToolUIPartApproval;
state: ToolUIPart["state"];
elevation: Elevation | undefined;
}
const ConfirmationContext = createContext<ConfirmationContextValue | null>(
null,
);
const useConfirmation = () => {
const context = useContext(ConfirmationContext);
if (!context) {
throw new Error("Confirmation components must be used within Confirmation");
}
return context;
};
export type ConfirmationProps = Omit<
ComponentProps<typeof Alert>,
"variant"
> & {
approval?: ToolUIPartApproval;
state: ToolUIPart["state"];
/** ✦ depth of the action buttons (they inherit it, each can override). The alert itself stays flat. */
elevation?: Elevation;
/** Leading icon of the request. Defaults to a shield. */
icon?: ReactNode;
};
/**
* Request = warning alert with Approve / Reject. Once answered it collapses to a plain status line
* (green check for approved, muted cross for rejected), as on board AI 02.
*/
export const Confirmation = ({
className,
approval,
state,
elevation,
icon,
children,
...props
}: ConfirmationProps) => {
const contextValue = useMemo(
() => ({ approval, elevation, state }),
[approval, elevation, state],
);
if (!approval || state === "input-streaming" || state === "input-available") {
return null;
}
if (state !== "approval-requested") {
return (
<ConfirmationContext.Provider value={contextValue}>
<div
data-slot="ai-confirmation"
data-state={state}
className={cn("flex items-center gap-2 text-[13.5px]", className)}
{...(props as ComponentProps<"div">)}
>
{children}
</div>
</ConfirmationContext.Provider>
);
}
return (
<ConfirmationContext.Provider value={contextValue}>
<Alert
data-slot="ai-confirmation"
data-state={state}
variant="warning"
className={className}
{...props}
>
{icon ?? (
<ShieldIcon
/>
)}
{children}
</Alert>
</ConfirmationContext.Provider>
);
};
export type ConfirmationTitleProps = ComponentProps<"div">;
export const ConfirmationTitle = ({
className,
...props
}: ConfirmationTitleProps) => {
const { state } = useConfirmation();
return (
<div
data-slot="ai-confirmation-title"
className={cn(
state === "approval-requested"
? "col-start-2 font-medium text-foreground"
: "flex items-center gap-2",
className,
)}
{...props}
/>
);
};
export type ConfirmationDescriptionProps = ComponentProps<"div">;
/** ✦ Muted second line under the request title (only while the request is open). */
export const ConfirmationDescription = ({
className,
...props
}: ConfirmationDescriptionProps) => {
const { state } = useConfirmation();
if (state !== "approval-requested") {
return null;
}
return (
<div
data-slot="ai-confirmation-description"
className={cn("col-start-2 text-[13px] text-foreground-2", className)}
{...props}
/>
);
};
export interface ConfirmationRequestProps {
children?: ReactNode;
}
export const ConfirmationRequest = ({ children }: ConfirmationRequestProps) => {
const { state } = useConfirmation();
// Only show when approval is requested
if (state !== "approval-requested") {
return null;
}
return children;
};
export interface ConfirmationAcceptedProps {
children?: ReactNode;
}
export const ConfirmationAccepted = ({
children,
}: ConfirmationAcceptedProps) => {
const { approval, state } = useConfirmation();
// Only show when approved and in response states
if (
!approval?.approved ||
(state !== "approval-responded" &&
state !== "output-denied" &&
state !== "output-available")
) {
return null;
}
return (
<span className="inline-flex items-center gap-2 text-success-text">
{children}
</span>
);
};
export interface ConfirmationRejectedProps {
children?: ReactNode;
}
export const ConfirmationRejected = ({
children,
}: ConfirmationRejectedProps) => {
const { approval, state } = useConfirmation();
// Only show when rejected and in response states
if (
approval?.approved !== false ||
(state !== "approval-responded" &&
state !== "output-denied" &&
state !== "output-available")
) {
return null;
}
return (
<span className="inline-flex items-center gap-2 text-muted-foreground">
{children}
</span>
);
};
export type ConfirmationActionsProps = ComponentProps<"div">;
export const ConfirmationActions = ({
className,
...props
}: ConfirmationActionsProps) => {
const { state } = useConfirmation();
// Only show when approval is requested
if (state !== "approval-requested") {
return null;
}
return (
<div
data-slot="ai-confirmation-actions"
className={cn("col-start-2 mt-2.5 flex items-center gap-2", className)}
{...props}
/>
);
};
export type ConfirmationActionProps = ComponentProps<typeof Button>;
export const ConfirmationAction = ({
elevation,
size = "sm",
...props
}: ConfirmationActionProps) => {
const ctx = useContext(ConfirmationContext);
return (
<Button
data-slot="ai-confirmation-action"
elevation={elevation ?? ctx?.elevation}
size={size}
type="button"
{...props}
/>
);
};
registry/components/ai/confirmation/Confirmation.vue
<script setup lang="ts">
import type { ToolUIPart } from "ai"
import type { HTMLAttributes } from "vue"
import type { ToolUIPartApproval } from "./context"
import { ShieldIcon } from "@lucide/vue"
import { computed, provide, toRef } from "vue"
import { cn } from "@/registry/edmi/lib/utils"
import { Alert } from "@/registry/edmi/ui/alert"
import type { Elevation } from "@/registry/edmi/ui/elevation"
import { ConfirmationKey } from "./context"
const props = withDefaults(defineProps<{
approval?: ToolUIPartApproval
state: ToolUIPart["state"]
/** ✦ depth of the action buttons (they inherit it, each can override). The alert itself stays flat. */
elevation?: Elevation
class?: HTMLAttributes["class"]
}>(), {
elevation: undefined,
})
provide(ConfirmationKey, {
approval: toRef(props, "approval"),
state: toRef(props, "state"),
elevation: toRef(props, "elevation"),
})
const hidden = computed(() => !props.approval || props.state === "input-streaming" || props.state === "input-available")
</script>
<template>
<template v-if="!hidden">
<!-- Request = warning alert with Approve / Reject (leading icon in the `icon` slot). -->
<Alert
v-if="state === 'approval-requested'"
data-slot="ai-confirmation"
:data-state="state"
variant="warning"
:class="props.class"
>
<slot name="icon">
<ShieldIcon />
</slot>
<slot />
</Alert>
<!-- Once answered it collapses to a plain status line. -->
<div
v-else
data-slot="ai-confirmation"
:data-state="state"
:class="cn('flex items-center gap-2 text-[13.5px]', props.class)"
>
<slot />
</div>
</template>
</template>
registry/components/ai/confirmation/ConfirmationAccepted.vue
<script setup lang="ts">
import { useConfirmationContext } from "./context"
const { approval, state } = useConfirmationContext()
</script>
<template>
<span
v-if="approval?.approved
&& (state === 'approval-responded' || state === 'output-denied' || state === 'output-available')"
class="inline-flex items-center gap-2 text-success-text"
>
<slot />
</span>
</template>
registry/components/ai/confirmation/ConfirmationAction.vue
<script setup lang="ts">
import type { HTMLAttributes } from "vue"
import type { ButtonVariants } from "@/registry/edmi/ui/button"
import { inject } from "vue"
import { Button } from "@/registry/edmi/ui/button"
import type { Elevation } from "@/registry/edmi/ui/elevation"
import { ConfirmationKey } from "./context"
// `elevation` falls back to the Confirmation's (an explicit value here wins).
const props = withDefaults(defineProps<{
variant?: ButtonVariants["variant"]
size?: ButtonVariants["size"]
elevation?: Elevation
class?: HTMLAttributes["class"]
}>(), {
size: "sm",
elevation: undefined,
})
const ctx = inject(ConfirmationKey, null)
</script>
<template>
<Button
data-slot="ai-confirmation-action"
type="button"
:variant="props.variant"
:size="props.size"
:elevation="props.elevation ?? ctx?.elevation.value"
:class="props.class"
>
<slot />
</Button>
</template>
registry/components/ai/confirmation/ConfirmationActions.vue
<script setup lang="ts">
import type { HTMLAttributes } from "vue"
import { cn } from "@/registry/edmi/lib/utils"
import { useConfirmationContext } from "./context"
const props = defineProps<{
class?: HTMLAttributes["class"]
}>()
const { state } = useConfirmationContext()
</script>
<template>
<div
v-if="state === 'approval-requested'"
data-slot="ai-confirmation-actions"
:class="cn('col-start-2 mt-2.5 flex items-center gap-2', props.class)"
>
<slot />
</div>
</template>
registry/components/ai/confirmation/ConfirmationDescription.vue
<script setup lang="ts">
// ✦ Edmi addition: muted second line under the request title (only while the request is open).
import type { HTMLAttributes } from "vue"
import { cn } from "@/registry/edmi/lib/utils"
import { useConfirmationContext } from "./context"
const props = defineProps<{
class?: HTMLAttributes["class"]
}>()
const { state } = useConfirmationContext()
</script>
<template>
<div
v-if="state === 'approval-requested'"
data-slot="ai-confirmation-description"
:class="cn('col-start-2 text-[13px] text-foreground-2', props.class)"
>
<slot />
</div>
</template>
registry/components/ai/confirmation/ConfirmationRejected.vue
<script setup lang="ts">
import { useConfirmationContext } from "./context"
const { approval, state } = useConfirmationContext()
</script>
<template>
<span
v-if="approval?.approved === false
&& (state === 'approval-responded' || state === 'output-denied' || state === 'output-available')"
class="inline-flex items-center gap-2 text-muted-foreground"
>
<slot />
</span>
</template>
registry/components/ai/confirmation/ConfirmationRequest.vue
<script setup lang="ts">
import { useConfirmationContext } from "./context"
const { state } = useConfirmationContext()
</script>
<template>
<template v-if="state === 'approval-requested'">
<slot />
</template>
</template>
registry/components/ai/confirmation/ConfirmationTitle.vue
<script setup lang="ts">
import type { HTMLAttributes } from "vue"
import { cn } from "@/registry/edmi/lib/utils"
import { useConfirmationContext } from "./context"
const props = defineProps<{
class?: HTMLAttributes["class"]
}>()
const { state } = useConfirmationContext()
</script>
<template>
<div
data-slot="ai-confirmation-title"
:class="cn(
state === 'approval-requested' ? 'col-start-2 font-medium text-foreground' : 'flex items-center gap-2',
props.class,
)"
>
<slot />
</div>
</template>
registry/components/ai/confirmation/context.ts
import type { ToolUIPart } from "ai";
import type { InjectionKey, Ref } from "vue";
import { inject } from "vue";
import type { Elevation } from "@/registry/edmi/ui/elevation";
export type ToolUIPartApproval =
| {
id: string;
approved?: never;
reason?: never;
}
| {
id: string;
approved: boolean;
reason?: string;
}
| undefined;
export interface ConfirmationContextValue {
approval: Ref<ToolUIPartApproval>;
state: Ref<ToolUIPart["state"]>;
elevation: Ref<Elevation | undefined>;
}
export const ConfirmationKey: InjectionKey<ConfirmationContextValue> = Symbol(
"ConfirmationContext",
);
export function useConfirmationContext() {
const context = inject(ConfirmationKey);
if (!context)
throw new Error(
"Confirmation components must be used within <Confirmation>",
);
return context;
}
registry/components/ai/confirmation/index.ts
export { default as Confirmation } from "./Confirmation.vue";
export { default as ConfirmationAccepted } from "./ConfirmationAccepted.vue";
export { default as ConfirmationAction } from "./ConfirmationAction.vue";
export { default as ConfirmationActions } from "./ConfirmationActions.vue";
export { default as ConfirmationDescription } from "./ConfirmationDescription.vue";
export { default as ConfirmationRejected } from "./ConfirmationRejected.vue";
export { default as ConfirmationRequest } from "./ConfirmationRequest.vue";
export { default as ConfirmationTitle } from "./ConfirmationTitle.vue";
export * from "./context";
src/lib/registry/ai/confirmation/confirmation-accepted.svelte
<script lang="ts">
import type { Snippet } from "svelte";
import { useConfirmation } from "./use-confirmation.svelte.js";
let { children }: { children?: Snippet } = $props();
const confirmation = useConfirmation();
const show = $derived(
!!confirmation.approval?.approved &&
(confirmation.state === "approval-responded" ||
confirmation.state === "output-denied" ||
confirmation.state === "output-available")
);
</script>
{#if show}
<span class="inline-flex items-center gap-2 text-success-text">{@render children?.()}</span>
{/if}
src/lib/registry/ai/confirmation/confirmation-action.svelte
<script lang="ts">
import { Button, type ButtonProps } from "$lib/registry/ui/button/index.js";
import { useConfirmationOptional } from "./use-confirmation.svelte.js";
// `elevation` falls back to the Confirmation's (an explicit value here wins).
let {
elevation,
size = "sm",
children,
...restProps
}: ButtonProps = $props();
const confirmation = useConfirmationOptional();
</script>
<Button
data-slot="ai-confirmation-action"
type="button"
elevation={elevation && elevation !== "auto" ? elevation : confirmation?.elevation}
{size}
{...restProps}
>
{@render children?.()}
</Button>
src/lib/registry/ai/confirmation/confirmation-actions.svelte
<script lang="ts">
import { cn } from "$lib/utils.js";
import type { HTMLAttributes } from "svelte/elements";
import { useConfirmation } from "./use-confirmation.svelte.js";
let { class: className, children, ...restProps }: HTMLAttributes<HTMLDivElement> = $props();
const confirmation = useConfirmation();
</script>
{#if confirmation.state === "approval-requested"}
<div
data-slot="ai-confirmation-actions"
class={cn("col-start-2 mt-2.5 flex items-center gap-2", className)}
{...restProps}
>
{@render children?.()}
</div>
{/if}
src/lib/registry/ai/confirmation/confirmation-description.svelte
<!-- ✦ Edmi addition: muted second line under the request title (only while the request is open). -->
<script lang="ts">
import { cn } from "$lib/utils.js";
import type { HTMLAttributes } from "svelte/elements";
import { useConfirmation } from "./use-confirmation.svelte.js";
let { class: className, children, ...restProps }: HTMLAttributes<HTMLDivElement> = $props();
const confirmation = useConfirmation();
</script>
{#if confirmation.state === "approval-requested"}
<div
data-slot="ai-confirmation-description"
class={cn("col-start-2 text-[13px] text-foreground-2", className)}
{...restProps}
>
{@render children?.()}
</div>
{/if}
src/lib/registry/ai/confirmation/confirmation-rejected.svelte
<script lang="ts">
import type { Snippet } from "svelte";
import { useConfirmation } from "./use-confirmation.svelte.js";
let { children }: { children?: Snippet } = $props();
const confirmation = useConfirmation();
const show = $derived(
confirmation.approval?.approved === false &&
(confirmation.state === "approval-responded" ||
confirmation.state === "output-denied" ||
confirmation.state === "output-available")
);
</script>
{#if show}
<span class="inline-flex items-center gap-2 text-muted-foreground">{@render children?.()}</span>
{/if}
src/lib/registry/ai/confirmation/confirmation-request.svelte
<script lang="ts">
import type { Snippet } from "svelte";
import { useConfirmation } from "./use-confirmation.svelte.js";
let { children }: { children?: Snippet } = $props();
const confirmation = useConfirmation();
</script>
{#if confirmation.state === "approval-requested"}
{@render children?.()}
{/if}
src/lib/registry/ai/confirmation/confirmation-title.svelte
<script lang="ts">
import { cn } from "$lib/utils.js";
import type { HTMLAttributes } from "svelte/elements";
import { useConfirmation } from "./use-confirmation.svelte.js";
let { class: className, children, ...restProps }: HTMLAttributes<HTMLDivElement> = $props();
const confirmation = useConfirmation();
</script>
<div
data-slot="ai-confirmation-title"
class={cn(
confirmation.state === "approval-requested"
? "col-start-2 font-medium text-foreground"
: "flex items-center gap-2",
className
)}
{...restProps}
>
{@render children?.()}
</div>
src/lib/registry/ai/confirmation/confirmation.svelte
<script lang="ts">
import ShieldIcon from "phosphor-svelte/lib/Shield";
import { cn } from "$lib/utils.js";
import { Alert } from "$lib/registry/ui/alert/index.js";
import type { Elevation } from "$lib/registry/ui/elevation/index.js";
import type { ToolUIPart } from "ai";
import type { Snippet } from "svelte";
import type { HTMLAttributes } from "svelte/elements";
import { setConfirmationContext, type ToolUIPartApproval } from "./use-confirmation.svelte.js";
let {
class: className,
approval,
state,
elevation = "auto",
icon,
children,
...restProps
}: Omit<HTMLAttributes<HTMLDivElement>, "children"> & {
approval?: ToolUIPartApproval;
state: ToolUIPart["state"];
/** ✦ depth of the action buttons (they inherit it, each can override). The alert itself stays flat. */
elevation?: Elevation;
/** Leading icon of the request. Defaults to a shield. */
icon?: Snippet;
children?: Snippet;
} = $props();
setConfirmationContext({
get approval() {
return approval;
},
get state() {
return state;
},
get elevation() {
return elevation;
},
});
const hidden = $derived(!approval || state === "input-streaming" || state === "input-available");
</script>
{#if !hidden}
<!-- Request = warning alert with Approve / Reject; once answered it collapses to a plain status line. -->
{#if state === "approval-requested"}
<Alert data-slot="ai-confirmation" data-state={state} variant="warning" class={className} {...restProps}>
{#if icon}
{@render icon()}
{:else}
<ShieldIcon
/>
{/if}
{@render children?.()}
</Alert>
{:else}
<div
data-slot="ai-confirmation"
data-state={state}
class={cn("flex items-center gap-2 text-[13.5px]", className)}
{...restProps}
>
{@render children?.()}
</div>
{/if}
{/if}
src/lib/registry/ai/confirmation/index.ts
import Root from "./confirmation.svelte";
import Accepted from "./confirmation-accepted.svelte";
import Action from "./confirmation-action.svelte";
import Actions from "./confirmation-actions.svelte";
import Description from "./confirmation-description.svelte";
import Rejected from "./confirmation-rejected.svelte";
import Request from "./confirmation-request.svelte";
import Title from "./confirmation-title.svelte";
export * from "./use-confirmation.svelte.js";
export {
Accepted,
Accepted as ConfirmationAccepted,
Action,
Action as ConfirmationAction,
Actions,
Actions as ConfirmationActions,
Description,
Description as ConfirmationDescription,
Rejected,
Rejected as ConfirmationRejected,
Request,
Request as ConfirmationRequest,
Root,
//
Root as Confirmation,
Title,
Title as ConfirmationTitle,
};
src/lib/registry/ai/confirmation/use-confirmation.svelte.ts
import type { ToolUIPart } from "ai";
import { getContext, setContext } from "svelte";
import type { Elevation } from "$lib/registry/ui/elevation/index.js";
export type ToolUIPartApproval =
| { id: string; approved?: never; reason?: never }
| { id: string; approved: boolean; reason?: string }
| undefined;
/** Getter object so consumers stay reactive. */
export interface ConfirmationContextValue {
readonly approval: ToolUIPartApproval;
readonly state: ToolUIPart["state"];
readonly elevation: Elevation | undefined;
}
const KEY = Symbol("ai-confirmation");
export function setConfirmationContext(value: ConfirmationContextValue) {
return setContext(KEY, value);
}
export function useConfirmation(): ConfirmationContextValue {
const ctx = getContext<ConfirmationContextValue | undefined>(KEY);
if (!ctx)
throw new Error(
"Confirmation components must be used within <Confirmation>",
);
return ctx;
}
export function useConfirmationOptional():
| ConfirmationContextValue
| undefined {
return getContext<ConfirmationContextValue | undefined>(KEY);
}

See the Elevation guide for the levels, roles and layered mode.

The request is a flat warning alert; elevation goes to the action buttons (flat, raised +1, floating +2, sunken -1 like a Button) and each ConfirmationAction can override it.

StackBlitz ↗StackBlitz ↗StackBlitz ↗

Flat (0)

Raised (+1)

Floating (+2)

Flat (0)

Raised (+1)

Floating (+2)

Flat (0)

Raised (+1)

Floating (+2)

Part Prop Type Default
Confirmation approval, state AI SDK tool approval and ToolUIPart["state"]
Confirmation elevation "auto" | "sunken" | "flat" | "raised" | "floating" (the action buttons inherit it) "auto"
Confirmation icon ReactNode shield
ConfirmationTitle request title (put ConfirmationRequest / Accepted / Rejected inside)
ConfirmationDescription muted second line, only while the request is open
ConfirmationActions row of actions, only while the request is open
ConfirmationAction elevation, variant, size Button props size="sm"
  • The open request is a warning alert; once answered it collapses to a plain status line (green check when approved, muted cross when rejected).
  • ConfirmationDescription ✦ is the muted line under the title.
  • elevation ✦ is passed to every ConfirmationAction (each can override it); the alert itself stays flat.