import figures from 'figures' import React, { useCallback, useState } from 'react' import { Dialog } from '../../components/design-system/Dialog.js' import { stringWidth } from '../../ink/stringWidth.js' // eslint-disable-next-line custom-rules/prefer-use-keybindings -- raw text input for config dialog import { Box, Text, useInput } from '../../ink.js' import { useKeybinding, useKeybindings, } from '../../keybindings/useKeybinding.js' import { isEnvTruthy } from '../../utils/envUtils.js' import type { PluginOptionSchema, PluginOptionValues, } from '../../utils/plugins/pluginOptionsStorage.js' /** * Build the onSave payload from collected string inputs. * * Sensitive fields are never prepopulated in the text buffer (security), so * by the time the user reaches the last field every sensitive field they * stepped through contains '' in collected. To avoid silently wiping saved * secrets on reconfigure: if a sensitive field is '' AND initialValues has * a value for it, OMIT the key entirely. savePluginOptions only writes keys * it receives, so omitting = keep existing. * * Exported for unit testing. */ export function buildFinalValues( fields: string[], collected: Record, configSchema: PluginOptionSchema, initialValues: PluginOptionValues | undefined, ): PluginOptionValues { const finalValues: PluginOptionValues = {} for (const fieldKey of fields) { const schema = configSchema[fieldKey] const value = collected[fieldKey] ?? '' if ( schema?.sensitive === true && value === '' && initialValues?.[fieldKey] !== undefined ) { continue } if (schema?.type === 'number') { // Number('') returns 0, not NaN โ€” omit blank number inputs so // validateUserConfig's required check actually catches them. if (value.trim() === '') continue const num = Number(value) finalValues[fieldKey] = Number.isNaN(num) ? value : num } else if (schema?.type === 'boolean') { finalValues[fieldKey] = isEnvTruthy(value) } else { finalValues[fieldKey] = value } } return finalValues } type Props = { title: string subtitle: string configSchema: PluginOptionSchema /** Pre-fill fields when reconfiguring. Sensitive fields are not prepopulated. */ initialValues?: PluginOptionValues onSave: (config: PluginOptionValues) => void onCancel: () => void } export function PluginOptionsDialog({ title, subtitle, configSchema, initialValues, onSave, onCancel, }: Props): React.ReactNode { const fields = Object.keys(configSchema) // Prepopulate from initialValues but skip sensitive fields โ€” we don't // want to echo secrets back into the text buffer. const initialFor = useCallback( (key: string): string => { if (configSchema[key]?.sensitive === true) return '' const v = initialValues?.[key] return v === undefined ? '' : String(v) }, [configSchema, initialValues], ) const [currentFieldIndex, setCurrentFieldIndex] = useState(0) const [values, setValues] = useState>({}) const [currentInput, setCurrentInput] = useState(() => fields[0] ? initialFor(fields[0]) : '', ) const currentField = fields[currentFieldIndex] const fieldSchema = currentField ? configSchema[currentField] : null // Use Settings context so 'n' key doesn't cancel (allows typing 'n' in input). // isCancelActive={false} on Dialog keeps its own confirm:no out of the way. useKeybinding('confirm:no', onCancel, { context: 'Settings' }) // Tab to next field const handleNextField = useCallback(() => { if (currentFieldIndex < fields.length - 1 && currentField) { setValues(prev => ({ ...prev, [currentField]: currentInput })) setCurrentFieldIndex(prev => prev + 1) const nextKey = fields[currentFieldIndex + 1] setCurrentInput(nextKey ? initialFor(nextKey) : '') } }, [currentFieldIndex, fields, currentField, currentInput, initialFor]) // Enter to save current field and move to next, or save all if last const handleConfirm = useCallback(() => { if (!currentField) return const newValues = { ...values, [currentField]: currentInput } if (currentFieldIndex === fields.length - 1) { onSave(buildFinalValues(fields, newValues, configSchema, initialValues)) } else { // Move to next field setValues(newValues) setCurrentFieldIndex(prev => prev + 1) const nextKey = fields[currentFieldIndex + 1] setCurrentInput(nextKey ? initialFor(nextKey) : '') } }, [ currentField, values, currentInput, currentFieldIndex, fields, configSchema, onSave, initialFor, initialValues, ]) useKeybindings( { 'confirm:nextField': handleNextField, 'confirm:yes': handleConfirm, }, { context: 'Confirmation' }, ) // Character input handling (backspace, typing) useInput((char, key) => { // Backspace if (key.backspace || key.delete) { setCurrentInput(prev => prev.slice(0, -1)) return } // Regular character input if (char && !key.ctrl && !key.meta && !key.tab && !key.return) { setCurrentInput(prev => prev + char) } }) if (!fieldSchema || !currentField) { return null } const isSensitive = fieldSchema.sensitive === true const isRequired = fieldSchema.required === true const displayValue = isSensitive ? '*'.repeat(stringWidth(currentInput)) : currentInput return ( {fieldSchema.title || currentField} {isRequired && *} {fieldSchema.description && ( {fieldSchema.description} )} {figures.pointerSmall} {displayValue} โ–ˆ Field {currentFieldIndex + 1} of {fields.length} {currentFieldIndex < fields.length - 1 && ( Tab: Next field ยท Enter: Save and continue )} {currentFieldIndex === fields.length - 1 && ( Enter: Save configuration )} ) }