{"version":3,"file":"selector.d.ts","sourceRoot":"","sources":["../../../src/slash/selector.ts"],"names":[],"mappings":"AAAA,OAAO,EAAsC,KAAK,KAAK,EAAE,MAAM,2BAA2B,CAAC;AAC3F,OAAO,EAAE,SAAS,EAAsB,KAAK,kBAAkB,EAAgB,KAAK,GAAG,EAAE,MAAM,kBAAkB,CAAC;AAElH,4FAA4F;AAC5F,MAAM,WAAW,YAAY;IAC5B,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,0EAA0E;IAC1E,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,mEAAiE;IACjE,OAAO,CAAC,EAAE,OAAO,CAAC;CAClB;AAED,MAAM,WAAW,cAAc;IAC9B,SAAS,EAAE,OAAO,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,eAAe;IAC/B,KAAK,EAAE,MAAM,CAAC;IACd,0EAA0E;IAC1E,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,YAAY,EAAE,CAAC;IACtB,IAAI,EAAE,CAAC,MAAM,EAAE,cAAc,KAAK,IAAI,CAAC;CACvC;AAID;;;;;;;;GAQG;AACH,qBAAa,iBAAkB,SAAQ,SAAS;IAC/C,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAM;IAC1B,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAQ;IAC9B,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAqB;IACjD,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAiB;IACvC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAmC;IAExD,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAQ;IACpC,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAY;IAC1C,OAAO,CAAC,QAAQ,CAAiB;IACjC,OAAO,CAAC,aAAa,CAAK;IAE1B,YAAY,GAAG,EAAE,GAAG,EAAE,KAAK,EAAE,KAAK,EAAE,WAAW,EAAE,kBAAkB,EAAE,OAAO,EAAE,eAAe,EA2C5F;IAED,OAAO,CAAC,WAAW;IAWnB,OAAO,CAAC,UAAU;IA8BlB,OAAO,CAAC,gBAAgB;IAKxB,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAqBjC;CACD","sourcesContent":["import { DynamicBorder, keyHint, rawKeyHint, type Theme } from \"@lpb-work/pi-coding-agent\";\nimport { Container, fuzzyFilter, Input, type KeybindingsManager, Spacer, Text, type TUI } from \"@lpb-work/pi-tui\";\n\n/** A single selectable row. `value` is returned on confirm; `label` is the primary text. */\nexport interface SelectorItem {\n\tvalue: string;\n\tlabel: string;\n\t/** Optional dimmed badge shown after the label (e.g. a provider name). */\n\tbadge?: string;\n\t/** Marks the current selection (rendered with a ✓ checkmark). */\n\tcurrent?: boolean;\n}\n\nexport interface SelectorResult {\n\tconfirmed: boolean;\n\tvalue?: string;\n}\n\nexport interface SelectorOptions {\n\ttitle: string;\n\t/** Optional dimmed hint line under the title (e.g. the current value). */\n\tsubtitle?: string;\n\titems: SelectorItem[];\n\tdone: (result: SelectorResult) => void;\n}\n\nconst MAX_VISIBLE = 10;\n\n/**\n * A single-select list with a search field and a bounded scroll window that keeps the\n * highlighted row on screen with an `(n/total)` indicator, so the selection never scrolls\n * out of view when the option list is long. Composed from pi's own TUI primitives so it\n * matches the built-in `/model` picker.\n *\n * Colors come from the `theme` passed to the factory: the module-level `theme` singleton\n * is undefined under the extension's jiti module cache.\n */\nexport class SelectorComponent extends Container {\n\tprivate readonly tui: TUI;\n\tprivate readonly theme: Theme;\n\tprivate readonly keybindings: KeybindingsManager;\n\tprivate readonly items: SelectorItem[];\n\tprivate readonly done: (result: SelectorResult) => void;\n\n\tprivate readonly searchInput: Input;\n\tprivate readonly listContainer: Container;\n\tprivate filtered: SelectorItem[];\n\tprivate selectedIndex = 0;\n\n\tconstructor(tui: TUI, theme: Theme, keybindings: KeybindingsManager, options: SelectorOptions) {\n\t\tsuper();\n\t\tthis.tui = tui;\n\t\tthis.theme = theme;\n\t\tthis.keybindings = keybindings;\n\t\tthis.items = options.items;\n\t\tthis.done = options.done;\n\t\tthis.filtered = [...this.items];\n\n\t\tconst border = () => new DynamicBorder((str) => theme.fg(\"border\", str));\n\t\tthis.addChild(border());\n\t\tthis.addChild(new Spacer(1));\n\t\tthis.addChild(new Text(theme.fg(\"accent\", theme.bold(options.title)), 0, 0));\n\t\tif (options.subtitle) this.addChild(new Text(theme.fg(\"muted\", options.subtitle), 0, 0));\n\t\tthis.addChild(new Spacer(1));\n\n\t\tthis.searchInput = new Input();\n\t\tthis.searchInput.focused = true;\n\t\tthis.searchInput.onSubmit = () => this.confirmSelection();\n\t\tthis.addChild(this.searchInput);\n\t\tthis.addChild(new Spacer(1));\n\n\t\tthis.listContainer = new Container();\n\t\tthis.addChild(this.listContainer);\n\t\tthis.addChild(new Spacer(1));\n\n\t\tthis.addChild(\n\t\t\tnew Text(\n\t\t\t\trawKeyHint(\"↑↓\", \"navigate\") +\n\t\t\t\t\t\"  \" +\n\t\t\t\t\tkeyHint(\"tui.select.confirm\", \"select\") +\n\t\t\t\t\t\"  \" +\n\t\t\t\t\tkeyHint(\"tui.select.cancel\", \"cancel\"),\n\t\t\t\t0,\n\t\t\t\t0,\n\t\t\t),\n\t\t);\n\t\tthis.addChild(new Spacer(1));\n\t\tthis.addChild(border());\n\n\t\tconst currentIndex = this.filtered.findIndex((item) => item.current);\n\t\tif (currentIndex >= 0) this.selectedIndex = currentIndex;\n\t\tthis.updateList();\n\t}\n\n\tprivate applyFilter(query: string): void {\n\t\tconst previous = this.filtered[this.selectedIndex]?.value;\n\t\tthis.filtered = query\n\t\t\t? fuzzyFilter(this.items, query, (item) => `${item.label} ${item.value} ${item.badge ?? \"\"}`)\n\t\t\t: [...this.items];\n\t\tconst keepIndex = this.filtered.findIndex((item) => item.value === previous);\n\t\tthis.selectedIndex =\n\t\t\tkeepIndex >= 0 ? keepIndex : Math.min(this.selectedIndex, Math.max(0, this.filtered.length - 1));\n\t\tthis.updateList();\n\t}\n\n\tprivate updateList(): void {\n\t\tconst th = this.theme;\n\t\tthis.listContainer.clear();\n\t\tconst startIndex = Math.max(\n\t\t\t0,\n\t\t\tMath.min(this.selectedIndex - Math.floor(MAX_VISIBLE / 2), this.filtered.length - MAX_VISIBLE),\n\t\t);\n\t\tconst endIndex = Math.min(startIndex + MAX_VISIBLE, this.filtered.length);\n\n\t\tfor (let i = startIndex; i < endIndex; i++) {\n\t\t\tconst item = this.filtered[i]!;\n\t\t\tconst isSelected = i === this.selectedIndex;\n\t\t\tconst badge = item.badge ? ` ${th.fg(\"muted\", `[${item.badge}]`)}` : \"\";\n\t\t\tconst checkmark = item.current ? th.fg(\"success\", \" ✓\") : \"\";\n\t\t\tconst line = isSelected\n\t\t\t\t? `${th.fg(\"accent\", \"→ \")}${th.fg(\"accent\", item.label)}${badge}${checkmark}`\n\t\t\t\t: `  ${item.label}${badge}${checkmark}`;\n\t\t\tthis.listContainer.addChild(new Text(line, 0, 0));\n\t\t}\n\n\t\tif (startIndex > 0 || endIndex < this.filtered.length) {\n\t\t\tthis.listContainer.addChild(\n\t\t\t\tnew Text(th.fg(\"muted\", `  (${this.selectedIndex + 1}/${this.filtered.length})`), 0, 0),\n\t\t\t);\n\t\t}\n\t\tif (this.filtered.length === 0) {\n\t\t\tthis.listContainer.addChild(new Text(th.fg(\"muted\", \"  No matching entries\"), 0, 0));\n\t\t}\n\t}\n\n\tprivate confirmSelection(): void {\n\t\tconst selected = this.filtered[this.selectedIndex];\n\t\tthis.done(selected ? { confirmed: true, value: selected.value } : { confirmed: false });\n\t}\n\n\thandleInput(keyData: string): void {\n\t\tconst kb = this.keybindings;\n\t\tif (kb.matches(keyData, \"tui.select.up\")) {\n\t\t\tif (this.filtered.length === 0) return;\n\t\t\tthis.selectedIndex = this.selectedIndex === 0 ? this.filtered.length - 1 : this.selectedIndex - 1;\n\t\t\tthis.updateList();\n\t\t\tthis.tui.requestRender();\n\t\t} else if (kb.matches(keyData, \"tui.select.down\")) {\n\t\t\tif (this.filtered.length === 0) return;\n\t\t\tthis.selectedIndex = this.selectedIndex === this.filtered.length - 1 ? 0 : this.selectedIndex + 1;\n\t\t\tthis.updateList();\n\t\t\tthis.tui.requestRender();\n\t\t} else if (kb.matches(keyData, \"tui.select.confirm\")) {\n\t\t\tthis.confirmSelection();\n\t\t} else if (kb.matches(keyData, \"tui.select.cancel\")) {\n\t\t\tthis.done({ confirmed: false });\n\t\t} else {\n\t\t\tthis.searchInput.handleInput(keyData);\n\t\t\tthis.applyFilter(this.searchInput.getValue());\n\t\t\tthis.tui.requestRender();\n\t\t}\n\t}\n}\n"]}