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 (
}
>
)
}