diff --git a/components/model-picker.tsx b/components/model-picker.tsx index 48c0c8a..9dbba2c 100644 --- a/components/model-picker.tsx +++ b/components/model-picker.tsx @@ -101,6 +101,7 @@ export function ModelControl({ const [query, setQuery] = useState(""); const [error, setError] = useState(null); const searchRef = useRef(null); + const containerRef = useRef(null); const defaults = catalog?.defaults; const effectiveId = settings?.model?.id ?? defaults?.model.id ?? fallbackModelId; @@ -114,6 +115,28 @@ export function ModelControl({ if (open === "model") searchRef.current?.focus(); }, [open]); + // The intuitive dropdown contract (owner, stats-control round 1): any + // click outside the open menu — or an Esc keypress — closes it. Only the + // menu's own surface keeps it open (the toggle buttons live inside the + // same container and handle their own toggling in onClick). + useEffect(() => { + if (open === null) return; + const onPointerDown = (e: PointerEvent) => { + if (containerRef.current && !containerRef.current.contains(e.target as Node)) { + setOpen(null); + } + }; + const onKeyDown = (e: KeyboardEvent) => { + if (e.key === "Escape") setOpen(null); + }; + document.addEventListener("pointerdown", onPointerDown); + document.addEventListener("keydown", onKeyDown); + return () => { + document.removeEventListener("pointerdown", onPointerDown); + document.removeEventListener("keydown", onKeyDown); + }; + }, [open]); + const save = useCallback( async (next: ChatSettings) => { setError(null); @@ -153,7 +176,7 @@ export function ModelControl({ .map((r) => r.m); return ( -
+
- · diff --git a/tests/render-stats-bar.test.tsx b/tests/render-stats-bar.test.tsx index 57e184b..869f429 100644 --- a/tests/render-stats-bar.test.tsx +++ b/tests/render-stats-bar.test.tsx @@ -34,9 +34,12 @@ describe("stats bar", () => { assert.match(html, /data-testid="stats-bar"/); assert.match(html, /data-testid="stats-context">——]*>\s*]*>—<\/span>/); - assert.match(html, /Thinking —]*>\s*]*>—<\/span>/); + assert.ok(!html.includes("aria-hidden>·"), "no dot between the controls"); // the control reads as a button: pill border + dropdown chevron assert.match(html, /data-testid="stats-model"[^>]*rounded-full[^>]*>/); assert.match(html, /▾/);