import React, { type ReactNode, useCallback, useMemo, useState } from 'react' import { Box, Text } from '@anthropic/ink' import type { KeybindingAction } from '../../keybindings/types.js' import { useKeybindings } from '../../keybindings/useKeybinding.js' import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent, } from '../../services/analytics/index.js' import { useSetAppState } from '../../state/AppState.js' import { type OptionWithDescription, Select } from '../CustomSelect/select.js' export type FeedbackType = 'accept' | 'reject' export type PermissionPromptOption = { value: T label: ReactNode feedbackConfig?: { type: FeedbackType placeholder?: string } keybinding?: KeybindingAction } export type ToolAnalyticsContext = { toolName: string isMcp: boolean } export type PermissionPromptProps = { options: PermissionPromptOption[] onSelect: (value: T, feedback?: string) => void onCancel?: () => void question?: string | ReactNode toolAnalyticsContext?: ToolAnalyticsContext } const DEFAULT_PLACEHOLDERS: Record = { accept: 'tell Claude what to do next', reject: 'tell Claude what to do differently', } /** * Shared component for permission prompts with optional feedback input. * * Handles: * - "Do you want to proceed?" question with optional Tab hint * - Feature flag check for feedback capability * - Input mode toggling (Tab to expand feedback input) * - Analytics events for feedback interactions * - Transforming options to Select-compatible format */ export function PermissionPrompt({ options, onSelect, onCancel, question = 'Do you want to proceed?', toolAnalyticsContext, }: PermissionPromptProps): React.ReactNode { const setAppState = useSetAppState() const [acceptFeedback, setAcceptFeedback] = useState('') const [rejectFeedback, setRejectFeedback] = useState('') const [acceptInputMode, setAcceptInputMode] = useState(false) const [rejectInputMode, setRejectInputMode] = useState(false) const [focusedValue, setFocusedValue] = useState(null) // Track whether user ever entered feedback mode (persists after collapse) const [acceptFeedbackModeEntered, setAcceptFeedbackModeEntered] = useState(false) const [rejectFeedbackModeEntered, setRejectFeedbackModeEntered] = useState(false) // Find which option is focused and whether it has feedback config const focusedOption = options.find(opt => opt.value === focusedValue) const focusedFeedbackType = focusedOption?.feedbackConfig?.type // Show Tab hint when focused on a feedback-enabled option that's not already in input mode const showTabHint = (focusedFeedbackType === 'accept' && !acceptInputMode) || (focusedFeedbackType === 'reject' && !rejectInputMode) // Transform options to Select-compatible format const selectOptions = useMemo((): OptionWithDescription[] => { return options.map(opt => { const { value, label, feedbackConfig } = opt // No feedback config = simple option if (!feedbackConfig) { return { label, value, } } const { type, placeholder } = feedbackConfig const isInputMode = type === 'accept' ? acceptInputMode : rejectInputMode const onChange = type === 'accept' ? setAcceptFeedback : setRejectFeedback const defaultPlaceholder = DEFAULT_PLACEHOLDERS[type] // When in input mode, show input field if (isInputMode) { return { type: 'input' as const, label, value, placeholder: placeholder ?? defaultPlaceholder, onChange, allowEmptySubmitToCancel: true, } } // Not in input mode - show simple option return { label, value, } }) }, [options, acceptInputMode, rejectInputMode]) // Handle Tab key to toggle input mode const handleInputModeToggle = useCallback( (value: T) => { const option = options.find(opt => opt.value === value) if (!option?.feedbackConfig) return const { type } = option.feedbackConfig const analyticsProps = { toolName: toolAnalyticsContext?.toolName as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, isMcp: toolAnalyticsContext?.isMcp ?? false, } if (type === 'accept') { if (acceptInputMode) { setAcceptInputMode(false) logEvent('tengu_accept_feedback_mode_collapsed', analyticsProps) } else { setAcceptInputMode(true) setAcceptFeedbackModeEntered(true) logEvent('tengu_accept_feedback_mode_entered', analyticsProps) } } else if (type === 'reject') { if (rejectInputMode) { setRejectInputMode(false) logEvent('tengu_reject_feedback_mode_collapsed', analyticsProps) } else { setRejectInputMode(true) setRejectFeedbackModeEntered(true) logEvent('tengu_reject_feedback_mode_entered', analyticsProps) } } }, [options, acceptInputMode, rejectInputMode, toolAnalyticsContext], ) // Handle selection const handleSelect = useCallback( (value: T) => { const option = options.find(opt => opt.value === value) if (!option) return // Get feedback if applicable let feedback: string | undefined if (option.feedbackConfig) { const rawFeedback = option.feedbackConfig.type === 'accept' ? acceptFeedback : rejectFeedback const trimmedFeedback = rawFeedback.trim() if (trimmedFeedback) { feedback = trimmedFeedback } // Log accept/reject submission with feedback context const analyticsProps = { toolName: toolAnalyticsContext?.toolName as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, isMcp: toolAnalyticsContext?.isMcp ?? false, has_instructions: !!trimmedFeedback, instructions_length: trimmedFeedback?.length ?? 0, entered_feedback_mode: option.feedbackConfig.type === 'accept' ? acceptFeedbackModeEntered : rejectFeedbackModeEntered, } if (option.feedbackConfig.type === 'accept') { logEvent('tengu_accept_submitted', analyticsProps) } else if (option.feedbackConfig.type === 'reject') { logEvent('tengu_reject_submitted', analyticsProps) } } onSelect(value, feedback) }, [ options, acceptFeedback, rejectFeedback, onSelect, toolAnalyticsContext, acceptFeedbackModeEntered, rejectFeedbackModeEntered, ], ) // Register keybinding handlers for options that have a keybinding set const keybindingHandlers = useMemo(() => { const handlers: Record void> = {} for (const opt of options) { if (opt.keybinding) { handlers[opt.keybinding] = () => handleSelect(opt.value) } } return handlers }, [options, handleSelect]) useKeybindings(keybindingHandlers, { context: 'Confirmation' }) // Handle cancel (Esc) const handleCancel = useCallback(() => { logEvent('tengu_permission_request_escape', {}) // Increment escape count for attribution tracking setAppState(prev => ({ ...prev, attribution: { ...prev.attribution, escapeCount: prev.attribution.escapeCount + 1, }, })) onCancel?.() }, [onCancel, setAppState]) return ( {typeof question === 'string' ? {question} : question}