/** * @license * Copyright 2025 Google LLC * SPDX-License-Identifier: Apache-2.0 */ import type React from 'react'; import { useEffect, useState, useRef, useCallback } from 'react'; import { Box, Text } from 'ink'; import { theme } from '../semantic-colors.js'; import type { ExtensionManager } from '../../config/extension-manager.js'; import { configureExtension, configureSpecificSetting, configureAllExtensions, type ConfigLogger, type RequestSettingCallback, type RequestConfirmationCallback, } from '../../commands/extensions/utils.js'; import { ExtensionSettingScope, type ExtensionSetting, } from '../../config/extensions/extensionSettings.js'; import { TextInput } from './shared/TextInput.js'; import { useTextBuffer } from './shared/text-buffer.js'; import { DialogFooter } from './shared/DialogFooter.js'; import { type Key, useKeypress } from '../hooks/useKeypress.js'; export interface ConfigExtensionDialogProps { extensionManager: ExtensionManager; onClose: () => void; extensionName?: string; settingKey?: string; scope?: ExtensionSettingScope; configureAll?: boolean; loggerAdapter: ConfigLogger; } type DialogState = | { type: 'IDLE' } | { type: 'BUSY'; message?: string } | { type: 'ASK_SETTING'; setting: ExtensionSetting; resolve: (val: string) => void; initialValue?: string; } | { type: 'ASK_CONFIRMATION'; message: string; resolve: (val: boolean) => void; } | { type: 'DONE' } | { type: 'ERROR'; error: Error }; export const ConfigExtensionDialog: React.FC = ({ extensionManager, onClose, extensionName, settingKey, scope = ExtensionSettingScope.USER, configureAll, loggerAdapter, }) => { const [state, setState] = useState({ type: 'IDLE' }); const [logMessages, setLogMessages] = useState([]); // Buffers for input const settingBuffer = useTextBuffer({ initialText: '', viewport: { width: 80, height: 1 }, singleLine: true, escapePastedPaths: true, }); const mounted = useRef(true); useEffect(() => { mounted.current = true; return () => { mounted.current = false; }; }, []); const addLog = useCallback( (msg: string) => { setLogMessages((prev) => [...prev, msg].slice(-5)); // Keep last 5 loggerAdapter.log(msg); }, [loggerAdapter], ); const requestSetting: RequestSettingCallback = useCallback( async (setting) => new Promise((resolve) => { if (!mounted.current) return; settingBuffer.setText(''); // Clear buffer setState({ type: 'ASK_SETTING', setting, resolve: (val) => { resolve(val); setState({ type: 'BUSY', message: 'Updating...' }); }, }); }), [settingBuffer], ); const requestConfirmation: RequestConfirmationCallback = useCallback( async (message) => new Promise((resolve) => { if (!mounted.current) return; setState({ type: 'ASK_CONFIRMATION', message, resolve: (val) => { resolve(val); setState({ type: 'BUSY', message: 'Processing...' }); }, }); }), [], ); useEffect(() => { async function run() { try { setState({ type: 'BUSY', message: 'Initializing...' }); // Wrap logger to capture logs locally too const localLogger: ConfigLogger = { log: (msg) => { addLog(msg); }, error: (msg) => { addLog('Error: ' + msg); loggerAdapter.error(msg); }, }; if (configureAll) { await configureAllExtensions( extensionManager, scope, localLogger, requestSetting, requestConfirmation, ); } else if (extensionName && settingKey) { await configureSpecificSetting( extensionManager, extensionName, settingKey, scope, localLogger, requestSetting, ); } else if (extensionName) { await configureExtension( extensionManager, extensionName, scope, localLogger, requestSetting, requestConfirmation, ); } if (mounted.current) { setState({ type: 'DONE' }); // Delay close slightly to show done setTimeout(onClose, 1000); } } catch (err: unknown) { if (mounted.current) { const error = err instanceof Error ? err : new Error(String(err)); setState({ type: 'ERROR', error }); loggerAdapter.error(error.message); } } } // Only run once if (state.type === 'IDLE') { void run(); } }, [ extensionManager, extensionName, settingKey, scope, configureAll, loggerAdapter, requestSetting, requestConfirmation, addLog, onClose, state.type, ]); // Handle Input Submission const handleSettingSubmit = (val: string) => { if (state.type === 'ASK_SETTING') { state.resolve(val); } }; // Handle Keys for Confirmation useKeypress( (key: Key) => { if (state.type === 'ASK_CONFIRMATION') { if (key.name === 'y' || key.name === 'return') { state.resolve(true); return true; } if (key.name === 'n' || key.name === 'escape') { state.resolve(false); return true; } } if (state.type === 'DONE' || state.type === 'ERROR') { if (key.name === 'return' || key.name === 'escape') { onClose(); return true; } } return false; }, { isActive: state.type === 'ASK_CONFIRMATION' || state.type === 'DONE' || state.type === 'ERROR', }, ); if (state.type === 'BUSY' || state.type === 'IDLE') { return ( {state.type === 'BUSY' ? state.message : 'Starting...'} {logMessages.map((msg, i) => ( {msg} ))} ); } if (state.type === 'ASK_SETTING') { return ( Configure {state.setting.name} {state.setting.description || state.setting.envVar} {'> '} ); } if (state.type === 'ASK_CONFIRMATION') { return ( Confirmation Required {state.message} Press{' '} Y {' '} to confirm or{' '} N {' '} to cancel ); } if (state.type === 'ERROR') { return ( Error {state.error.message} ); } return ( Configuration Complete ); };