"use client"; /** * The model menu (extracted 2026-09-20 from model-picker.tsx, owner ask: * the agent settings' role selects use the EXACT same dropdown as the * chat model selector — same look, same machine). The fuzzy search + * arrow-walk + Enter pick machine lives here once; each mount owns only * the popover shell (positioning, outside-click, Esc — the chat control * opens UP from the bottom bar, the settings rows open DOWN inside the * dialog). * * Keyboard machine (owner polish, 2026-09-17): focus STAYS in the search * field — ↑/↓ walk the filtered results (clamped, no wrap), Enter picks * the walked row (or the first result when nothing is walked). * stopPropagation keeps these keys out of every other listener while the * menu is open. */ import { useEffect, useRef, useState } from "react"; import type { ModelCatalogEntry } from "@/src/core"; import { fuzzyScore } from "@/src/core"; import { formatTokens } from "@/components/usage-format"; function priceLabel(cost: ModelCatalogEntry["cost"]): string { const fmt = (n: number) => `$${n < 1 ? n.toFixed(2).replace(/0$/, "") : n.toFixed(2)}`; return `${fmt(cost.input)}/${fmt(cost.output)}`; } /** The model dropdown body — fuzzy search + ranked list. `models === * undefined` is the catalog-loading state; an empty result list gets * its own quiet line. */ export function ModelMenuList({ models, effectiveId, onPick, footer, }: { /** The full catalog — undefined while it loads. */ models: ModelCatalogEntry[] | undefined; /** Currently effective id (the row gets the quiet highlight). */ effectiveId: string | undefined; /** Receives the chosen entry. */ onPick: (entry: ModelCatalogEntry) => void; /** Quiet footer note under the list (context-specific). */ footer?: string; }) { const [query, setQuery] = useState(""); const [menuIdx, setMenuIdx] = useState(null); const searchRef = useRef(null); const listRef = useRef(null); useEffect(() => { searchRef.current?.focus(); }, []); // Keep the arrow-walked result visible in the scrolling list. useEffect(() => { if (menuIdx === null) return; listRef.current?.querySelector(`[data-idx="${menuIdx}"]`)?.scrollIntoView({ block: "nearest" }); }, [menuIdx]); // The filtered, ranked model list. const results = (models ?? []) .map((m) => ({ m, s: fuzzyScore(query, m.provider, m.id, m.name) })) .filter((r) => r.s !== null) .sort((a, b) => b.s! - a.s! || a.m.id.localeCompare(b.m.id)) .map((r) => r.m); return ( <> { setQuery(e.target.value); setMenuIdx(null); // new query → the walk restarts on Enter/↓ }} onKeyDown={(e) => { if (e.key === "ArrowDown" || e.key === "ArrowUp") { e.preventDefault(); e.stopPropagation(); if (results.length === 0) return; setMenuIdx((i) => e.key === "ArrowDown" ? Math.min((i ?? -1) + 1, results.length - 1) : Math.max((i ?? results.length) - 1, 0), ); } else if (e.key === "Enter") { const chosen = results[menuIdx ?? 0]; if (chosen) { e.preventDefault(); e.stopPropagation(); onPick(chosen); } } }} placeholder="Search models…" className="lh-input mb-1.5 w-full rounded-lg border bg-background px-2 py-1.5 text-foreground outline-none focus:ring-1 focus:ring-ring" /> {!models &&
Loading models…
} {models && results.length === 0 && (
No models match.
)}
    {results.map((m, i) => (
  • ))}
{footer && (
{footer}
)} ); } /** The thinking-level dropdown body — the levels this model actually * supports, ✓ on the effective one. Same shape as the chat selector's. */ export function ThinkingMenuList({ levels, effectiveLevel, onPick, empty, footer, }: { levels: string[]; effectiveLevel: string | undefined; onPick: (level: string) => void; /** Shown instead of the list when there are no levels. */ empty?: string; footer?: string; }) { return ( <> {levels.length === 0 ? (
{empty ?? "No thinking levels."}
) : ( levels.map((level) => ( )) )} {footer && (
{footer}
)} ); }