import React, { Suspense, use, useState } from 'react' import { Box, Text } from '@anthropic/ink' import { useKeybinding } from '../../keybindings/useKeybinding.js' import { logEvent } from '../../services/analytics/index.js' import type { Message } from '../../types/message.js' import { generatePermissionExplanation, isPermissionExplainerEnabled, type PermissionExplanation as PermissionExplanationType, type RiskLevel, } from '../../utils/permissions/permissionExplainer.js' import { ShimmerChar } from '../Spinner/ShimmerChar.js' import { useShimmerAnimation } from '../Spinner/useShimmerAnimation.js' const LOADING_MESSAGE = 'Loading explanation…' function ShimmerLoadingText(): React.ReactNode { const [ref, glimmerIndex] = useShimmerAnimation( 'responding', LOADING_MESSAGE, false, ) return ( {LOADING_MESSAGE.split('').map((char, index) => ( ))} ) } function getRiskColor(riskLevel: RiskLevel): 'success' | 'warning' | 'error' { switch (riskLevel) { case 'LOW': return 'success' case 'MEDIUM': return 'warning' case 'HIGH': return 'error' } } function getRiskLabel(riskLevel: RiskLevel): string { switch (riskLevel) { case 'LOW': return 'Low risk' case 'MEDIUM': return 'Med risk' case 'HIGH': return 'High risk' } } type PermissionExplanationProps = { toolName: string toolInput: unknown toolDescription?: string messages?: Message[] } type ExplainerState = { visible: boolean enabled: boolean promise: Promise | null } /** * Creates an explanation promise that never rejects. * Errors are caught and returned as null. */ function createExplanationPromise( props: PermissionExplanationProps, ): Promise { return generatePermissionExplanation({ toolName: props.toolName, toolInput: props.toolInput, toolDescription: props.toolDescription, messages: props.messages, signal: new AbortController().signal, // Won't abort - request is fast enough }).catch(() => null) } /** * Hook that manages the permission explainer state. * Creates the fetch promise lazily (only when user hits Ctrl+E) * to avoid consuming tokens for explanations users never view. */ export function usePermissionExplainerUI( props: PermissionExplanationProps, ): ExplainerState { const enabled = isPermissionExplainerEnabled() const [visible, setVisible] = useState(false) const [promise, setPromise] = useState | null>(null) // Use keybinding for ctrl+e toggle (configurable via keybindings.json) useKeybinding( 'confirm:toggleExplanation', () => { if (!visible) { logEvent('tengu_permission_explainer_shortcut_used', {}) // Only create the promise on first toggle (lazy loading) if (!promise) { setPromise(createExplanationPromise(props)) } } setVisible(v => !v) }, { context: 'Confirmation', isActive: enabled }, ) return { visible, enabled, promise } } /** * Inner component that uses React 19's use() to read the promise. * Suspends while loading, returns null on error. */ function ExplanationResult({ promise, }: { promise: Promise }): React.ReactNode { const explanation = use(promise) if (!explanation) { return ( Explanation unavailable ) } return ( {explanation.explanation} {explanation.reasoning} {getRiskLabel(explanation.riskLevel)}: {explanation.risk} ) } /** * Content component - shows loading (via Suspense) or explanation when visible */ export function PermissionExplainerContent({ visible, promise, }: { visible: boolean promise: Promise | null }): React.ReactNode { if (!visible || !promise) { return null } return ( } > ) }