From 4fbffdf617b2fb87c1b663391fbe488c5c81beb8 Mon Sep 17 00:00:00 2001
From: Billy Biggs <bbiggs@google.com>
Date: Fri, 27 Jun 2025 10:57:32 -0700
Subject: Handle stdin for prompts using readline for escape character parsing
 (#1972)

---
 packages/cli/src/ui/components/InputPrompt.tsx | 90 +++++++++++++++-----------
 1 file changed, 51 insertions(+), 39 deletions(-)

(limited to 'packages/cli/src/ui/components/InputPrompt.tsx')

diff --git a/packages/cli/src/ui/components/InputPrompt.tsx b/packages/cli/src/ui/components/InputPrompt.tsx
index 6721132d..d687cca9 100644
--- a/packages/cli/src/ui/components/InputPrompt.tsx
+++ b/packages/cli/src/ui/components/InputPrompt.tsx
@@ -5,7 +5,7 @@
  */
 
 import React, { useCallback, useEffect, useState } from 'react';
-import { Text, Box, useInput } from 'ink';
+import { Box, Text } from 'ink';
 import { Colors } from '../colors.js';
 import { SuggestionsDisplay } from './SuggestionsDisplay.js';
 import { useInputHistory } from '../hooks/useInputHistory.js';
@@ -16,6 +16,7 @@ import stringWidth from 'string-width';
 import process from 'node:process';
 import { useShellHistory } from '../hooks/useShellHistory.js';
 import { useCompletion } from '../hooks/useCompletion.js';
+import { useKeypress, Key } from '../hooks/useKeypress.js';
 import { isAtCommand, isSlashCommand } from '../utils/commandUtils.js';
 import { SlashCommand } from '../hooks/slashCommandProcessor.js';
 import { Config } from '@google/gemini-cli-core';
@@ -155,29 +156,29 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
     ],
   );
 
-  useInput(
-    (input, key) => {
+  const handleInput = useCallback(
+    (key: Key) => {
       if (!focus) {
         return;
       }
       const query = buffer.text;
 
-      if (input === '!' && query === '' && !completion.showSuggestions) {
+      if (key.sequence === '!' && query === '' && !completion.showSuggestions) {
         setShellModeActive(!shellModeActive);
         buffer.setText(''); // Clear the '!' from input
         return true;
       }
 
       if (completion.showSuggestions) {
-        if (key.upArrow) {
+        if (key.name === 'up') {
           completion.navigateUp();
           return;
         }
-        if (key.downArrow) {
+        if (key.name === 'down') {
           completion.navigateDown();
           return;
         }
-        if (key.tab) {
+        if (key.name === 'tab') {
           if (completion.suggestions.length > 0) {
             const targetIndex =
               completion.activeSuggestionIndex === -1
@@ -189,7 +190,7 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
           }
           return;
         }
-        if (key.return) {
+        if (key.name === 'return') {
           if (completion.activeSuggestionIndex >= 0) {
             handleAutocomplete(completion.activeSuggestionIndex);
           } else if (query.trim()) {
@@ -199,19 +200,19 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
         }
       } else {
         // Keybindings when suggestions are not shown
-        if (key.ctrl && input === 'l') {
+        if (key.ctrl && key.name === 'l') {
           onClearScreen();
-          return true;
+          return;
         }
-        if (key.ctrl && input === 'p') {
+        if (key.ctrl && key.name === 'p') {
           inputHistory.navigateUp();
-          return true;
+          return;
         }
-        if (key.ctrl && input === 'n') {
+        if (key.ctrl && key.name === 'n') {
           inputHistory.navigateDown();
-          return true;
+          return;
         }
-        if (key.escape) {
+        if (key.name === 'escape') {
           if (shellModeActive) {
             setShellModeActive(false);
             return;
@@ -222,54 +223,55 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
       }
 
       // Ctrl+A (Home)
-      if (key.ctrl && input === 'a') {
+      if (key.ctrl && key.name === 'a') {
         buffer.move('home');
         buffer.moveToOffset(0);
         return;
       }
       // Ctrl+E (End)
-      if (key.ctrl && input === 'e') {
+      if (key.ctrl && key.name === 'e') {
         buffer.move('end');
         buffer.moveToOffset(cpLen(buffer.text));
         return;
       }
       // Ctrl+L (Clear Screen)
-      if (key.ctrl && input === 'l') {
+      if (key.ctrl && key.name === 'l') {
         onClearScreen();
         return;
       }
       // Ctrl+P (History Up)
-      if (key.ctrl && input === 'p' && !completion.showSuggestions) {
+      if (key.ctrl && key.name === 'p' && !completion.showSuggestions) {
         inputHistory.navigateUp();
         return;
       }
       // Ctrl+N (History Down)
-      if (key.ctrl && input === 'n' && !completion.showSuggestions) {
+      if (key.ctrl && key.name === 'n' && !completion.showSuggestions) {
         inputHistory.navigateDown();
         return;
       }
 
       // Core text editing from MultilineTextEditor's useInput
-      if (key.ctrl && input === 'k') {
+      if (key.ctrl && key.name === 'k') {
         buffer.killLineRight();
         return;
       }
-      if (key.ctrl && input === 'u') {
+      if (key.ctrl && key.name === 'u') {
         buffer.killLineLeft();
         return;
       }
       const isCtrlX =
-        (key.ctrl && (input === 'x' || input === '\x18')) || input === '\x18';
+        (key.ctrl && (key.name === 'x' || key.sequence === '\x18')) ||
+        key.sequence === '\x18';
       const isCtrlEFromEditor =
-        (key.ctrl && (input === 'e' || input === '\x05')) ||
-        input === '\x05' ||
+        (key.ctrl && (key.name === 'e' || key.sequence === '\x05')) ||
+        key.sequence === '\x05' ||
         (!key.ctrl &&
-          input === 'e' &&
-          input.length === 1 &&
-          input.charCodeAt(0) === 5);
+          key.name === 'e' &&
+          key.sequence.length === 1 &&
+          key.sequence.charCodeAt(0) === 5);
 
       if (isCtrlX || isCtrlEFromEditor) {
-        if (isCtrlEFromEditor && !(key.ctrl && input === 'e')) {
+        if (isCtrlEFromEditor && !(key.ctrl && key.name === 'e')) {
           // Avoid double handling Ctrl+E
           buffer.openInExternalEditor();
           return;
@@ -284,16 +286,15 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
         process.env['TEXTBUFFER_DEBUG'] === '1' ||
         process.env['TEXTBUFFER_DEBUG'] === 'true'
       ) {
-        console.log('[InputPromptCombined] event', { input, key });
+        console.log('[InputPromptCombined] event', { key });
       }
 
       // Ctrl+Enter for newline, Enter for submit
-      if (key.return) {
+      if (key.name === 'return') {
         const [row, col] = buffer.cursor;
         const line = buffer.lines[row];
         const charBefore = col > 0 ? cpSlice(line, col - 1, col) : '';
-
-        if (key.ctrl || charBefore === '\\') {
+        if (key.ctrl || key.meta || charBefore === '\\' || key.paste) {
           // Ctrl+Enter or escaped newline
           if (charBefore === '\\') {
             buffer.backspace();
@@ -309,7 +310,7 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
       }
 
       // Standard arrow navigation within the buffer
-      if (key.upArrow && !completion.showSuggestions) {
+      if (key.name === 'up' && !completion.showSuggestions) {
         if (shellModeActive) {
           const prevCommand = shellHistory.getPreviousCommand();
           if (prevCommand !== null) {
@@ -328,7 +329,7 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
         }
         return;
       }
-      if (key.downArrow && !completion.showSuggestions) {
+      if (key.name === 'down' && !completion.showSuggestions) {
         if (shellModeActive) {
           const nextCommand = shellHistory.getNextCommand();
           if (nextCommand !== null) {
@@ -349,13 +350,24 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
       }
 
       // Fallback to buffer's default input handling
-      buffer.handleInput(input, key as Record<string, boolean>);
-    },
-    {
-      isActive: focus,
+      buffer.handleInput(key);
     },
+    [
+      focus,
+      buffer,
+      completion,
+      shellModeActive,
+      setShellModeActive,
+      onClearScreen,
+      inputHistory,
+      handleAutocomplete,
+      handleSubmitAndClear,
+      shellHistory,
+    ],
   );
 
+  useKeypress(handleInput, { isActive: focus });
+
   const linesToRender = buffer.viewportVisualLines;
   const [cursorVisualRowAbsolute, cursorVisualColAbsolute] =
     buffer.visualCursor;
-- 
cgit v1.2.3

