diff --git a/src/cli/components/Autocomplete.tsx b/src/cli/components/Autocomplete.tsx new file mode 100644 index 0000000..8ec08d9 --- /dev/null +++ b/src/cli/components/Autocomplete.tsx @@ -0,0 +1,54 @@ +import { Box, Text } from "ink"; +import type { ReactNode } from "react"; +import { colors } from "./colors"; + +interface AutocompleteBoxProps { + /** Header text shown at top of autocomplete */ + header: ReactNode; + children: ReactNode; +} + +/** + * Shared container for autocomplete dropdowns. + * Provides consistent styling for both file and command autocomplete. + */ +export function AutocompleteBox({ header, children }: AutocompleteBoxProps) { + return ( + + {header} + {children} + + ); +} + +interface AutocompleteItemProps { + /** Whether this item is currently selected */ + selected: boolean; + /** Unique key for React */ + children: ReactNode; +} + +/** + * Shared item component for autocomplete lists. + * Handles selection indicator and styling. + */ +export function AutocompleteItem({ + selected, + children, +}: AutocompleteItemProps) { + return ( + + {selected ? "▶ " : " "} + {children} + + ); +} diff --git a/src/cli/components/FileAutocomplete.tsx b/src/cli/components/FileAutocomplete.tsx index f083075..14701c0 100644 --- a/src/cli/components/FileAutocomplete.tsx +++ b/src/cli/components/FileAutocomplete.tsx @@ -1,7 +1,8 @@ -import { Box, Text } from "ink"; +import { Text } from "ink"; import { useEffect, useRef, useState } from "react"; import { searchFiles } from "../helpers/fileSearch"; import { useAutocompleteNavigation } from "../hooks/useAutocompleteNavigation"; +import { AutocompleteBox, AutocompleteItem } from "./Autocomplete"; import { colors } from "./colors"; import type { AutocompleteProps, FileMatch } from "./types/autocomplete"; @@ -189,29 +190,19 @@ export function FileAutocomplete({ return null; } + const header = ( + <> + File/URL autocomplete (↑↓ navigate, Tab/Enter select): + {isLoading && " Searching..."} + + ); + return ( - - - File/URL autocomplete (↑↓ to navigate, Tab/Enter to select): - {isLoading && " Searching..."} - + {matches.length > 0 ? ( <> {matches.slice(0, 10).map((item, idx) => ( - - {idx === selectedIndex ? "▶ " : " "} + {" "} {item.path} - + ))} {matches.length > 10 && ( ... and {matches.length - 10} more @@ -231,6 +222,6 @@ export function FileAutocomplete({ ) : ( isLoading && Searching... )} - + ); } diff --git a/src/cli/components/InputAssist.tsx b/src/cli/components/InputAssist.tsx index 4ab9640..e52212f 100644 --- a/src/cli/components/InputAssist.tsx +++ b/src/cli/components/InputAssist.tsx @@ -9,6 +9,7 @@ interface InputAssistProps { cursorPosition: number; onFileSelect: (path: string) => void; onCommandSelect: (command: string) => void; + onCommandAutocomplete: (command: string) => void; onAutocompleteActiveChange: (isActive: boolean) => void; agentId?: string; agentName?: string | null; @@ -27,6 +28,7 @@ export function InputAssist({ cursorPosition, onFileSelect, onCommandSelect, + onCommandAutocomplete, onAutocompleteActiveChange, agentId, agentName, @@ -68,6 +70,7 @@ export function InputAssist({ currentInput={currentInput} cursorPosition={cursorPosition} onSelect={onCommandSelect} + onAutocomplete={onCommandAutocomplete} onActiveChange={onAutocompleteActiveChange} agentId={agentId} workingDirectory={workingDirectory} diff --git a/src/cli/components/InputRich.tsx b/src/cli/components/InputRich.tsx index 6d6c164..aa7618c 100644 --- a/src/cli/components/InputRich.tsx +++ b/src/cli/components/InputRich.tsx @@ -458,10 +458,10 @@ export function Input({ setCursorPos(newCursorPos); }; - // Handle slash command selection from autocomplete + // Handle slash command selection from autocomplete (Enter key - execute) const handleCommandSelect = async (selectedCommand: string) => { - // For slash commands, submit immediately when selected from autocomplete - // This provides a better UX - selecting /model should open the model selector + // For slash commands, submit immediately when selected via Enter + // This provides a better UX - pressing Enter on /model should open the model selector const commandToSubmit = selectedCommand.trim(); // Add to history if not a duplicate of the last entry @@ -477,6 +477,14 @@ export function Input({ await onSubmit(commandToSubmit); }; + // Handle slash command autocomplete (Tab key - fill text only) + const handleCommandAutocomplete = (selectedCommand: string) => { + // Just fill in the command text without executing + // User can then press Enter to execute or continue typing arguments + setValue(selectedCommand); + setCursorPos(selectedCommand.length); + }; + // Get display name and color for permission mode const getModeInfo = () => { switch (currentMode) { @@ -567,6 +575,7 @@ export function Input({ cursorPosition={currentCursorPosition} onFileSelect={handleFileSelect} onCommandSelect={handleCommandSelect} + onCommandAutocomplete={handleCommandAutocomplete} onAutocompleteActiveChange={setIsAutocompleteActive} agentId={agentId} agentName={agentName} diff --git a/src/cli/components/SlashCommandAutocomplete.tsx b/src/cli/components/SlashCommandAutocomplete.tsx index 5552d0d..4af9cdc 100644 --- a/src/cli/components/SlashCommandAutocomplete.tsx +++ b/src/cli/components/SlashCommandAutocomplete.tsx @@ -1,9 +1,9 @@ -import { Box, Text } from "ink"; +import { Text } from "ink"; import { useEffect, useMemo, useState } from "react"; import { settingsManager } from "../../settings-manager"; import { commands } from "../commands/registry"; import { useAutocompleteNavigation } from "../hooks/useAutocompleteNavigation"; -import { colors } from "./colors"; +import { AutocompleteBox, AutocompleteItem } from "./Autocomplete"; import type { AutocompleteProps, CommandMatch } from "./types/autocomplete"; // Compute filtered command list (excluding hidden commands) @@ -42,6 +42,7 @@ export function SlashCommandAutocomplete({ currentInput, cursorPosition = currentInput.length, onSelect, + onAutocomplete, onActiveChange, agentId, workingDirectory = process.cwd(), @@ -82,6 +83,9 @@ export function SlashCommandAutocomplete({ const { selectedIndex } = useAutocompleteNavigation({ matches, onSelect: onSelect ? (item) => onSelect(item.cmd) : undefined, + onAutocomplete: onAutocomplete + ? (item) => onAutocomplete(item.cmd) + : undefined, onActiveChange, }); @@ -131,25 +135,13 @@ export function SlashCommandAutocomplete({ } return ( - - ↑↓ navigate, Tab/Enter select + {matches.map((item, idx) => ( - - {idx === selectedIndex ? "▶ " : " "} + {item.cmd.padEnd(14)}{" "} {item.desc} - + ))} - + ); } diff --git a/src/cli/components/types/autocomplete.ts b/src/cli/components/types/autocomplete.ts index 1502884..3b553c9 100644 --- a/src/cli/components/types/autocomplete.ts +++ b/src/cli/components/types/autocomplete.ts @@ -10,8 +10,10 @@ export interface AutocompleteProps { currentInput: string; /** Current cursor position in the input */ cursorPosition?: number; - /** Callback when an item is selected */ + /** Callback when an item is selected (Enter key - may execute) */ onSelect?: (value: string) => void; + /** Callback when an item is autocompleted (Tab key - fill text only) */ + onAutocomplete?: (value: string) => void; /** Callback when autocomplete active state changes */ onActiveChange?: (isActive: boolean) => void; /** Current agent ID for context-sensitive command filtering */ diff --git a/src/cli/hooks/useAutocompleteNavigation.ts b/src/cli/hooks/useAutocompleteNavigation.ts index d74d9b9..8910430 100644 --- a/src/cli/hooks/useAutocompleteNavigation.ts +++ b/src/cli/hooks/useAutocompleteNavigation.ts @@ -6,8 +6,10 @@ interface UseAutocompleteNavigationOptions { matches: T[]; /** Maximum number of visible items (for wrapping navigation) */ maxVisible?: number; - /** Callback when an item is selected via Tab or Enter */ + /** Callback when an item is selected via Tab (autocomplete only) or Enter (when onAutocomplete is not provided) */ onSelect?: (item: T) => void; + /** Callback when an item is autocompleted via Tab (fill text without executing) */ + onAutocomplete?: (item: T) => void; /** Callback when active state changes (has matches or not) */ onActiveChange?: (isActive: boolean) => void; /** Skip automatic active state management (for components with async loading) */ @@ -29,6 +31,7 @@ export function useAutocompleteNavigation({ matches, maxVisible, onSelect, + onAutocomplete, onActiveChange, manageActiveState = true, disabled = false, @@ -65,7 +68,17 @@ export function useAutocompleteNavigation({ setSelectedIndex((prev) => (prev > 0 ? prev - 1 : maxIndex)); } else if (key.downArrow) { setSelectedIndex((prev) => (prev < maxIndex ? prev + 1 : 0)); - } else if ((key.tab || key.return) && onSelect) { + } else if (key.tab) { + const selected = matches[selectedIndex]; + if (selected) { + // Tab: use onAutocomplete if provided, otherwise fall back to onSelect + if (onAutocomplete) { + onAutocomplete(selected); + } else if (onSelect) { + onSelect(selected); + } + } + } else if (key.return && onSelect) { const selected = matches[selectedIndex]; if (selected) { onSelect(selected);