oli-server 0.1.4

A simple, blazingly fast AI coding assistant server
Documentation
import React, { useState, useEffect, useMemo } from "react";
import { Box, Text, useInput } from "ink";
import theme from "../styles/gruvbox.js";

// Command interface
interface Command {
  name: string;
  description: string;
  value: string;
}

// Component props
interface CommandPaletteProps {
  visible: boolean;
  filterText: string;
  onSelect: (command: string) => void;
  onFilteredCommandsChange?: (commands: Command[]) => void;
  onSelectedIndexChange?: (index: number) => void;
}

// Available commands
const AVAILABLE_COMMANDS: Command[] = [
  { name: "help", description: "Show help information", value: "/help" },
  { name: "clear", description: "Clear conversation history", value: "/clear" },
  { name: "model", description: "Change the current model", value: "/model" },
  { name: "exit", description: "Exit the application", value: "/exit" },
];

// Command palette component
const CommandPalette: React.FC<CommandPaletteProps> = ({
  visible,
  filterText,
  onSelect,
  onFilteredCommandsChange,
  onSelectedIndexChange,
}) => {
  const [filteredCommands, setFilteredCommands] = useState<Command[]>([]);
  const [selectedIndex, setSelectedIndex] = useState(0);

  // Update parent component when filteredCommands change
  useEffect(() => {
    if (onFilteredCommandsChange) {
      onFilteredCommandsChange(filteredCommands);
    }
  }, [filteredCommands, onFilteredCommandsChange]);

  // Update parent component when selectedIndex changes
  useEffect(() => {
    if (onSelectedIndexChange) {
      onSelectedIndexChange(selectedIndex);
    }
  }, [selectedIndex, onSelectedIndexChange]);

  // Filter commands based on input with debouncing
  useEffect(() => {
    if (!visible) return;

    // Use timeout to debounce filtering for better performance
    const debounceTimeout = setTimeout(() => {
      // Strip the leading slash for filtering
      const searchText = filterText.startsWith("/")
        ? filterText.slice(1)
        : filterText;

      // Filter commands that match the input
      const filtered = AVAILABLE_COMMANDS.filter((cmd) =>
        cmd.name.toLowerCase().includes(searchText.toLowerCase()),
      );

      setFilteredCommands(filtered);
      // Reset selection to first item when filter changes
      setSelectedIndex(0);
    }, 5); // Small debounce time for better responsiveness

    return () => clearTimeout(debounceTimeout);
  }, [filterText, visible]);

  // Handle keyboard navigation using Ink's useInput hook instead of DOM events
  useInput((input, key) => {
    if (!visible) return;

    if (key.downArrow) {
      setSelectedIndex((prev) =>
        Math.min(prev + 1, filteredCommands.length - 1),
      );
    } else if (key.upArrow) {
      setSelectedIndex((prev) => Math.max(prev - 1, 0));
    } else if (key.return || key.tab) {
      if (filteredCommands.length > 0) {
        onSelect(filteredCommands[selectedIndex].value);
      }
    }
  });

  // Enhanced descriptions for commands
  const enhancedCommands = useMemo(() => {
    return [
      {
        name: "help",
        description: "Show help and available commands",
        value: "/help",
      },
      {
        name: "clear",
        description: "Clear conversation history and free up context",
        value: "/clear",
      },
      {
        name: "model",
        description: "Change the current model",
        value: "/model",
      },
      { name: "exit", description: "Exit the application", value: "/exit" },
    ].filter((cmd) => filteredCommands.some((fc) => fc.name === cmd.name));
  }, [filteredCommands]);

  // Memoize the command list for efficient rendering
  const commandItems = useMemo(() => {
    return enhancedCommands.map((command, index) => {
      const isSelected = index === selectedIndex;
      return (
        <Box key={command.value} paddingY={0} paddingX={2} marginY={0}>
          <Box width={16} marginRight={2}>
            <Text
              bold
              color={
                isSelected ? theme.colors.dark.yellow : theme.colors.dark.green
              }
            >
              /{command.name}
            </Text>
          </Box>
          <Text
            color={
              isSelected ? theme.colors.dark.yellow : theme.colors.dark.fg4
            }
          >
            {command.description}
          </Text>
        </Box>
      );
    });
  }, [enhancedCommands, selectedIndex]);

  if (!visible || filteredCommands.length === 0) return null;

  return (
    <Box flexDirection="column" width="100%" marginBottom={1}>
      {commandItems}
    </Box>
  );
};

export default React.memo(CommandPalette);