/** * Interactive marketplace plugin selector. * * Shows available plugins from all configured marketplaces in a SelectList. * Selecting a plugin triggers installation. Esc cancels. */ import { Container, type SelectItem, SelectList } from "@oh-my-pi/pi-tui"; import { getSelectListTheme } from "../theme/theme"; import { DynamicBorder } from "./dynamic-border"; export interface PluginSelectorCallbacks { onSelect: (pluginName: string, marketplace: string, scope?: "user" | "project") => void; onCancel: () => void; } export interface PluginItem { plugin: { name: string; version?: string; description?: string }; marketplace: string; /** Scope of this entry. When set, appended to the label and forwarded to onSelect. */ scope?: "user" | "project"; } export class PluginSelectorComponent extends Container { #selectList: SelectList; constructor( marketplaceCount: number, plugins: PluginItem[], installedIds: Set, callbacks: PluginSelectorCallbacks, ) { super(); const items: SelectItem[] = plugins.map(({ plugin, marketplace, scope }) => { // Encode scope into the value so onSelect can recover it without a parallel Map. // Format: "name@marketplace" or "name@marketplace#scope" const id = scope ? `${plugin.name}@${marketplace}#${scope}` : `${plugin.name}@${marketplace}`; const installed = installedIds.has(`${plugin.name}@${marketplace}`); const version = plugin.version ? `@${plugin.version}` : ""; const status = installed ? " [installed]" : ""; const scopeTag = scope ? ` [${scope}]` : ""; return { value: id, label: `${plugin.name}${version}${status}${scopeTag}`, description: plugin.description, hint: marketplace, }; }); if (items.length === 0) { items.push({ value: "__empty__", label: "No plugins available", description: marketplaceCount === 0 ? "Add a marketplace first: /marketplace add " : "Configured marketplaces have no plugins", }); } this.addChild(new DynamicBorder()); this.#selectList = new SelectList(items, Math.min(items.length, 20), getSelectListTheme()); this.#selectList.onSelect = item => { if (item.value === "__empty__") return; const [name, marketplace, scope] = splitPluginId(item.value); if (name && marketplace) { callbacks.onSelect(name, marketplace, scope); } }; this.#selectList.onCancel = () => { callbacks.onCancel(); }; this.addChild(this.#selectList); this.addChild(new DynamicBorder()); } getSelectList(): SelectList { return this.#selectList; } } function splitPluginId(id: string): [string, string, "user" | "project" | undefined] | [null, null, null] { // value format: "name@marketplace" or "name@marketplace#scope" const hashIdx = id.indexOf("#"); const base = hashIdx >= 0 ? id.slice(0, hashIdx) : id; const scope = hashIdx >= 0 ? (id.slice(hashIdx + 1) as "user" | "project") : undefined; const atIdx = base.lastIndexOf("@"); if (atIdx <= 0) return [null, null, null]; return [base.slice(0, atIdx), base.slice(atIdx + 1), scope]; }