HumanApprovalModal

AG-UI React HITL Human-in-the-loop approval primitive. Blocks on pending agent actions. Supports diff, JSON, and text preview modes. Keyboard shortcuts, edit-before-approve, dark mode. Zero external deps.

Three scenarios

Click any card to open the modal. Use Approve, Reject, or Edit & Approve to see the resolved state.

diff
Write production config
Agent wants to modify config/production.yaml to update the database connection pool settings.
json
Send Slack notification
Agent wants to post a message to #eng-alerts via the Slack API with incident details.
text · high-stakes
Initiate wire transfer
Agent wants to transfer $24,000 to a vendor account. High-stakes warning styling is active.

Copy-paste snippet

// ── Controlled usage ──────────────────────────────────────────────
import { HumanApprovalModal } from './HumanApprovalModal.jsx';

const approval = {
  id: 'approval-001',
  title: 'Write production config',
  agentName: 'DevOps Agent',
  description: 'I want to update the database pool settings in config/production.yaml.',
  previewMode: 'diff', // 'text' | 'diff' | 'json'
  preview: '- pool_size: 5\n+ pool_size: 20\n- timeout: 30\n+ timeout: 60',
  highStakes: false,
};

<HumanApprovalModal
  approval={approval}
  onApprove={(id) => console.log('approved', id)}
  onReject={(id, reason) => console.log('rejected', id, reason)}
  onEdit={(id, payload) => console.log('edited', id, payload)}
  theme="light"
/>

// ── AG-UI auto-wiring ─────────────────────────────────────────────
import { HumanApprovalContainer } from './HumanApprovalModal.jsx';
const bus = new EventTarget();

// Your agent runtime emits STATE_DELTA when approval is pending:
bus.dispatchEvent(new CustomEvent('STATE_DELTA', {
  detail: {
    path: 'pendingApproval',
    value: {
      id: 'a-001', title: 'Write production config',
      agentName: 'DevOps Agent',
      description: 'I want to update pool settings.',
      previewMode: 'diff',
      preview: '- pool_size: 5\n+ pool_size: 20',
    }
  }
}));

// Clear approval after decision:
bus.dispatchEvent(new CustomEvent('STATE_DELTA', {
  detail: { path: 'pendingApproval', value: null }
}));

// Container listens to the bus and emits USER_MESSAGE on decision:
<HumanApprovalContainer
  eventSource={bus}
  theme="dark"
/>