import { Box, useInput } from "ink"; import { useCallback, useEffect, useMemo, useState } from "react"; import { getVersion } from "../../version"; import { commands } from "../commands/registry"; import { colors } from "./colors"; import { Text } from "./Text"; const PAGE_SIZE = 10; type HelpTab = "commands" | "shortcuts"; const HELP_TABS: HelpTab[] = ["commands", "shortcuts"]; interface CommandItem { name: string; description: string; order: number; } interface ShortcutItem { keys: string; description: string; } interface HelpDialogProps { onClose: () => void; } export function HelpDialog({ onClose }: HelpDialogProps) { const [activeTab, setActiveTab] = useState("commands"); const [currentPage, setCurrentPage] = useState(0); const [selectedIndex, setSelectedIndex] = useState(0); const [customCommands, setCustomCommands] = useState([]); // Load custom commands once on mount useEffect(() => { import("../commands/custom.js").then(({ getCustomCommands }) => { getCustomCommands().then((customs) => { setCustomCommands( customs.map((cmd) => ({ name: `/${cmd.id}`, description: `${cmd.description} (${cmd.source}${cmd.namespace ? `:${cmd.namespace}` : ""})`, order: 200 + (cmd.source === "project" ? 0 : 100), })), ); }); }); }, []); // Get all non-hidden commands, sorted by order (includes custom commands) const allCommands = useMemo(() => { const builtins = Object.entries(commands) .filter(([_, cmd]) => !cmd.hidden) .map(([name, cmd]) => ({ name, description: cmd.desc, order: cmd.order ?? 100, })); return [...builtins, ...customCommands].sort((a, b) => a.order - b.order); }, [customCommands]); // Keyboard shortcuts const shortcuts = useMemo(() => { return [ { keys: "/", description: "Open command autocomplete" }, { keys: "@", description: "Open file autocomplete" }, { keys: "Esc", description: "Cancel dialog / clear input (double press)", }, { keys: "Tab", description: "Autocomplete command or file path" }, { keys: "↓", description: "Navigate down / next command in history" }, { keys: "↑", description: "Navigate up / previous command in history" }, { keys: "Shift+Enter", description: "Insert newline (multi-line input)" }, { keys: "Opt+Enter", description: "Insert newline (alternative)" }, { keys: "Ctrl+C", description: "Interrupt operation / exit (double press)", }, { keys: "Ctrl+V", description: "Paste content or image" }, ]; }, []); const cycleTab = useCallback(() => { setActiveTab((current) => { const idx = HELP_TABS.indexOf(current); return HELP_TABS[(idx + 1) % HELP_TABS.length] as HelpTab; }); setCurrentPage(0); setSelectedIndex(0); }, []); const visibleItems = activeTab === "commands" ? allCommands : shortcuts; const totalPages = Math.ceil(visibleItems.length / PAGE_SIZE); const startIndex = currentPage * PAGE_SIZE; const visiblePageItems = visibleItems.slice( startIndex, startIndex + PAGE_SIZE, ); useInput( useCallback( (input, key) => { // CTRL-C: immediately close if (key.ctrl && input === "c") { onClose(); return; } if (key.escape) { onClose(); } else if (key.tab) { cycleTab(); } else if (key.upArrow) { setSelectedIndex((prev) => Math.max(0, prev - 1)); } else if (key.downArrow) { setSelectedIndex((prev) => Math.min(visiblePageItems.length - 1, prev + 1), ); } else if (input === "j" || input === "J") { // Previous page if (currentPage > 0) { setCurrentPage((prev) => prev - 1); setSelectedIndex(0); } } else if (input === "k" || input === "K") { // Next page if (currentPage < totalPages - 1) { setCurrentPage((prev) => prev + 1); setSelectedIndex(0); } } else if (key.leftArrow && currentPage > 0) { setCurrentPage((prev) => prev - 1); setSelectedIndex(0); } else if (key.rightArrow && currentPage < totalPages - 1) { setCurrentPage((prev) => prev + 1); setSelectedIndex(0); } }, [currentPage, totalPages, visiblePageItems.length, onClose, cycleTab], ), { isActive: true }, ); const version = getVersion(); const getTabLabel = (tab: HelpTab) => { if (tab === "commands") return `Commands (${allCommands.length})`; return `Shortcuts (${shortcuts.length})`; }; return ( Letta Code v{version} (↑↓ navigate, ←→/jk page, ESC close) Tab: {HELP_TABS.map((tab, i) => ( {i > 0 && · } {getTabLabel(tab)} ))} (Tab to switch) Page {currentPage + 1}/{totalPages} {activeTab === "commands" && (visiblePageItems as CommandItem[]).map((command, index) => { const isSelected = index === selectedIndex; return ( {isSelected ? "›" : " "} {command.name} {command.description} ); })} {activeTab === "shortcuts" && (visiblePageItems as ShortcutItem[]).map((shortcut, index) => { const isSelected = index === selectedIndex; return ( {isSelected ? "›" : " "} {shortcut.keys} {shortcut.description} ); })} Getting started: • Run /init to initialize agent memory for this project • Press / at any time to see command autocomplete • Visit https://docs.letta.com/letta-code for more help ); }