/** * `CompletionInput` — a text field wired to MCP `completion/complete`. As the * human types, it asks the provided `fetchCompletions` (a thin wrapper over the * MCP completions capability) for argument suggestions and renders them as a * selectable list; picking one fills the field. A fetch failure clears the * suggestions and surfaces through `onError` — never a silent swallow. * * The field is a `tai-input` and its popup wears the Select popup's classes * (`tai-select-content` / `tai-select-item`), so a suggestion list and a select * list are the same object to a reader. * * Reusable by any feature; the intended consumer is a `SchemaForm` string field * that has a completion provider for its tool/prompt/resource argument. */ import type { CSSProperties, ReactNode } from 'react'; import { useEffect, useId, useRef, useState } from 'react'; import { useFieldControl } from './field'; export interface CompletionInputProps { readonly value: string; readonly onChange: (value: string) => void; /** Fetch argument completions for the current input value. */ readonly fetchCompletions: (value: string) => Promise; /** Surfaced (not swallowed) when a completion fetch fails. */ readonly onError?: (error: unknown) => void; readonly placeholder?: string; } const wrapStyle: CSSProperties = { position: 'relative' }; /** * The popup FLOATS below the field: absolutely positioned within the * `position: relative` wrapper (`top: 100%` = the field's bottom edge, stretched * `left`/`right` to the field's width) so it is taken OUT OF FLOW — it neither * displaces the content below the field nor gets laid out inside it. Its stacking * comes from `tai-select-content`'s `--tai-z-popover` z-index, which was inert * while the list was in-flow (z-index needs a positioned box) and is exactly what * this `position` now activates, so the list clears a sibling control it overlaps. * * Absolute-in-wrapper rather than a portal on purpose: this is an inline form * control (a `SchemaForm` string field). A portal would need anchor-rect popper * plumbing, and it buys nothing here — the wrapper is a plain flow box, so the * only container that could clip the list is a scrollable ancestor (e.g. the * `Dialog` body), where an absolute list simply extends that ancestor's scroll * area instead of being cut off. It is still a `