Rules Menu
Implement /command autocomplete menu for quick rule access.
Implement a /rule command menu that lets users quickly trigger predefined rules with autocomplete and keyboard navigation.
Overview
When users type /, display a list of available rules with search filtering and keyboard navigation support.
Fetch Rules
First, fetch the rules list from the Config API:
Fetch rules
const [rules, setRules] = useState<Rule[]>([]);
useEffect(() => {
fetch("/api/lens/agent/config")
.then((res) => res.json())
.then((data) => {
if (data.success) {
setRules(data.config.rules || []);
}
});
}, []);Rules Menu State
State management
const [showRulesMenu, setShowRulesMenu] = useState(false);
const [selectedIndex, setSelectedIndex] = useState(0);
const [input, setInput] = useState("");Filter Logic
Filtering rules
// Get search string after /
const slashQuery = input.startsWith("/")
? input.slice(1).toLowerCase()
: "";
// Filter rules
const filteredRules = showRulesMenu
? rules.filter(
(rule) =>
rule.name.toLowerCase().includes(slashQuery) ||
(rule.displayName || "").toLowerCase().includes(slashQuery)
)
: [];Input Change Handler
Handle input changes
const handleInputChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
const value = e.target.value;
setInput(value);
// Show menu when starting with "/" and no space
if (value.startsWith("/") && !value.includes(" ")) {
setShowRulesMenu(true);
setSelectedIndex(0);
} else {
setShowRulesMenu(false);
}
};Keyboard Navigation
Keyboard handling
const handleKeyDown = (e: React.KeyboardEvent) => {
if (showRulesMenu && filteredRules.length > 0) {
switch (e.key) {
case "ArrowDown":
e.preventDefault();
setSelectedIndex((prev) => (prev + 1) % filteredRules.length);
return;
case "ArrowUp":
e.preventDefault();
setSelectedIndex(
(prev) => (prev - 1 + filteredRules.length) % filteredRules.length
);
return;
case "Enter":
case "Tab":
e.preventDefault();
selectRule(filteredRules[selectedIndex]);
return;
case "Escape":
e.preventDefault();
setShowRulesMenu(false);
return;
}
}
// Normal Enter to send
if (e.key === "Enter" && !e.shiftKey && !e.nativeEvent.isComposing) {
e.preventDefault();
handleSend();
}
};Select Rule
Rule selection
const selectRule = (rule: Rule) => {
// Set input to "/ruleName "
setInput(`/${rule.name} `);
setShowRulesMenu(false);
inputRef.current?.focus();
};Render Menu
UI rendering
{/* Rules Menu */}
{showRulesMenu && filteredRules.length > 0 && (
<div className="absolute bottom-full left-0 right-0 mb-2 bg-white border rounded-lg shadow-lg max-h-64 overflow-auto">
{/* Keyboard hints */}
<div className="px-3 py-2 text-xs text-gray-400 border-b flex gap-4">
<span>↑↓ Select</span>
<span>Enter Confirm</span>
<span>Esc Close</span>
</div>
{/* Rules list */}
{filteredRules.map((rule, index) => (
<div
key={rule.id}
className={`px-3 py-2 cursor-pointer transition ${
index === selectedIndex
? "bg-blue-50 text-blue-700"
: "hover:bg-gray-50"
}`}
onClick={() => selectRule(rule)}
onMouseEnter={() => setSelectedIndex(index)}
>
<div className="flex items-center gap-2">
<span className="font-mono text-sm text-blue-500">
/{rule.name}
</span>
<span className="text-gray-700">
{rule.displayName || rule.name}
</span>
</div>
{rule.description && (
<p className="text-xs text-gray-500 mt-1">
{rule.description}
</p>
)}
</div>
))}
</div>
)}
{/* No results */}
{showRulesMenu && filteredRules.length === 0 && (
<div className="absolute bottom-full left-0 right-0 mb-2 bg-white border rounded-lg shadow-lg p-4 text-center text-gray-400">
No matching rules
</div>
)}Rule Type
Type definition
interface Rule {
id: string;
name: string;
displayName?: string;
description?: string;
prompt: string;
}Tip
Rules are configured in the Lens OS console and automatically synced to your application through the Config API.