From e2c3611c637f6d18fd5f6354e496b89cdb6bd173 Mon Sep 17 00:00:00 2001
From: Jacob Richman <jacob314@gmail.com>
Date: Tue, 13 May 2025 11:24:04 -0700
Subject: Multiline editor (#302)

Co-authored-by: Taylor Mullen <ntaylormullen@google.com>
---
 packages/cli/src/ui/components/InputPrompt.tsx | 90 ++++++++++++++++----------
 1 file changed, 56 insertions(+), 34 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 20d4bcdf..072fe074 100644
--- a/packages/cli/src/ui/components/InputPrompt.tsx
+++ b/packages/cli/src/ui/components/InputPrompt.tsx
@@ -5,40 +5,47 @@
  */
 
 import React, { useCallback } from 'react';
-import { Text, Box, useInput, useFocus, Key } from 'ink';
-import TextInput from 'ink-text-input';
+import { Text, Box, Key } from 'ink';
 import { Colors } from '../colors.js';
 import { Suggestion } from './SuggestionsDisplay.js';
+import { MultilineTextEditor } from './shared/multiline-editor.js';
 
 interface InputPromptProps {
   query: string;
-  setQuery: React.Dispatch<React.SetStateAction<string>>;
-  inputKey: number;
-  setInputKey: React.Dispatch<React.SetStateAction<number>>;
+  onChange: (value: string) => void;
+  onChangeAndMoveCursor: (value: string) => void;
+  editorState: EditorState;
   onSubmit: (value: string) => void;
   showSuggestions: boolean;
   suggestions: Suggestion[];
   activeSuggestionIndex: number;
-  navigateUp: () => void;
-  navigateDown: () => void;
   resetCompletion: () => void;
+  navigateHistoryUp: () => void;
+  navigateHistoryDown: () => void;
+  navigateSuggestionUp: () => void;
+  navigateSuggestionDown: () => void;
+}
+
+export interface EditorState {
+  key: number;
+  initialCursorOffset?: number;
 }
 
 export const InputPrompt: React.FC<InputPromptProps> = ({
   query,
-  setQuery,
-  inputKey,
-  setInputKey,
+  onChange,
+  onChangeAndMoveCursor,
+  editorState,
   onSubmit,
   showSuggestions,
   suggestions,
   activeSuggestionIndex,
-  navigateUp,
-  navigateDown,
+  navigateHistoryUp,
+  navigateHistoryDown,
+  navigateSuggestionUp,
+  navigateSuggestionDown,
   resetCompletion,
 }) => {
-  const { isFocused } = useFocus({ autoFocus: true });
-
   const handleAutocomplete = useCallback(
     (indexToUse: number) => {
       if (indexToUse < 0 || indexToUse >= suggestions.length) {
@@ -52,7 +59,7 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
         const slashIndex = query.indexOf('/');
         const base = query.substring(0, slashIndex + 1);
         const newValue = base + selectedSuggestion.value;
-        setQuery(newValue);
+        onChangeAndMoveCursor(newValue);
       } else {
         // Handle @ command completion
         const atIndex = query.lastIndexOf('@');
@@ -73,32 +80,30 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
         }
 
         const newValue = base + selectedSuggestion.value;
-        setQuery(newValue);
+        onChangeAndMoveCursor(newValue);
       }
 
       resetCompletion(); // Hide suggestions after selection
-      setInputKey((k) => k + 1); // Increment key to force re-render and cursor reset
     },
-    [query, setQuery, suggestions, resetCompletion, setInputKey],
+    [query, suggestions, resetCompletion, onChangeAndMoveCursor],
   );
 
-  useInput(
+  const inputPreprocessor = useCallback(
     (input: string, key: Key) => {
-      if (!isFocused) {
-        return;
-      }
-
       if (showSuggestions) {
         if (key.upArrow) {
-          navigateUp();
+          navigateSuggestionUp();
+          return true;
         } else if (key.downArrow) {
-          navigateDown();
+          navigateSuggestionDown();
+          return true;
         } else if (key.tab) {
           if (suggestions.length > 0) {
             const targetIndex =
               activeSuggestionIndex === -1 ? 0 : activeSuggestionIndex;
             if (targetIndex < suggestions.length) {
               handleAutocomplete(targetIndex);
+              return true;
             }
           }
         } else if (key.return) {
@@ -109,34 +114,51 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
               onSubmit(query);
             }
           }
+          return true;
         } else if (key.escape) {
           resetCompletion();
+          return true;
         }
       }
-      // Enter key when suggestions are NOT showing is handled by TextInput's onSubmit prop below
+      return false;
     },
-    { isActive: true },
+    [
+      handleAutocomplete,
+      navigateSuggestionDown,
+      navigateSuggestionUp,
+      query,
+      suggestions,
+      showSuggestions,
+      resetCompletion,
+      activeSuggestionIndex,
+      onSubmit,
+    ],
   );
 
   return (
     <Box borderStyle="round" borderColor={Colors.AccentBlue} paddingX={1}>
       <Text color={Colors.AccentPurple}>&gt; </Text>
       <Box flexGrow={1}>
-        <TextInput
-          key={inputKey.toString()}
-          value={query}
-          onChange={setQuery}
+        <MultilineTextEditor
+          key={editorState.key.toString()}
+          initialCursorOffset={editorState.initialCursorOffset}
+          initialText={query}
+          onChange={onChange}
           placeholder="Enter your message or use tools (e.g., @src/file.txt)..."
+          /* Account for width used by the box and &gt; */
+          navigateUp={navigateHistoryUp}
+          navigateDown={navigateHistoryDown}
+          inputPreprocessor={inputPreprocessor}
+          widthUsedByParent={3}
+          widthFraction={0.9}
           onSubmit={() => {
             // This onSubmit is for the TextInput component itself.
             // It should only fire if suggestions are NOT showing,
-            // as useInput handles Enter when suggestions are visible.
+            // as inputPreprocessor handles Enter when suggestions are visible.
             const trimmedQuery = query.trim();
             if (!showSuggestions && trimmedQuery) {
               onSubmit(trimmedQuery);
             }
-            // If suggestions ARE showing, useInput's Enter handler
-            // would have already dealt with it (either completing or submitting).
           }}
         />
       </Box>
-- 
cgit v1.2.3

