UNPKG

@earendil-works/pi-coding-agent

Version:

Coding agent CLI with read, bash, edit, write tools and session management

167 lines • 6.43 kB
import { Container, fuzzyFilter, getKeybindings, Input, SelectList, Spacer, Text, } from "@earendil-works/pi-tui"; import { getSelectListTheme, theme } from "../theme/theme.js"; const SUBMENU_SELECT_LIST_LAYOUT = { minPrimaryColumnWidth: 12, maxPrimaryColumnWidth: 32, }; /** * Single-step submenu that shows a titled select list. * With `searchable: true`, typing filters the list using fuzzy matching. */ export class SelectSubmenu extends Container { selectList; listChildIndex; allOptions; listLayout; searchInput; onSelectCb; onCancelCb; onSelectionChangeCb; constructor(title, description, options, currentValue, onSelect, onCancel, onSelectionChange, submenuOptions) { super(); this.allOptions = options; this.listLayout = submenuOptions?.layout ?? SUBMENU_SELECT_LIST_LAYOUT; this.onSelectCb = onSelect; this.onCancelCb = onCancel; this.onSelectionChangeCb = onSelectionChange; // Title this.addChild(new Text(theme.bold(theme.fg("accent", title)), 0, 0)); // Description if (description) { this.addChild(new Spacer(1)); this.addChild(new Text(theme.fg("muted", description), 0, 0)); } // Search input if (submenuOptions?.searchable) { this.addChild(new Spacer(1)); this.searchInput = new Input(); this.searchInput.onSubmit = () => { this.selectList.handleInput("\r"); }; this.addChild(this.searchInput); } // Spacer this.addChild(new Spacer(1)); // Select list this.selectList = this.buildSelectList(options, currentValue); this.listChildIndex = this.children.length; this.addChild(this.selectList); // Hint this.addChild(new Spacer(1)); const hint = submenuOptions?.searchable ? " Type to filter \u00b7 Enter to select \u00b7 Esc to go back" : " Enter to select \u00b7 Esc to go back"; this.addChild(new Text(theme.fg("dim", hint), 0, 0)); } buildSelectList(options, preselect) { const list = new SelectList(options, Math.min(options.length, 10), getSelectListTheme(), this.listLayout); const idx = options.findIndex((o) => o.value === preselect); if (idx !== -1) list.setSelectedIndex(idx); list.onSelect = (item) => this.onSelectCb(item.value); list.onCancel = this.onCancelCb; if (this.onSelectionChangeCb) { const cb = this.onSelectionChangeCb; list.onSelectionChange = (item) => cb(item.value); } return list; } applyFilter(query) { const filtered = query ? fuzzyFilter(this.allOptions, query, (item) => `${item.label} ${item.description ?? ""}`) : this.allOptions; const newList = this.buildSelectList(filtered, ""); this.children[this.listChildIndex] = newList; this.selectList = newList; } handleInput(data) { if (this.searchInput) { const kb = getKeybindings(); const isNav = kb.matches(data, "tui.select.up") || kb.matches(data, "tui.select.down") || kb.matches(data, "tui.select.pageUp") || kb.matches(data, "tui.select.pageDown") || kb.matches(data, "tui.select.confirm") || kb.matches(data, "tui.select.cancel"); if (isNav) { this.selectList.handleInput(data); } else { this.searchInput.handleInput(data); this.applyFilter(this.searchInput.getValue()); } } else { this.selectList.handleInput(data); } } } /** * Generic N-step submenu built on top of {@link SelectSubmenu}. * * Each step's options can depend on prior selections via the shared context. * Esc goes back one step; Esc at step 0 cancels. * With `loop: true`, completing the final step invokes `onComplete` then returns to step 0. */ export class SteppedSubmenu extends Container { steps; onComplete; onCancel; opts; activeComponent; context; constructor(steps, onComplete, onCancel, opts = {}) { super(); this.steps = steps; this.onComplete = onComplete; this.onCancel = onCancel; this.opts = opts; this.context = { ...(opts.initialContext ?? {}) }; this.activeComponent = this.buildStep(opts.startAtStep ?? 0); } buildStep(stepIndex) { const step = this.steps[stepIndex]; const total = this.steps.length; const stepLabel = total > 1 ? `Step ${stepIndex + 1}/${total} \u00b7 ` : ""; const title = typeof step.title === "function" ? step.title(this.context) : step.title; const desc = typeof step.description === "function" ? step.description(this.context) : step.description; const items = step.options(this.context); const preselect = step.preselect?.(this.context) ?? ""; return new SelectSubmenu(title, `${stepLabel}${desc}`, items, preselect, (value) => { this.context[step.key] = value; if (stepIndex < total - 1) { // Advance to next step this.activeComponent = this.buildStep(stepIndex + 1); } else { // Final step \u2014 deliver result this.onComplete({ ...this.context }); if (this.opts.loop) { this.context = {}; this.activeComponent = this.buildStep(0); } else { this.onCancel(); } } }, () => { if (stepIndex > 0) { delete this.context[step.key]; this.activeComponent = this.buildStep(stepIndex - 1); } else { this.onCancel(); } }, undefined, step.searchable || step.layout ? { searchable: step.searchable, layout: step.layout } : undefined); } render(width) { return this.activeComponent.render(width); } handleInput(data) { this.activeComponent.handleInput?.(data); } invalidate() { this.activeComponent.invalidate?.(); } } //# sourceMappingURL=settings-submenu.js.map