Files
kandev/apps/web/components/settings/mode-combobox.tsx
T
2026-08-16 19:18:21 +00:00

101 lines
3.6 KiB
TypeScript

"use client";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { IconSelector } from "@tabler/icons-react";
import { Button } from "@kandev/ui/button";
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from "@kandev/ui/command";
import { Popover, PopoverContent, PopoverTrigger } from "@kandev/ui/popover";
import type { ModeEntry } from "@/lib/types/http";
import { settingsControlClassName } from "@/components/settings/settings-control";
type ModeComboboxProps = {
value: string;
onChange: (value: string) => void;
modes: ModeEntry[];
currentModeId: string | undefined;
};
/**
* Searchable session-mode picker. Same Popover+Command pattern as
* ModelCombobox so descriptions in the dropdown don't leak into the
* trigger text (which happens with Radix Select).
*/
export function ModeCombobox({ value, onChange, modes, currentModeId }: ModeComboboxProps) {
const { t } = useTranslation();
const [open, setOpen] = useState(false);
const selected = value || currentModeId || modes[0]?.id || "";
const activeMode = modes.find((m) => m.id === selected);
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
aria-expanded={open}
data-testid="profile-mode-select"
className={settingsControlClassName("w-full justify-between font-normal cursor-pointer")}
>
<span className="flex items-center gap-2 truncate">
{activeMode?.name ?? selected}
{activeMode?.id === currentModeId && (
<span className="text-muted-foreground">{t("agents:defaultSuffix")}</span>
)}
</span>
<IconSelector className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent
className="min-w-[--radix-popover-trigger-width] w-[min(32rem,calc(100vw-2rem))] p-0"
align="start"
onWheel={(e) => e.stopPropagation()}
>
<Command>
<CommandInput placeholder={t("agents:searchModes")} />
<CommandList
className="max-h-[min(60vh,24rem)] overflow-y-auto overscroll-contain"
onWheel={(e) => e.stopPropagation()}
>
<CommandEmpty>{t("agents:noModeFound")}</CommandEmpty>
<CommandGroup>
{modes.map((m) => (
<CommandItem
key={m.id}
value={`${m.id} ${m.name}`}
onSelect={() => {
onChange(m.id);
setOpen(false);
}}
data-checked={selected === m.id}
className="cursor-pointer"
>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 truncate">
<span className="truncate">{m.name}</span>
{m.id === currentModeId && (
<span className="text-muted-foreground text-xs">
{t("agents:defaultSuffix")}
</span>
)}
</div>
{m.description && m.description !== m.name && (
<p className="text-xs text-muted-foreground truncate">{m.description}</p>
)}
</div>
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
);
}