diff options
Diffstat (limited to 'packages/cli/src/ui/App.tsx')
| -rw-r--r-- | packages/cli/src/ui/App.tsx | 106 |
1 files changed, 73 insertions, 33 deletions
diff --git a/packages/cli/src/ui/App.tsx b/packages/cli/src/ui/App.tsx index c7ed9a81..c022fb31 100644 --- a/packages/cli/src/ui/App.tsx +++ b/packages/cli/src/ui/App.tsx @@ -11,6 +11,7 @@ import { measureElement, Static, Text, + useStdin, useInput, type Key as InkKeyType, } from 'ink'; @@ -54,8 +55,10 @@ import { useLogger } from './hooks/useLogger.js'; import { StreamingContext } from './contexts/StreamingContext.js'; import { SessionStatsProvider } from './contexts/SessionContext.js'; import { useGitBranchName } from './hooks/useGitBranchName.js'; +import { useTextBuffer } from './components/shared/text-buffer.js'; +import * as fs from 'fs'; -const CTRL_C_PROMPT_DURATION_MS = 1000; +const CTRL_EXIT_PROMPT_DURATION_MS = 1000; interface AppProps { config: Config; @@ -98,6 +101,8 @@ const App = ({ config, settings, startupWarnings = [] }: AppProps) => { HistoryItem[] | null >(null); const ctrlCTimerRef = useRef<NodeJS.Timeout | null>(null); + const [ctrlDPressedOnce, setCtrlDPressedOnce] = useState(false); + const ctrlDTimerRef = useRef<NodeJS.Timeout | null>(null); const errorCount = useMemo( () => consoleMessages.filter((msg) => msg.type === 'error').length, @@ -181,54 +186,84 @@ const App = ({ config, settings, startupWarnings = [] }: AppProps) => { setQuittingMessages, ); + const { rows: terminalHeight, columns: terminalWidth } = useTerminalSize(); + const { stdin, setRawMode } = useStdin(); + const isValidPath = useCallback((filePath: string): boolean => { + try { + return fs.existsSync(filePath) && fs.statSync(filePath).isFile(); + } catch (_e) { + return false; + } + }, []); + + const widthFraction = 0.9; + const inputWidth = Math.max( + 20, + Math.round(terminalWidth * widthFraction) - 3, + ); + const suggestionsWidth = Math.max(60, Math.floor(terminalWidth * 0.8)); + + const buffer = useTextBuffer({ + initialText: '', + viewport: { height: 10, width: inputWidth }, + stdin, + setRawMode, + isValidPath, + }); + + const handleExit = useCallback( + ( + pressedOnce: boolean, + setPressedOnce: (value: boolean) => void, + timerRef: React.MutableRefObject<NodeJS.Timeout | null>, + ) => { + if (pressedOnce) { + if (timerRef.current) { + clearTimeout(timerRef.current); + } + const quitCommand = slashCommands.find( + (cmd) => cmd.name === 'quit' || cmd.altName === 'exit', + ); + if (quitCommand) { + quitCommand.action('quit', '', ''); + } else { + process.exit(0); + } + } else { + setPressedOnce(true); + timerRef.current = setTimeout(() => { + setPressedOnce(false); + timerRef.current = null; + }, CTRL_EXIT_PROMPT_DURATION_MS); + } + }, + [slashCommands], + ); + useInput((input: string, key: InkKeyType) => { if (key.ctrl && input === 'o') { setShowErrorDetails((prev) => !prev); refreshStatic(); } else if (key.ctrl && input === 't') { - // Toggle showing tool descriptions const newValue = !showToolDescriptions; setShowToolDescriptions(newValue); refreshStatic(); - // Re-execute the MCP command to show/hide descriptions const mcpServers = config.getMcpServers(); if (Object.keys(mcpServers || {}).length > 0) { - // Pass description flag based on the new value handleSlashCommand(newValue ? '/mcp desc' : '/mcp nodesc'); } } else if (key.ctrl && (input === 'c' || input === 'C')) { - if (ctrlCPressedOnce) { - if (ctrlCTimerRef.current) { - clearTimeout(ctrlCTimerRef.current); - } - const quitCommand = slashCommands.find( - (cmd) => cmd.name === 'quit' || cmd.altName === 'exit', - ); - if (quitCommand) { - quitCommand.action('quit', '', ''); - } else { - process.exit(0); - } - } else { - setCtrlCPressedOnce(true); - ctrlCTimerRef.current = setTimeout(() => { - setCtrlCPressedOnce(false); - ctrlCTimerRef.current = null; - }, CTRL_C_PROMPT_DURATION_MS); + handleExit(ctrlCPressedOnce, setCtrlCPressedOnce, ctrlCTimerRef); + } else if (key.ctrl && (input === 'd' || input === 'D')) { + if (buffer.text.length > 0) { + // Do nothing if there is text in the input. + return; } + handleExit(ctrlDPressedOnce, setCtrlDPressedOnce, ctrlDTimerRef); } }); - useEffect( - () => () => { - if (ctrlCTimerRef.current) { - clearTimeout(ctrlCTimerRef.current); - } - }, - [], - ); - useConsolePatcher({ onNewMessage: handleNewMessage, debugMode: config.getDebugMode(), @@ -324,7 +359,6 @@ const App = ({ config, settings, startupWarnings = [] }: AppProps) => { refreshStatic(); }, [clearItems, clearConsoleMessagesState, refreshStatic]); - const { rows: terminalHeight, columns: terminalWidth } = useTerminalSize(); // Get terminalWidth const mainControlsRef = useRef<DOMElement>(null); const pendingHistoryItemRef = useRef<DOMElement>(null); @@ -514,6 +548,10 @@ const App = ({ config, settings, startupWarnings = [] }: AppProps) => { <Text color={Colors.AccentYellow}> Press Ctrl+C again to exit. </Text> + ) : ctrlDPressedOnce ? ( + <Text color={Colors.AccentYellow}> + Press Ctrl+D again to exit. + </Text> ) : ( <ContextSummaryDisplay geminiMdFileCount={geminiMdFileCount} @@ -540,7 +578,9 @@ const App = ({ config, settings, startupWarnings = [] }: AppProps) => { {isInputActive && ( <InputPrompt - widthFraction={0.9} + buffer={buffer} + inputWidth={inputWidth} + suggestionsWidth={suggestionsWidth} onSubmit={handleFinalSubmit} userMessages={userMessages} onClearScreen={handleClearScreen} |
