Stats-control round 1 (owner): bare labels + intuitive dropdown close

Model + thinking control refinements (components/model-picker.tsx):
- Drop the '·' separator between the two buttons.
- The thinking button shows just the level ('High', 'Max') — same
  doctrine as the model button showing just the model name; '—' when
  unknown. Pinned in render-stats-bar (label + no-dot assertions).
- The dropdowns now close on any pointerdown outside the control
  (the toggle buttons stay inside the container and keep their own
  toggling) and on Esc — the intuitive dropdown contract, instead of
  the button being the only way out.

Note: file is graph-stream-owned per SCOPE; touching it per the owner's
direct request (same as cd518a4) — flagged for master.
This commit is contained in:
amosblomqvist
2026-09-14 08:10:27 +03:00
parent a207e6b4a2
commit 6505f12d37
2 changed files with 33 additions and 6 deletions
+28 -4
View File
@@ -101,6 +101,7 @@ export function ModelControl({
const [query, setQuery] = useState("");
const [error, setError] = useState<string | null>(null);
const searchRef = useRef<HTMLInputElement>(null);
const containerRef = useRef<HTMLDivElement>(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 (
<div className="relative flex items-center gap-1.5" data-testid="model-control">
<div ref={containerRef} className="relative flex items-center gap-1.5" data-testid="model-control">
<button
type="button"
data-testid="stats-model"
@@ -167,9 +190,8 @@ export function ModelControl({
className="flex items-center gap-1.5 rounded-full border border-border/60 bg-card/70 px-2.5 py-1 transition-colors hover:border-border hover:bg-muted"
>
<span className="text-card-foreground">{effectiveId ? prettyModel(effectiveId) : "—"}</span>
<span aria-hidden className="text-xxs text-muted-foreground">▾</span>
<span aria-hidden className="text-xxs text-muted-foreground">▾</span>
</button>
<span aria-hidden>·</span>
<button
type="button"
data-testid="stats-thinking"
@@ -181,8 +203,10 @@ export function ModelControl({
title="Thinking level — applies next turn"
className="flex items-center gap-1.5 rounded-full border border-border/60 bg-card/70 px-2.5 py-1 transition-colors hover:border-border hover:bg-muted"
>
{/* Just the level — the button reads like the model button reads
the model name (owner, stats-control round 1). */}
<span className="text-card-foreground">
{effectiveLevel === undefined ? "Thinking —" : `Thinking: ${LEVEL_LABEL[effectiveLevel] ?? effectiveLevel}`}
{effectiveLevel === undefined ? "—" : (LEVEL_LABEL[effectiveLevel] ?? effectiveLevel)}
</span>
<span aria-hidden className="text-xxs text-muted-foreground">▾</span>
</button>
+5 -2
View File
@@ -34,9 +34,12 @@ describe("stats bar", () => {
assert.match(html, /data-testid="stats-bar"/);
assert.match(html, /data-testid="stats-context">—</);
assert.match(html, /data-testid="stats-cost">—</);
// no turns, no settings, catalog not loaded → both control labels "—"
// no turns, no settings, catalog not loaded → both control labels
// "—" (the thinking button shows just the level, like the model
// button shows just the model name — no prefix, no dot separator)
assert.match(html, /data-testid="stats-model"[^>]*>\s*<span[^>]*>—<\/span>/);
assert.match(html, /Thinking —</);
assert.match(html, /data-testid="stats-thinking"[^>]*>\s*<span[^>]*>—<\/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, /▾/);