/** * `ExtensionPicker` — a controlled multi-select over the extension catalog * (`GET /api/extensions`), authoring ONE extension combo (an ordered list of * extension names). Built on the DS `Checkbox` so features share one picker * instead of re-implementing a checklist (the same reason `SchemaForm`/`ToolPicker` * live in the SDK). It is PRESENTATIONAL: the caller fetches `available` and owns * the selected `value`; this component fetches nothing. * * The catalog is GROUPED by `kind`. The `backend` kind is the single NON-STACKABLE * one (`multiple=False`), so it is SINGLE-SELECT — checking a backend * extension replaces any other backend already in the combo; every other kind is * freely multi-select. Selection order is preserved (the combo is ordered — the * stacking order that produces the branch tool name), so a newly-checked extension * appends to the end and unchecking removes in place. * * SAFETY: an extension name AND its kind are server-supplied, so every name/kind * renders as TEXT through the DS `Checkbox`/`Badge` (React escapes it) — never an * HTML sink. Pinned by a test. * * Geometry and ink come from the design-system layout classes (`tai-stack`, * `tai-row`, `tai-muted`); the component carries no palette of its own. */ import type { Extension } from '@tai42/api-client'; import { Badge } from './badge'; import { Checkbox } from './checkbox'; import { groupByKind, kindVariant } from './extension-grouping'; import { EmptyState } from './primitives'; export interface ExtensionPickerProps { /** The extension catalog (`GET /api/extensions`) the checklist is drawn from. */ readonly available: readonly Extension[]; /** The currently-selected extension names, in combo (stacking) order. */ readonly value: readonly string[]; /** Fired with the next ordered selection whenever a box toggles. */ readonly onChange: (names: string[]) => void; readonly disabled?: boolean; readonly idPrefix?: string; } export function ExtensionPicker({ available, value, onChange, disabled, idPrefix = 'extension-picker', }: ExtensionPickerProps) { const groups = groupByKind(available); const selected = new Set(value); // An empty catalog renders the shared EmptyState HERE — inside the picker — so // every call site inherits it rather than each guarding an empty checklist (a // bare div that reads as broken). No provider has published an extension yet. if (available.length === 0) { return (