feat: add scrolling and manual ordering to command autocomplete (#381)
Co-authored-by: Letta <noreply@letta.com>
This commit is contained in:
@@ -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<string, Command> = {
|
||||
// === 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 <name>)",
|
||||
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 <text>)",
|
||||
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<string, Command> = {
|
||||
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<string, Command> = {
|
||||
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 <name>)",
|
||||
handler: () => {
|
||||
// Handled specially in App.tsx to access agent ID and client
|
||||
return "Renaming agent...";
|
||||
},
|
||||
},
|
||||
"/description": {
|
||||
desc: "Update the current agent's description (/description <text>)",
|
||||
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<string, Command> = {
|
||||
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<string, Command> = {
|
||||
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...";
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -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<CommandItem[]>(() => {
|
||||
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
|
||||
|
||||
@@ -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 (
|
||||
<AutocompleteBox header="↑↓ navigate, Tab autocomplete, Enter execute">
|
||||
{matches.map((item, idx) => (
|
||||
<AutocompleteItem key={item.cmd} selected={idx === selectedIndex}>
|
||||
{item.cmd.padEnd(14)}{" "}
|
||||
<Text dimColor={idx !== selectedIndex}>{item.desc}</Text>
|
||||
</AutocompleteItem>
|
||||
))}
|
||||
{showScrollUp && <Text dimColor> ↑ {startIndex} more above</Text>}
|
||||
{visibleMatches.map((item, idx) => {
|
||||
const actualIndex = startIndex + idx;
|
||||
return (
|
||||
<AutocompleteItem
|
||||
key={item.cmd}
|
||||
selected={actualIndex === selectedIndex}
|
||||
>
|
||||
{item.cmd.padEnd(14)}{" "}
|
||||
<Text dimColor={actualIndex !== selectedIndex}>{item.desc}</Text>
|
||||
</AutocompleteItem>
|
||||
);
|
||||
})}
|
||||
{showScrollDown && (
|
||||
<Text dimColor>
|
||||
{" "}
|
||||
↓ {totalMatches - startIndex - VISIBLE_COMMANDS} more below
|
||||
</Text>
|
||||
)}
|
||||
</AutocompleteBox>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -36,4 +36,5 @@ export interface FileMatch {
|
||||
export interface CommandMatch {
|
||||
cmd: string;
|
||||
desc: string;
|
||||
order?: number;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user