/** * The pieces of `ToolPicker`: the tool `Select` itself (grouped or flat), the tag * filter field, and the selected tool's capability badges. The picker owns the * state and composes these; the option-label and grouping helpers live here too. */ import type { ReactNode } from 'react'; import { toolDisplayLabel } from '../hooks/useToolDisplayNames'; import { Badge } from './badge'; import { Field } from './field'; import { Select, type SelectGroup } from './select'; import { Tooltip } from './tooltip'; /** The shared informational note every badge surface carries: declared, never enforced. */ export const BADGES_NOTE = 'Declared capability labels — informational only. They describe what the tool touches; the server never enforces them.'; const NO_DISPLAY_NAMES: Readonly> = {}; const UNTAGGED = 'Untagged'; // Radix `Select.Item` forbids an empty-string value, so the "no filter" choice // uses a sentinel rather than ''. A tool literally tagged this string would just // never be filtered out — harmless for a UI convenience whose authority is the server. export const ALL_TAGS = '__all_tags__'; /** The group a tool sorts into: its alphabetically-first tag, else "Untagged". */ function groupFor(name: string, tagsByTool: Readonly>): string { const tags = [...(tagsByTool[name] ?? [])].sort((a, b) => a.localeCompare(b)); return tags[0] ?? UNTAGGED; } /** * The option label: `Display Name (raw)` when a distinct non-empty display name maps * the name, else the bare name; either way suffixed " (agent)" for an agent run tool. */ function optionLabel( name: string, displayNames: Readonly> | undefined, agentToolNames: ReadonlySet | undefined, ): string { const base = toolDisplayLabel(displayNames ?? NO_DISPLAY_NAMES, name); return agentToolNames?.has(name) === true ? `${base} (agent)` : base; } /** Build the grouped option clusters (sorted group labels, "Untagged" last). */ function buildGroups( names: readonly string[], tagsByTool: Readonly>, displayNames: Readonly> | undefined, agentToolNames: ReadonlySet | undefined, ): SelectGroup[] { const byGroup = new Map(); for (const name of names) { const group = groupFor(name, tagsByTool); const bucket = byGroup.get(group) ?? []; bucket.push(name); byGroup.set(group, bucket); } return [...byGroup.entries()] .sort(([a], [b]) => { if (a === UNTAGGED) return 1; if (b === UNTAGGED) return -1; return a.localeCompare(b); }) .map(([label, groupNames]) => ({ label, options: groupNames.map((name) => ({ value: name, label: optionLabel(name, displayNames, agentToolNames), })), })); } interface ToolSelectProps { readonly tagsByTool: Readonly> | undefined; readonly filtered: readonly string[]; readonly value: string | null; readonly onChange: (toolName: string) => void; readonly placeholder: string; readonly ariaLabel: string | undefined; readonly disabled: boolean | undefined; readonly displayNames: Readonly> | undefined; readonly agentToolNames: ReadonlySet | undefined; } /** The tool select: grouped by tag when `tagsByTool` is present, else a flat list. * Either branch is a `Select`, which claims the enclosing `Field`'s control id. */ export function ToolSelect({ tagsByTool, filtered, value, onChange, placeholder, ariaLabel, disabled, displayNames, agentToolNames, }: ToolSelectProps): ReactNode { if (tagsByTool !== undefined) { return ( ({ value: name, label: optionLabel(name, displayNames, agentToolNames), }))} value={value ?? ''} onValueChange={onChange} placeholder={placeholder} aria-label={ariaLabel} disabled={disabled} /> ); } /** The tag filter: an "All tags" sentinel plus one option per distinct tag. */ export function TagFilterField({ allTags, tagFilter, onTagFilterChange, disabled, }: { readonly allTags: readonly string[]; readonly tagFilter: string; readonly onTagFilterChange: (value: string) => void; readonly disabled: boolean | undefined; }): ReactNode { return (