diff --git a/packages/cli/src/ui/App.tsx b/packages/cli/src/ui/App.tsx index 5ddf13db..194a18cf 100644 --- a/packages/cli/src/ui/App.tsx +++ b/packages/cli/src/ui/App.tsx @@ -26,7 +26,7 @@ import { ConsoleOutput } from './components/ConsolePatcher.js'; import { HistoryItemDisplay } from './components/HistoryItemDisplay.js'; import { useCompletion } from './hooks/useCompletion.js'; import { SuggestionsDisplay } from './components/SuggestionsDisplay.js'; -import { isAtCommand } from './utils/commandUtils.js'; +import { isAtCommand, isSlashCommand } from './utils/commandUtils.js'; interface AppProps { config: Config; @@ -96,7 +96,8 @@ export const App = ({ config, settings, cliVersion }: AppProps) => { const completion = useCompletion( query, config.getTargetDir(), - isInputActive && isAtCommand(query), + isInputActive && (isAtCommand(query) || isSlashCommand(query)), + slashCommands, ); // --- Render Logic --- diff --git a/packages/cli/src/ui/components/InputPrompt.tsx b/packages/cli/src/ui/components/InputPrompt.tsx index fbf84766..1c8cff4d 100644 --- a/packages/cli/src/ui/components/InputPrompt.tsx +++ b/packages/cli/src/ui/components/InputPrompt.tsx @@ -47,25 +47,37 @@ export const InputPrompt: React.FC = ({ return; } const selectedSuggestion = suggestions[activeSuggestionIndex]; - const atIndex = query.lastIndexOf('@'); - if (atIndex === -1) return; + const trimmedQuery = query.trimStart(); - // Find the part of the query after the '@' - const pathPart = query.substring(atIndex + 1); - // Find the last slash within that part - const lastSlashIndexInPath = pathPart.lastIndexOf('/'); - - let base = ''; - if (lastSlashIndexInPath === -1) { - // No slash after '@', replace everything after '@' - base = query.substring(0, atIndex + 1); + if (trimmedQuery.startsWith('/')) { + // Handle / command completion + const slashIndex = query.indexOf('/'); + const base = query.substring(0, slashIndex + 1); + const newValue = base + selectedSuggestion.value; + setQuery(newValue); } else { - // Slash found, keep everything up to and including the last slash - base = query.substring(0, atIndex + 1 + lastSlashIndexInPath + 1); + // Handle @ command completion + const atIndex = query.lastIndexOf('@'); + if (atIndex === -1) return; + + // Find the part of the query after the '@' + const pathPart = query.substring(atIndex + 1); + // Find the last slash within that part + const lastSlashIndexInPath = pathPart.lastIndexOf('/'); + + let base = ''; + if (lastSlashIndexInPath === -1) { + // No slash after '@', replace everything after '@' + base = query.substring(0, atIndex + 1); + } else { + // Slash found, keep everything up to and including the last slash + base = query.substring(0, atIndex + 1 + lastSlashIndexInPath + 1); + } + + const newValue = base + selectedSuggestion.value; + setQuery(newValue); } - const newValue = base + selectedSuggestion.value; - setQuery(newValue); resetCompletion(); // Hide suggestions after selection setInputKey((k) => k + 1); // Increment key to force re-render and cursor reset }, [ diff --git a/packages/cli/src/ui/hooks/useCompletion.ts b/packages/cli/src/ui/hooks/useCompletion.ts index 07a71630..31c59bcf 100644 --- a/packages/cli/src/ui/hooks/useCompletion.ts +++ b/packages/cli/src/ui/hooks/useCompletion.ts @@ -12,6 +12,8 @@ import { MAX_SUGGESTIONS_TO_SHOW, Suggestion, } from '../components/SuggestionsDisplay.js'; +import { SlashCommand } from './slashCommandProcessor.js'; + export interface UseCompletionReturn { suggestions: Suggestion[]; activeSuggestionIndex: number; @@ -29,6 +31,7 @@ export function useCompletion( query: string, cwd: string, isActive: boolean, + slashCommands: SlashCommand[], ): UseCompletionReturn { const [suggestions, setSuggestions] = useState([]); const [activeSuggestionIndex, setActiveSuggestionIndex] = @@ -111,6 +114,26 @@ export function useCompletion( return; } + const trimmedQuery = query.trimStart(); // Trim leading whitespace + + // --- Handle Slash Command Completion --- + if (trimmedQuery.startsWith('/')) { + const partialCommand = trimmedQuery.substring(1); + const filteredSuggestions = slashCommands + .map((cmd) => cmd.name) + .filter((name) => name.startsWith(partialCommand)) + .map((name) => ({ label: name, value: name })) + .sort(); + + setSuggestions(filteredSuggestions); + setShowSuggestions(filteredSuggestions.length > 0); + setActiveSuggestionIndex(-1); + setVisibleStartIndex(0); + setIsLoadingSuggestions(false); + return; + } + + // --- Handle At Command Completion --- const atIndex = query.lastIndexOf('@'); if (atIndex === -1) { resetCompletionState(); diff --git a/packages/cli/src/ui/utils/commandUtils.ts b/packages/cli/src/ui/utils/commandUtils.ts index 64046658..bcae7b6c 100644 --- a/packages/cli/src/ui/utils/commandUtils.ts +++ b/packages/cli/src/ui/utils/commandUtils.ts @@ -16,6 +16,15 @@ export const isAtCommand = (query: string): boolean => // Check if starts with @ OR has a space, then @, then a non-space character. query.startsWith('@') || /\s@\S/.test(query); +/** + * Checks if a query string potentially represents an '/' command. + * It triggers if the query starts with '/' + * + * @param query The input query string. + * @returns True if the query looks like an '/' command, false otherwise. + */ +export const isSlashCommand = (query: string): boolean => query.startsWith('/'); + const control_symbols: string[] = ['/', '@', '!', '?', '$']; /** * Returns the first word of query with optional leading slash, ampersand, bang.