Add autocomplete for slash commands

This commit is contained in:
Seth Troisi 2025-05-01 00:52:01 +00:00
parent f237082c37
commit cc838fad44
4 changed files with 62 additions and 17 deletions

View File

@ -26,7 +26,7 @@ import { ConsoleOutput } from './components/ConsolePatcher.js';
import { HistoryItemDisplay } from './components/HistoryItemDisplay.js'; import { HistoryItemDisplay } from './components/HistoryItemDisplay.js';
import { useCompletion } from './hooks/useCompletion.js'; import { useCompletion } from './hooks/useCompletion.js';
import { SuggestionsDisplay } from './components/SuggestionsDisplay.js'; import { SuggestionsDisplay } from './components/SuggestionsDisplay.js';
import { isAtCommand } from './utils/commandUtils.js'; import { isAtCommand, isSlashCommand } from './utils/commandUtils.js';
interface AppProps { interface AppProps {
config: Config; config: Config;
@ -96,7 +96,8 @@ export const App = ({ config, settings, cliVersion }: AppProps) => {
const completion = useCompletion( const completion = useCompletion(
query, query,
config.getTargetDir(), config.getTargetDir(),
isInputActive && isAtCommand(query), isInputActive && (isAtCommand(query) || isSlashCommand(query)),
slashCommands,
); );
// --- Render Logic --- // --- Render Logic ---

View File

@ -47,25 +47,37 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
return; return;
} }
const selectedSuggestion = suggestions[activeSuggestionIndex]; const selectedSuggestion = suggestions[activeSuggestionIndex];
const atIndex = query.lastIndexOf('@'); const trimmedQuery = query.trimStart();
if (atIndex === -1) return;
// Find the part of the query after the '@' if (trimmedQuery.startsWith('/')) {
const pathPart = query.substring(atIndex + 1); // Handle / command completion
// Find the last slash within that part const slashIndex = query.indexOf('/');
const lastSlashIndexInPath = pathPart.lastIndexOf('/'); const base = query.substring(0, slashIndex + 1);
const newValue = base + selectedSuggestion.value;
let base = ''; setQuery(newValue);
if (lastSlashIndexInPath === -1) {
// No slash after '@', replace everything after '@'
base = query.substring(0, atIndex + 1);
} else { } else {
// Slash found, keep everything up to and including the last slash // Handle @ command completion
base = query.substring(0, atIndex + 1 + lastSlashIndexInPath + 1); 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 resetCompletion(); // Hide suggestions after selection
setInputKey((k) => k + 1); // Increment key to force re-render and cursor reset setInputKey((k) => k + 1); // Increment key to force re-render and cursor reset
}, [ }, [

View File

@ -12,6 +12,8 @@ import {
MAX_SUGGESTIONS_TO_SHOW, MAX_SUGGESTIONS_TO_SHOW,
Suggestion, Suggestion,
} from '../components/SuggestionsDisplay.js'; } from '../components/SuggestionsDisplay.js';
import { SlashCommand } from './slashCommandProcessor.js';
export interface UseCompletionReturn { export interface UseCompletionReturn {
suggestions: Suggestion[]; suggestions: Suggestion[];
activeSuggestionIndex: number; activeSuggestionIndex: number;
@ -29,6 +31,7 @@ export function useCompletion(
query: string, query: string,
cwd: string, cwd: string,
isActive: boolean, isActive: boolean,
slashCommands: SlashCommand[],
): UseCompletionReturn { ): UseCompletionReturn {
const [suggestions, setSuggestions] = useState<Suggestion[]>([]); const [suggestions, setSuggestions] = useState<Suggestion[]>([]);
const [activeSuggestionIndex, setActiveSuggestionIndex] = const [activeSuggestionIndex, setActiveSuggestionIndex] =
@ -111,6 +114,26 @@ export function useCompletion(
return; 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('@'); const atIndex = query.lastIndexOf('@');
if (atIndex === -1) { if (atIndex === -1) {
resetCompletionState(); resetCompletionState();

View File

@ -16,6 +16,15 @@ export const isAtCommand = (query: string): boolean =>
// Check if starts with @ OR has a space, then @, then a non-space character. // Check if starts with @ OR has a space, then @, then a non-space character.
query.startsWith('@') || /\s@\S/.test(query); 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[] = ['/', '@', '!', '?', '$']; const control_symbols: string[] = ['/', '@', '!', '?', '$'];
/** /**
* Returns the first word of query with optional leading slash, ampersand, bang. * Returns the first word of query with optional leading slash, ampersand, bang.