diff options
| author | Abhi <[email protected]> | 2025-07-07 16:45:44 -0400 |
|---|---|---|
| committer | GitHub <[email protected]> | 2025-07-07 20:45:44 +0000 |
| commit | aa10ccba713d49bef6bf474bfd72c0852e3da611 (patch) | |
| tree | 92f1de8bec31cdb10a02fe8ddac1fbde41b75e7f /packages/cli/src/ui/components/InputPrompt.tsx | |
| parent | 6eccb474c77e41aa88d1d1d4ea7eada3e85e746c (diff) | |
feature(commands) - Refactor Slash Command + Vision For the Future (#3175)
Diffstat (limited to 'packages/cli/src/ui/components/InputPrompt.tsx')
| -rw-r--r-- | packages/cli/src/ui/components/InputPrompt.tsx | 292 |
1 files changed, 125 insertions, 167 deletions
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 hasTrailingSpace = query.endsWith(' '); + const parts = query + .trimStart() + .substring(1) + .split(/\s+/) + .filter(Boolean); - 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); + 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); }, [ |
