From aa10ccba713d49bef6bf474bfd72c0852e3da611 Mon Sep 17 00:00:00 2001
From: Abhi <43648792+abhipatel12@users.noreply.github.com>
Date: Mon, 7 Jul 2025 16:45:44 -0400
Subject: feature(commands) - Refactor Slash Command + Vision For the Future
 (#3175)

---
 packages/cli/src/ui/components/InputPrompt.tsx | 294 +++++++++++--------------
 1 file changed, 126 insertions(+), 168 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 763d4e7e..3771f5b9 100644
--- a/packages/cli/src/ui/components/InputPrompt.tsx
+++ b/packages/cli/src/ui/components/InputPrompt.tsx
@@ -13,12 +13,11 @@ import { TextBuffer } from './shared/text-buffer.js';
 import { cpSlice, cpLen } from '../utils/textUtils.js';
 import chalk from 'chalk';
 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 { CommandContext, SlashCommand } from '../commands/types.js';
 import { Config } from '@google/gemini-cli-core';
 
 export interface InputPromptProps {
@@ -26,8 +25,9 @@ export interface InputPromptProps {
   onSubmit: (value: string) => void;
   userMessages: readonly string[];
   onClearScreen: () => void;
-  config: Config; // Added config for useCompletion
-  slashCommands: SlashCommand[]; // Added slashCommands for useCompletion
+  config: Config;
+  slashCommands: SlashCommand[];
+  commandContext: CommandContext;
   placeholder?: string;
   focus?: boolean;
   inputWidth: number;
@@ -43,6 +43,7 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
   onClearScreen,
   config,
   slashCommands,
+  commandContext,
   placeholder = '  Type your message or @path/to/file',
   focus = true,
   inputWidth,
@@ -57,6 +58,7 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
     config.getTargetDir(),
     isAtCommand(buffer.text) || isSlashCommand(buffer.text),
     slashCommands,
+    commandContext,
     config,
   );
 
@@ -116,28 +118,46 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
       const suggestion = completionSuggestions[indexToUse].value;
 
       if (query.trimStart().startsWith('/')) {
-        const parts = query.trimStart().substring(1).split(' ');
-        const commandName = parts[0];
-        const slashIndex = query.indexOf('/');
-        const base = query.substring(0, slashIndex + 1);
-
-        const command = slashCommands.find((cmd) => cmd.name === commandName);
-        // Make sure completion isn't the original command when command.completion hasn't happened yet.
-        if (command && command.completion && suggestion !== commandName) {
-          const newValue = `${base}${commandName} ${suggestion}`;
-          if (newValue === query) {
-            handleSubmitAndClear(newValue);
-          } else {
-            buffer.setText(newValue);
-          }
-        } else {
-          const newValue = base + suggestion;
-          if (newValue === query) {
-            handleSubmitAndClear(newValue);
-          } else {
-            buffer.setText(newValue);
+        const hasTrailingSpace = query.endsWith(' ');
+        const parts = query
+          .trimStart()
+          .substring(1)
+          .split(/\s+/)
+          .filter(Boolean);
+
+        let isParentPath = false;
+        // If there's no trailing space, we need to check if the current query
+        // is already a complete path to a parent command.
+        if (!hasTrailingSpace) {
+          let currentLevel: SlashCommand[] | undefined = slashCommands;
+          for (let i = 0; i < parts.length; i++) {
+            const part = parts[i];
+            const found: SlashCommand | undefined = currentLevel?.find(
+              (cmd) => cmd.name === part || cmd.altName === part,
+            );
+
+            if (found) {
+              if (i === parts.length - 1 && found.subCommands) {
+                isParentPath = true;
+              }
+              currentLevel = found.subCommands;
+            } else {
+              // Path is invalid, so it can't be a parent path.
+              currentLevel = undefined;
+              break;
+            }
           }
         }
+
+        // Determine the base path of the command.
+        // - If there's a trailing space, the whole command is the base.
+        // - If it's a known parent path, the whole command is the base.
+        // - Otherwise, the base is everything EXCEPT the last partial part.
+        const basePath =
+          hasTrailingSpace || isParentPath ? parts : parts.slice(0, -1);
+        const newValue = `/${[...basePath, suggestion].join(' ')} `;
+
+        buffer.setText(newValue);
       } else {
         const atIndex = query.lastIndexOf('@');
         if (atIndex === -1) return;
@@ -155,13 +175,7 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
       }
       resetCompletionState();
     },
-    [
-      resetCompletionState,
-      handleSubmitAndClear,
-      buffer,
-      completionSuggestions,
-      slashCommands,
-    ],
+    [resetCompletionState, buffer, completionSuggestions, slashCommands],
   );
 
   const handleInput = useCallback(
@@ -169,12 +183,32 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
       if (!focus) {
         return;
       }
-      const query = buffer.text;
 
-      if (key.sequence === '!' && query === '' && !completion.showSuggestions) {
+      if (
+        key.sequence === '!' &&
+        buffer.text === '' &&
+        !completion.showSuggestions
+      ) {
         setShellModeActive(!shellModeActive);
         buffer.setText(''); // Clear the '!' from input
-        return true;
+        return;
+      }
+
+      if (key.name === 'escape') {
+        if (shellModeActive) {
+          setShellModeActive(false);
+          return;
+        }
+
+        if (completion.showSuggestions) {
+          completion.resetCompletionState();
+          return;
+        }
+      }
+
+      if (key.ctrl && key.name === 'l') {
+        onClearScreen();
+        return;
       }
 
       if (completion.showSuggestions) {
@@ -186,11 +220,12 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
           completion.navigateDown();
           return;
         }
-        if (key.name === 'tab') {
+
+        if (key.name === 'tab' || (key.name === 'return' && !key.ctrl)) {
           if (completion.suggestions.length > 0) {
             const targetIndex =
               completion.activeSuggestionIndex === -1
-                ? 0
+                ? 0 // Default to the first if none is active
                 : completion.activeSuggestionIndex;
             if (targetIndex < completion.suggestions.length) {
               handleAutocomplete(targetIndex);
@@ -198,67 +233,72 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
           }
           return;
         }
-        if (key.name === 'return') {
-          if (completion.activeSuggestionIndex >= 0) {
-            handleAutocomplete(completion.activeSuggestionIndex);
-          } else if (query.trim()) {
-            handleSubmitAndClear(query);
-          }
-          return;
-        }
       } else {
-        // Keybindings when suggestions are not shown
-        if (key.ctrl && key.name === 'l') {
-          onClearScreen();
-          return;
-        }
-        if (key.ctrl && key.name === 'p') {
-          inputHistory.navigateUp();
-          return;
-        }
-        if (key.ctrl && key.name === 'n') {
-          inputHistory.navigateDown();
-          return;
-        }
-        if (key.name === 'escape') {
-          if (shellModeActive) {
-            setShellModeActive(false);
+        if (!shellModeActive) {
+          if (key.ctrl && key.name === 'p') {
+            inputHistory.navigateUp();
             return;
           }
-          completion.resetCompletionState();
+          if (key.ctrl && key.name === 'n') {
+            inputHistory.navigateDown();
+            return;
+          }
+          // Handle arrow-up/down for history on single-line or at edges
+          if (
+            key.name === 'up' &&
+            (buffer.allVisualLines.length === 1 ||
+              (buffer.visualCursor[0] === 0 && buffer.visualScrollRow === 0))
+          ) {
+            inputHistory.navigateUp();
+            return;
+          }
+          if (
+            key.name === 'down' &&
+            (buffer.allVisualLines.length === 1 ||
+              buffer.visualCursor[0] === buffer.allVisualLines.length - 1)
+          ) {
+            inputHistory.navigateDown();
+            return;
+          }
+        } else {
+          // Shell History Navigation
+          if (key.name === 'up') {
+            const prevCommand = shellHistory.getPreviousCommand();
+            if (prevCommand !== null) buffer.setText(prevCommand);
+            return;
+          }
+          if (key.name === 'down') {
+            const nextCommand = shellHistory.getNextCommand();
+            if (nextCommand !== null) buffer.setText(nextCommand);
+            return;
+          }
+        }
+
+        if (key.name === 'return' && !key.ctrl && !key.meta && !key.paste) {
+          if (buffer.text.trim()) {
+            handleSubmitAndClear(buffer.text);
+          }
           return;
         }
       }
 
-      // Ctrl+A (Home)
+      // Newline insertion
+      if (key.name === 'return' && (key.ctrl || key.meta || key.paste)) {
+        buffer.newline();
+        return;
+      }
+
+      // Ctrl+A (Home) / Ctrl+E (End)
       if (key.ctrl && key.name === 'a') {
         buffer.move('home');
-        buffer.moveToOffset(0);
         return;
       }
-      // Ctrl+E (End)
       if (key.ctrl && key.name === 'e') {
         buffer.move('end');
-        buffer.moveToOffset(cpLen(buffer.text));
-        return;
-      }
-      // Ctrl+L (Clear Screen)
-      if (key.ctrl && key.name === 'l') {
-        onClearScreen();
-        return;
-      }
-      // Ctrl+P (History Up)
-      if (key.ctrl && key.name === 'p' && !completion.showSuggestions) {
-        inputHistory.navigateUp();
-        return;
-      }
-      // Ctrl+N (History Down)
-      if (key.ctrl && key.name === 'n' && !completion.showSuggestions) {
-        inputHistory.navigateDown();
         return;
       }
 
-      // Core text editing from MultilineTextEditor's useInput
+      // Kill line commands
       if (key.ctrl && key.name === 'k') {
         buffer.killLineRight();
         return;
@@ -267,97 +307,15 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
         buffer.killLineLeft();
         return;
       }
-      const isCtrlX =
-        (key.ctrl && (key.name === 'x' || key.sequence === '\x18')) ||
-        key.sequence === '\x18';
-      const isCtrlEFromEditor =
-        (key.ctrl && (key.name === 'e' || key.sequence === '\x05')) ||
-        key.sequence === '\x05' ||
-        (!key.ctrl &&
-          key.name === 'e' &&
-          key.sequence.length === 1 &&
-          key.sequence.charCodeAt(0) === 5);
-
-      if (isCtrlX || isCtrlEFromEditor) {
-        if (isCtrlEFromEditor && !(key.ctrl && key.name === 'e')) {
-          // Avoid double handling Ctrl+E
-          buffer.openInExternalEditor();
-          return;
-        }
-        if (isCtrlX) {
-          buffer.openInExternalEditor();
-          return;
-        }
-      }
-
-      if (
-        process.env['TEXTBUFFER_DEBUG'] === '1' ||
-        process.env['TEXTBUFFER_DEBUG'] === 'true'
-      ) {
-        console.log('[InputPromptCombined] event', { key });
-      }
-
-      // Ctrl+Enter for newline, Enter for submit
-      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 || key.meta || charBefore === '\\' || key.paste) {
-          // Ctrl+Enter or escaped newline
-          if (charBefore === '\\') {
-            buffer.backspace();
-          }
-          buffer.newline();
-        } else {
-          // Enter for submit
-          if (query.trim()) {
-            handleSubmitAndClear(query);
-          }
-        }
-        return;
-      }
 
-      // Standard arrow navigation within the buffer
-      if (key.name === 'up' && !completion.showSuggestions) {
-        if (shellModeActive) {
-          const prevCommand = shellHistory.getPreviousCommand();
-          if (prevCommand !== null) {
-            buffer.setText(prevCommand);
-          }
-          return;
-        }
-        if (
-          (buffer.allVisualLines.length === 1 || // Always navigate for single line
-            (buffer.visualCursor[0] === 0 && buffer.visualScrollRow === 0)) &&
-          inputHistory.navigateUp
-        ) {
-          inputHistory.navigateUp();
-        } else {
-          buffer.move('up');
-        }
-        return;
-      }
-      if (key.name === 'down' && !completion.showSuggestions) {
-        if (shellModeActive) {
-          const nextCommand = shellHistory.getNextCommand();
-          if (nextCommand !== null) {
-            buffer.setText(nextCommand);
-          }
-          return;
-        }
-        if (
-          (buffer.allVisualLines.length === 1 || // Always navigate for single line
-            buffer.visualCursor[0] === buffer.allVisualLines.length - 1) &&
-          inputHistory.navigateDown
-        ) {
-          inputHistory.navigateDown();
-        } else {
-          buffer.move('down');
-        }
+      // External editor
+      const isCtrlX = key.ctrl && (key.name === 'x' || key.sequence === '\x18');
+      if (isCtrlX) {
+        buffer.openInExternalEditor();
         return;
       }
 
-      // Fallback to buffer's default input handling
+      // Fallback to the text buffer's default input handling for all other keys
       buffer.handleInput(key);
     },
     [
-- 
cgit v1.2.3

