From c1268076e75c53c3ec85cd99995f78b808cd4b1f Mon Sep 17 00:00:00 2001 From: Charles Packer Date: Tue, 23 Dec 2025 21:13:46 -0800 Subject: [PATCH] feat: add scrolling and manual ordering to command autocomplete (#381) Co-authored-by: Letta --- src/cli/commands/registry.ts | 402 ++++++++++-------- src/cli/components/HelpDialog.tsx | 6 +- .../components/SlashCommandAutocomplete.tsx | 53 ++- src/cli/components/types/autocomplete.ts | 1 + 4 files changed, 269 insertions(+), 193 deletions(-) diff --git a/src/cli/commands/registry.ts b/src/cli/commands/registry.ts index 8f7ec87..b9e7c2d 100644 --- a/src/cli/commands/registry.ts +++ b/src/cli/commands/registry.ts @@ -7,15 +7,235 @@ interface Command { desc: string; handler: CommandHandler; hidden?: boolean; // Hidden commands don't show in autocomplete but still work + order?: number; // Lower numbers appear first in autocomplete (default: 100) } export const commands: Record = { + // === Page 1: Most commonly used (order 10-19) === + "/pinned": { + desc: "Browse pinned agents", + order: 10, + handler: () => { + // Handled specially in App.tsx to open pinned agents selector + return "Opening pinned agents..."; + }, + }, "/model": { desc: "Switch model", + order: 11, handler: () => { return "Opening model selector..."; }, }, + "/init": { + desc: "Initialize (or re-init) your agent's memory", + order: 12, + handler: () => { + // Handled specially in App.tsx to send initialization prompt + return "Initializing memory..."; + }, + }, + "/remember": { + desc: "Remember something from the conversation (/remember [instructions])", + order: 13, + handler: () => { + // Handled specially in App.tsx to trigger memory update + return "Processing memory request..."; + }, + }, + "/skill": { + desc: "Enter skill creation mode (/skill [description])", + order: 14, + handler: () => { + // Handled specially in App.tsx to trigger skill-creation workflow + return "Starting skill creation..."; + }, + }, + "/memory": { + desc: "View your agent's memory blocks", + order: 15, + handler: () => { + // Handled specially in App.tsx to open memory viewer + return "Opening memory viewer..."; + }, + }, + "/search": { + desc: "Search messages across all agents", + order: 16, + handler: () => { + // Handled specially in App.tsx to show message search + return "Opening message search..."; + }, + }, + "/clear": { + desc: "Clear conversation history", + order: 17, + handler: () => { + // Handled specially in App.tsx to access client and agent ID + return "Clearing messages..."; + }, + }, + + // === Page 2: Agent management (order 20-29) === + "/new": { + desc: "Create a new agent and switch to it", + order: 20, + handler: () => { + // Handled specially in App.tsx + return "Creating new agent..."; + }, + }, + "/agents": { + desc: "Browse all agents", + order: 21, + handler: () => { + // Handled specially in App.tsx to show agent selector + return "Opening agent selector..."; + }, + }, + "/pin": { + desc: "Pin current agent globally, or use -l for local only", + order: 22, + handler: () => { + // Handled specially in App.tsx + return "Pinning agent..."; + }, + }, + "/unpin": { + desc: "Unpin current agent globally, or use -l for local only", + order: 23, + handler: () => { + // Handled specially in App.tsx + return "Unpinning agent..."; + }, + }, + "/rename": { + desc: "Rename the current agent (/rename )", + order: 24, + handler: () => { + // Handled specially in App.tsx to access agent ID and client + return "Renaming agent..."; + }, + }, + "/description": { + desc: "Update the current agent's description (/description )", + order: 25, + handler: () => { + // Handled specially in App.tsx to access agent ID and client + return "Updating description..."; + }, + }, + "/download": { + desc: "Download AgentFile (.af)", + order: 26, + handler: () => { + // Handled specially in App.tsx to access agent ID and client + return "Downloading agent file..."; + }, + }, + "/toolset": { + desc: "Switch toolset (replaces /link and /unlink)", + order: 27, + handler: () => { + // Handled specially in App.tsx to access agent ID and client + return "Opening toolset selector..."; + }, + }, + + // === Page 3: Advanced features (order 30-39) === + "/system": { + desc: "Switch system prompt", + order: 30, + handler: () => { + // Handled specially in App.tsx to open system prompt selector + return "Opening system prompt selector..."; + }, + }, + "/subagents": { + desc: "Manage custom subagents", + order: 31, + handler: () => { + // Handled specially in App.tsx to open SubagentManager component + return "Opening subagent manager..."; + }, + }, + "/mcp": { + desc: "Manage MCP servers", + order: 32, + handler: () => { + // Handled specially in App.tsx to show MCP server selector + return "Opening MCP server manager..."; + }, + }, + "/usage": { + desc: "Show session usage statistics and balance", + order: 33, + handler: () => { + // Handled specially in App.tsx to display usage stats + return "Fetching usage statistics..."; + }, + }, + "/feedback": { + desc: "Send feedback to the Letta team", + order: 34, + handler: () => { + // Handled specially in App.tsx to send feedback request + return "Sending feedback..."; + }, + }, + "/help": { + desc: "Show available commands", + order: 35, + hidden: true, // Redundant with improved autocomplete, but still works if typed + handler: () => { + // Handled specially in App.tsx to open help dialog + return "Opening help..."; + }, + }, + + // === Session management (order 40-49) === + "/connect": { + desc: "Connect an existing Claude account (/connect claude)", + order: 40, + handler: () => { + // Handled specially in App.tsx + return "Initiating OAuth connection..."; + }, + }, + "/disconnect": { + desc: "Disconnect from Claude OAuth", + order: 41, + handler: () => { + // Handled specially in App.tsx + return "Disconnecting..."; + }, + }, + "/bg": { + desc: "Show background shell processes", + order: 42, + handler: () => { + // Handled specially in App.tsx to show background processes + return "Showing background processes..."; + }, + }, + "/exit": { + desc: "Exit this session", + order: 43, + handler: () => { + // Handled specially in App.tsx + return "Exiting..."; + }, + }, + "/logout": { + desc: "Clear credentials and exit", + order: 44, + handler: () => { + // Handled specially in App.tsx to access settings manager + return "Clearing credentials..."; + }, + }, + + // === Hidden commands (not shown in autocomplete) === "/stream": { desc: "Toggle token streaming on/off", hidden: true, @@ -24,20 +244,6 @@ export const commands: Record = { return "Toggling token streaming..."; }, }, - "/exit": { - desc: "Exit this session", - handler: () => { - // Handled specially in App.tsx - return "Exiting..."; - }, - }, - "/clear": { - desc: "Clear conversation history", - handler: () => { - // Handled specially in App.tsx to access client and agent ID - return "Clearing messages..."; - }, - }, "/compact": { desc: "Summarize conversation history (compaction)", hidden: true, @@ -46,27 +252,6 @@ export const commands: Record = { return "Compacting conversation..."; }, }, - "/logout": { - desc: "Clear credentials and exit", - handler: () => { - // Handled specially in App.tsx to access settings manager - return "Clearing credentials..."; - }, - }, - "/rename": { - desc: "Rename the current agent (/rename )", - handler: () => { - // Handled specially in App.tsx to access agent ID and client - return "Renaming agent..."; - }, - }, - "/description": { - desc: "Update the current agent's description (/description )", - handler: () => { - // Handled specially in App.tsx to access agent ID and client - return "Updating description..."; - }, - }, "/link": { desc: "Attach all Letta Code tools to agent (deprecated, use /toolset instead)", hidden: true, @@ -83,62 +268,6 @@ export const commands: Record = { return "Unlinking tools..."; }, }, - "/toolset": { - desc: "Switch toolset (replaces /link and /unlink)", - handler: () => { - // Handled specially in App.tsx to access agent ID and client - return "Opening toolset selector..."; - }, - }, - "/system": { - desc: "Switch system prompt", - handler: () => { - // Handled specially in App.tsx to open system prompt selector - return "Opening system prompt selector..."; - }, - }, - "/download": { - desc: "Download AgentFile (.af)", - handler: () => { - // Handled specially in App.tsx to access agent ID and client - return "Downloading agent file..."; - }, - }, - "/bg": { - desc: "Show background shell processes", - handler: () => { - // Handled specially in App.tsx to show background processes - return "Showing background processes..."; - }, - }, - "/init": { - desc: "Initialize agent memory for this project", - handler: () => { - // Handled specially in App.tsx to send initialization prompt - return "Initializing memory..."; - }, - }, - "/skill": { - desc: "Enter skill creation mode (/skill [description])", - handler: () => { - // Handled specially in App.tsx to trigger skill-creation workflow - return "Starting skill creation..."; - }, - }, - "/remember": { - desc: "Remember something from the conversation (/remember [instructions])", - handler: () => { - // Handled specially in App.tsx to trigger memory update - return "Processing memory request..."; - }, - }, - "/agents": { - desc: "Browse and switch to another agent", - handler: () => { - // Handled specially in App.tsx to show agent selector - return "Opening agent selector..."; - }, - }, "/resume": { desc: "Browse and switch to another agent", hidden: true, // Backwards compatibility alias for /agents @@ -147,97 +276,6 @@ export const commands: Record = { return "Opening agent selector..."; }, }, - "/search": { - desc: "Search messages across all agents", - handler: () => { - // Handled specially in App.tsx to show message search - return "Opening message search..."; - }, - }, - "/pin": { - desc: "Pin current agent globally, or use -l for local only", - handler: () => { - // Handled specially in App.tsx - return "Pinning agent..."; - }, - }, - "/unpin": { - desc: "Unpin current agent globally, or use -l for local only", - handler: () => { - // Handled specially in App.tsx - return "Unpinning agent..."; - }, - }, - "/pinned": { - desc: "Show pinned agents", - handler: () => { - // Handled specially in App.tsx to open pinned agents selector - return "Opening pinned agents..."; - }, - }, - "/new": { - desc: "Create a new agent and switch to it", - handler: () => { - // Handled specially in App.tsx - return "Creating new agent..."; - }, - }, - "/subagents": { - desc: "Manage custom subagents", - handler: () => { - // Handled specially in App.tsx to open SubagentManager component - return "Opening subagent manager..."; - }, - }, - "/feedback": { - desc: "Send feedback to the Letta team", - handler: () => { - // Handled specially in App.tsx to send feedback request - return "Sending feedback..."; - }, - }, - "/memory": { - desc: "View agent memory blocks", - handler: () => { - // Handled specially in App.tsx to open memory viewer - return "Opening memory viewer..."; - }, - }, - "/usage": { - desc: "Show session usage statistics and balance", - handler: () => { - // Handled specially in App.tsx to display usage stats - return "Fetching usage statistics..."; - }, - }, - "/mcp": { - desc: "Manage MCP servers", - handler: () => { - // Handled specially in App.tsx to show MCP server selector - return "Opening MCP server manager..."; - }, - }, - "/help": { - desc: "Show available commands", - handler: () => { - // Handled specially in App.tsx to open help dialog - return "Opening help..."; - }, - }, - "/connect": { - desc: "Connect to Claude via OAuth (/connect claude)", - handler: () => { - // Handled specially in App.tsx - return "Initiating OAuth connection..."; - }, - }, - "/disconnect": { - desc: "Disconnect from Claude OAuth", - handler: () => { - // Handled specially in App.tsx - return "Disconnecting..."; - }, - }, }; /** diff --git a/src/cli/components/HelpDialog.tsx b/src/cli/components/HelpDialog.tsx index b9042f7..72fe865 100644 --- a/src/cli/components/HelpDialog.tsx +++ b/src/cli/components/HelpDialog.tsx @@ -12,6 +12,7 @@ const HELP_TABS: HelpTab[] = ["commands", "shortcuts"]; interface CommandItem { name: string; description: string; + order: number; } interface ShortcutItem { @@ -28,15 +29,16 @@ export function HelpDialog({ onClose }: HelpDialogProps) { const [currentPage, setCurrentPage] = useState(0); const [selectedIndex, setSelectedIndex] = useState(0); - // Get all non-hidden commands + // Get all non-hidden commands, sorted by order const allCommands = useMemo(() => { return Object.entries(commands) .filter(([_, cmd]) => !cmd.hidden) .map(([name, cmd]) => ({ name, description: cmd.desc, + order: cmd.order ?? 100, })) - .sort((a, b) => a.name.localeCompare(b.name)); + .sort((a, b) => a.order - b.order); }, []); // Keyboard shortcuts diff --git a/src/cli/components/SlashCommandAutocomplete.tsx b/src/cli/components/SlashCommandAutocomplete.tsx index 4af9cdc..d127d4f 100644 --- a/src/cli/components/SlashCommandAutocomplete.tsx +++ b/src/cli/components/SlashCommandAutocomplete.tsx @@ -6,14 +6,17 @@ import { useAutocompleteNavigation } from "../hooks/useAutocompleteNavigation"; import { AutocompleteBox, AutocompleteItem } from "./Autocomplete"; import type { AutocompleteProps, CommandMatch } from "./types/autocomplete"; -// Compute filtered command list (excluding hidden commands) +const VISIBLE_COMMANDS = 8; // Number of commands visible at once + +// Compute filtered command list (excluding hidden commands), sorted by order const _allCommands: CommandMatch[] = Object.entries(commands) .filter(([, { hidden }]) => !hidden) - .map(([cmd, { desc }]) => ({ + .map(([cmd, { desc, order }]) => ({ cmd, desc, + order: order ?? 100, // Default order for commands without explicit order })) - .sort((a, b) => a.cmd.localeCompare(b.cmd)); + .sort((a, b) => a.order - b.order); // Extract the text after the "/" symbol where the cursor is positioned function extractSearchQuery( @@ -134,14 +137,46 @@ export function SlashCommandAutocomplete({ return null; } + // Calculate visible window based on selected index + const totalMatches = matches.length; + const needsScrolling = totalMatches > VISIBLE_COMMANDS; + + let startIndex = 0; + if (needsScrolling) { + // Keep selected item visible, preferring to show it in the middle + const halfWindow = Math.floor(VISIBLE_COMMANDS / 2); + startIndex = Math.max(0, selectedIndex - halfWindow); + startIndex = Math.min(startIndex, totalMatches - VISIBLE_COMMANDS); + } + + const visibleMatches = matches.slice( + startIndex, + startIndex + VISIBLE_COMMANDS, + ); + const showScrollUp = startIndex > 0; + const showScrollDown = startIndex + VISIBLE_COMMANDS < totalMatches; + return ( - {matches.map((item, idx) => ( - - {item.cmd.padEnd(14)}{" "} - {item.desc} - - ))} + {showScrollUp && ↑ {startIndex} more above} + {visibleMatches.map((item, idx) => { + const actualIndex = startIndex + idx; + return ( + + {item.cmd.padEnd(14)}{" "} + {item.desc} + + ); + })} + {showScrollDown && ( + + {" "} + ↓ {totalMatches - startIndex - VISIBLE_COMMANDS} more below + + )} ); } diff --git a/src/cli/components/types/autocomplete.ts b/src/cli/components/types/autocomplete.ts index 3b553c9..2c043c5 100644 --- a/src/cli/components/types/autocomplete.ts +++ b/src/cli/components/types/autocomplete.ts @@ -36,4 +36,5 @@ export interface FileMatch { export interface CommandMatch { cmd: string; desc: string; + order?: number; }