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.