import { useState, type ReactNode } from "react" import { Box, Text, useWindowSize } from "ink" import { TextInput } from "./text-input.js" type Row = { id: string; label: ReactNode; detail?: ReactNode } export type PickerProps = { items: { id: string; label: string; detail?: string }[] onSelect: (id: string) => void onCancel: () => void /** Removes the row the remove key points at; an unhandled id leaves the key to the query. */ onDelete?: (id: string) => boolean search?: boolean title?: string initialQuery?: string initialSelection?: string } /** The agents list and menus share a marker and a terminal-sized selection window. */ export function SelectableRows({ items, selected, limit }: { items: Row[]; selected: number; limit?: number }) { const { rows } = useWindowSize() const available = Math.max(1, Math.min(limit ?? rows, rows - 8)) const first = Math.min(Math.max(0, selected - available + 1), Math.max(0, items.length - available)) return {items.slice(first, first + available).map((item, offset) => ( {first + offset === selected ? "> " : " "}{item.label}{item.detail && {item.detail}} ))} {!items.length && no matches} } export function Picker({ items, onSelect, onCancel, onDelete, search = false, title, initialQuery = "", initialSelection }: PickerProps) { const [query, setQuery] = useState(initialQuery) const [selectedId, setSelectedId] = useState(initialSelection) const tokens = query.toLowerCase().trim().split(/\s+/) const matches = items.filter((item) => tokens.every((token) => `${item.id} ${item.label} ${item.detail ?? ""}`.toLowerCase().includes(token))) const selected = Math.max(0, matches.findIndex((item) => item.id === (selectedId ?? initialSelection))) return {title && {title}} {search ? "search: " : ""} { if (key.escape || (key.ctrl && input === "c")) { onCancel() return true } if (key.upArrow || key.downArrow) { setSelectedId(matches[Math.max(0, Math.min(matches.length - 1, selected + (key.upArrow ? -1 : 1)))]?.id) return true } if ((input === "d" || key.delete) && onDelete && matches[selected] && onDelete(matches[selected]!.id)) { return true } if (key.return) { const choice = matches[selected] if (choice) { onSelect(choice.id) } return true } return !search }} /> ↑↓ select · enter choose{onDelete ? " · d remove" : ""} · esc cancel }