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);