feat: Autocomplete for slash commands (#216)

This commit is contained in:
Devansh Jain
2025-12-15 12:54:52 -08:00
committed by GitHub
parent 3c0b60f82d
commit 7ce41e52f4
9 changed files with 416 additions and 326 deletions

View File

@@ -0,0 +1,121 @@
import { Box, Text } from "ink";
import { useEffect, useState } from "react";
import { commands } from "../commands/registry";
import { useAutocompleteNavigation } from "../hooks/useAutocompleteNavigation";
import { colors } from "./colors";
import type { AutocompleteProps, CommandMatch } from "./types/autocomplete";
// Compute filtered command list (excluding hidden commands)
const allCommands: CommandMatch[] = Object.entries(commands)
.filter(([, { hidden }]) => !hidden)
.map(([cmd, { desc }]) => ({
cmd,
desc,
}))
.sort((a, b) => a.cmd.localeCompare(b.cmd));
// Extract the text after the "/" symbol where the cursor is positioned
function extractSearchQuery(
input: string,
cursor: number,
): { query: string; hasSpaceAfter: boolean } | null {
if (!input.startsWith("/")) return null;
const afterSlash = input.slice(1);
const spaceIndex = afterSlash.indexOf(" ");
const endPos = spaceIndex === -1 ? input.length : 1 + spaceIndex;
// Check if cursor is within this /command
if (cursor < 0 || cursor > endPos) {
return null;
}
const query =
spaceIndex === -1 ? afterSlash : afterSlash.slice(0, spaceIndex);
const hasSpaceAfter = spaceIndex !== -1;
return { query, hasSpaceAfter };
}
export function SlashCommandAutocomplete({
currentInput,
cursorPosition = currentInput.length,
onSelect,
onActiveChange,
}: AutocompleteProps) {
const [matches, setMatches] = useState<CommandMatch[]>([]);
const { selectedIndex } = useAutocompleteNavigation({
matches,
onSelect: onSelect ? (item) => onSelect(item.cmd) : undefined,
onActiveChange,
});
// Update matches when input changes
useEffect(() => {
const result = extractSearchQuery(currentInput, cursorPosition);
if (!result) {
setMatches([]);
return;
}
const { query, hasSpaceAfter } = result;
// If there's a space after the command, user has moved on - hide autocomplete
if (hasSpaceAfter) {
setMatches([]);
return;
}
let newMatches: CommandMatch[];
// If query is empty (just typed "/"), show all commands
if (query.length === 0) {
newMatches = allCommands;
} else {
// Filter commands that contain the query (case-insensitive)
// Match against the command name without the leading "/"
const lowerQuery = query.toLowerCase();
newMatches = allCommands.filter((item) => {
const cmdName = item.cmd.slice(1).toLowerCase(); // Remove leading "/"
return cmdName.includes(lowerQuery);
});
}
setMatches(newMatches);
}, [currentInput, cursorPosition]);
// Don't show if input doesn't start with "/"
if (!currentInput.startsWith("/")) {
return null;
}
// Don't show if no matches
if (matches.length === 0) {
return null;
}
return (
<Box
flexDirection="column"
borderStyle="round"
borderColor={colors.command.border}
paddingX={1}
marginBottom={1}
>
<Text dimColor> navigate, Tab/Enter select</Text>
{matches.map((item, idx) => (
<Text
key={item.cmd}
color={idx === selectedIndex ? colors.command.selected : undefined}
bold={idx === selectedIndex}
>
{idx === selectedIndex ? "▶ " : " "}
{item.cmd.padEnd(14)}{" "}
<Text dimColor={idx !== selectedIndex}>{item.desc}</Text>
</Text>
))}
</Box>
);
}