Design system
Docs
Desktop

Confirm dialog

components/confirm-dialog.tsx: the desktop's destructive confirmation on the headless alert dialog

Import
import { ConfirmDialog } from "../components/confirm-dialog.tsx";

File: packages/desktop/src/renderer/src/components/confirm-dialog.tsx, over @nyte-ai/ui/alert-dialog. Used for: delete session, archive all chats in a workspace, discard unsaved file, close a running terminal, sign out of GitHub CLI.

Anatomy

AlertDialog.Root (open, onOpenChange)
└─ Portal
   ├─ Backdrop            layer.dialogBackdrop, t.bgScrim
   └─ Popup               layer.dialog, min(400px, 100vw − 48px), initialFocus=cancel, finalFocus=returnFocusRef, aria-busy
      ├─ Title
      ├─ Description
      ├─ <p role="alert">  error, when present
      └─ actions
         ├─ AlertDialog.Close  (Button secondary)  "Cancel"
         └─ Button danger       confirmLabel | pendingLabel

Props

PropTypeDefaultDescription
openbooleanRequired, controlled.
pendingbooleanRequired. Disables both buttons, sets aria-busy, blocks closing.
errorstring | undefinedRequired. Rendered as role="alert" when defined.
returnFocusRefRefObject<HTMLButtonElement | null>Required. The control that opened the dialog; focus returns to it on close.
titlestring"Delete chat?"
descriptionReactNode"This permanently deletes the chat and its history. This can't be undone."
confirmLabelstring"Delete"
pendingLabelstring"Deleting…"
onOpenChange(open: boolean) => voidRequired. Only called with false, and never while pending.
onConfirm() => voidRequired. The dialog does not close itself; the caller closes after success.

Behavior that must be kept

  • Initial focus is on Cancel, not on the destructive action. Pressing Enter reflexively cancels.
  • finalFocus={returnFocusRef} is required so the row or button that opened the dialog gets focus back. Callers pass a ref to the exact element.
  • While pending, onOpenChange is swallowed: Escape and outside press do nothing until the mutation settles.
  • The error stays inside the dialog as a live role="alert"; the dialog stays open so the user can retry or cancel.

Example

const deleteRef = useRef<HTMLButtonElement>(null);
const remove = useMutation(…);

<Button ref={deleteRef} variant="danger" onClick={() => setOpen(true)}>Delete</Button>
<ConfirmDialog
  open={open}
  pending={remove.isPending}
  error={remove.error?.message}
  returnFocusRef={deleteRef}
  onOpenChange={setOpen}
  onConfirm={() => remove.mutate(id, { onSuccess: () => setOpen(false) })}
/>

Compared with @nyte-ai/ui AlertDialog

The shared Alert dialog is parts you compose; this is a finished surface with a fixed footer and a pending state. They share Base UI behavior and nothing else.