feat: add /help command with interactive dialog (#303)

Co-authored-by: Letta <noreply@letta.com>
This commit is contained in:
cthomas
2025-12-18 13:37:49 -08:00
committed by GitHub
parent b195b2a70d
commit 1355c44dbc
3 changed files with 257 additions and 6 deletions

View File

@@ -49,6 +49,7 @@ import { CommandMessage } from "./components/CommandMessage";
import { EnterPlanModeDialog } from "./components/EnterPlanModeDialog";
import { ErrorMessage } from "./components/ErrorMessageRich";
import { FeedbackDialog } from "./components/FeedbackDialog";
import { HelpDialog } from "./components/HelpDialog";
import { Input } from "./components/InputRich";
import { MemoryViewer } from "./components/MemoryViewer";
import { MessageSearch } from "./components/MessageSearch";
@@ -409,6 +410,7 @@ export default function App({
| "feedback"
| "memory"
| "pin"
| "help"
| null;
const [activeOverlay, setActiveOverlay] = useState<ActiveOverlay>(null);
const closeOverlay = useCallback(() => setActiveOverlay(null), []);
@@ -1753,6 +1755,12 @@ export default function App({
return { submitted: true };
}
// Special handling for /help command - opens help dialog
if (trimmed === "/help") {
setActiveOverlay("help");
return { submitted: true };
}
// Special handling for /usage command - show session stats
if (trimmed === "/usage") {
const cmdId = uid("cmd");
@@ -4601,6 +4609,9 @@ Plan file path: ${planFilePath}`;
/>
)}
{/* Help Dialog - conditionally mounted as overlay */}
{activeOverlay === "help" && <HelpDialog onClose={closeOverlay} />}
{/* Pin Dialog - for naming agent before pinning */}
{activeOverlay === "pin" && (
<PinDialog

View File

@@ -54,14 +54,14 @@ export const commands: Record<string, Command> = {
},
},
"/rename": {
desc: "Rename the current agent",
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",
desc: "Update the current agent's description (/description <text>)",
handler: () => {
// Handled specially in App.tsx to access agent ID and client
return "Updating description...";
@@ -119,14 +119,14 @@ export const commands: Record<string, Command> = {
},
},
"/skill": {
desc: "Enter skill creation mode (optionally: /skill <description>)",
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",
desc: "Remember something from the conversation (/remember [instructions])",
handler: () => {
// Handled specially in App.tsx to trigger memory update
return "Processing memory request...";
@@ -147,14 +147,14 @@ export const commands: Record<string, Command> = {
},
},
"/pin": {
desc: "Pin current agent globally (use -l for local only)",
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 (use -l for local only)",
desc: "Unpin current agent globally, or use -l for local only",
handler: () => {
// Handled specially in App.tsx
return "Unpinning agent...";
@@ -195,6 +195,13 @@ export const commands: Record<string, Command> = {
return "Fetching usage statistics...";
},
},
"/help": {
desc: "Show available commands",
handler: () => {
// Handled specially in App.tsx to open help dialog
return "Opening help...";
},
},
};
/**

View File

@@ -0,0 +1,233 @@
import { Box, Text, useInput } from "ink";
import { useCallback, useMemo, useState } from "react";
import { getVersion } from "../../version";
import { commands } from "../commands/registry";
import { colors } from "./colors";
const PAGE_SIZE = 10;
type HelpTab = "commands" | "shortcuts";
const HELP_TABS: HelpTab[] = ["commands", "shortcuts"];
interface CommandItem {
name: string;
description: string;
}
interface ShortcutItem {
keys: string;
description: string;
}
interface HelpDialogProps {
onClose: () => void;
}
export function HelpDialog({ onClose }: HelpDialogProps) {
const [activeTab, setActiveTab] = useState<HelpTab>("commands");
const [currentPage, setCurrentPage] = useState(0);
const [selectedIndex, setSelectedIndex] = useState(0);
// Get all non-hidden commands
const allCommands = useMemo<CommandItem[]>(() => {
return Object.entries(commands)
.filter(([_, cmd]) => !cmd.hidden)
.map(([name, cmd]) => ({
name,
description: cmd.desc,
}))
.sort((a, b) => a.name.localeCompare(b.name));
}, []);
// Keyboard shortcuts
const shortcuts = useMemo<ShortcutItem[]>(() => {
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: "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) => {
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 (
<Box flexDirection="column" gap={1}>
<Box flexDirection="column">
<Text bold color={colors.selector.title}>
Letta Code v{version} ( navigate, /jk page, ESC close)
</Text>
<Box>
<Text dimColor>Tab: </Text>
{HELP_TABS.map((tab, i) => (
<Text key={tab}>
{i > 0 && <Text dimColor> · </Text>}
<Text
bold={tab === activeTab}
color={
tab === activeTab
? colors.selector.itemHighlighted
: undefined
}
>
{getTabLabel(tab)}
</Text>
</Text>
))}
<Text dimColor> (Tab to switch)</Text>
</Box>
<Text dimColor>
Page {currentPage + 1}/{totalPages}
</Text>
</Box>
<Box flexDirection="column">
{activeTab === "commands" &&
(visiblePageItems as CommandItem[]).map((command, index) => {
const isSelected = index === selectedIndex;
return (
<Box key={command.name} flexDirection="row" gap={1}>
<Text
color={
isSelected ? colors.selector.itemHighlighted : undefined
}
>
{isSelected ? "" : " "}
</Text>
<Box flexDirection="column">
<Box flexDirection="row">
<Text
bold={isSelected}
color={
isSelected ? colors.selector.itemHighlighted : undefined
}
>
{command.name}
</Text>
<Text dimColor> {command.description}</Text>
</Box>
</Box>
</Box>
);
})}
{activeTab === "shortcuts" &&
(visiblePageItems as ShortcutItem[]).map((shortcut, index) => {
const isSelected = index === selectedIndex;
return (
<Box key={shortcut.keys} flexDirection="row" gap={1}>
<Text
color={
isSelected ? colors.selector.itemHighlighted : undefined
}
>
{isSelected ? "" : " "}
</Text>
<Box flexDirection="column">
<Box flexDirection="row">
<Text
bold={isSelected}
color={
isSelected ? colors.selector.itemHighlighted : undefined
}
>
{shortcut.keys}
</Text>
<Text dimColor> {shortcut.description}</Text>
</Box>
</Box>
</Box>
);
})}
</Box>
<Box flexDirection="column" marginTop={1}>
<Text dimColor>Getting started:</Text>
<Text dimColor>
Run <Text bold>/init</Text> to initialize agent memory for this
project
</Text>
<Text dimColor>
Press <Text bold>/</Text> at any time to see command autocomplete
</Text>
<Text dimColor>
Visit <Text bold>https://docs.letta.com/letta-code</Text> for more
help
</Text>
</Box>
</Box>
);
}