@agimon-ai/doompi 0.0.1-alpha.11 → 0.0.1-alpha.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/dist/adapters/extensionCompiler.cjs +2 -2
  2. package/dist/adapters/extensionCompiler.cjs.map +1 -1
  3. package/dist/adapters/extensionCompiler.mjs +2 -2
  4. package/dist/adapters/extensionCompiler.mjs.map +1 -1
  5. package/dist/entries/autoStop.cjs +1 -1
  6. package/dist/entries/autoStop.mjs +1 -1
  7. package/dist/entries/notifications.cjs +1 -1
  8. package/dist/entries/notifications.mjs +1 -1
  9. package/dist/extensions/entries/autoStop.cjs +1 -1
  10. package/dist/extensions/entries/autoStop.cjs.map +1 -1
  11. package/dist/extensions/entries/autoStop.mjs +1 -1
  12. package/dist/extensions/entries/autoStop.mjs.map +1 -1
  13. package/dist/extensions/entries/components/matrixPicker.cjs +1 -1
  14. package/dist/extensions/entries/components/matrixPicker.cjs.map +1 -1
  15. package/dist/extensions/entries/components/matrixPicker.d.cts.map +1 -1
  16. package/dist/extensions/entries/components/matrixPicker.d.mts.map +1 -1
  17. package/dist/extensions/entries/components/matrixPicker.mjs +1 -1
  18. package/dist/extensions/entries/components/matrixPicker.mjs.map +1 -1
  19. package/dist/extensions/entries/components/skillsOverlay.cjs +1 -1
  20. package/dist/extensions/entries/components/skillsOverlay.cjs.map +1 -1
  21. package/dist/extensions/entries/components/skillsOverlay.d.cts.map +1 -1
  22. package/dist/extensions/entries/components/skillsOverlay.d.mts.map +1 -1
  23. package/dist/extensions/entries/components/skillsOverlay.mjs +2 -2
  24. package/dist/extensions/entries/components/skillsOverlay.mjs.map +1 -1
  25. package/dist/extensions/entries/domains.cjs +1 -1
  26. package/dist/extensions/entries/domains.cjs.map +1 -1
  27. package/dist/extensions/entries/domains.mjs +1 -1
  28. package/dist/extensions/entries/domains.mjs.map +1 -1
  29. package/dist/extensions/entries/notifications.cjs +1 -1
  30. package/dist/extensions/entries/notifications.cjs.map +1 -1
  31. package/dist/extensions/entries/notifications.mjs +1 -1
  32. package/dist/extensions/entries/notifications.mjs.map +1 -1
  33. package/package.json +18 -18
@@ -1 +1 @@
1
- {"version":3,"file":"matrixPicker.cjs","names":["Container","Input","SelectList","DynamicBorder","Spacer","Text"],"sources":["../../../../src/extensions/entries/components/matrixPicker.ts"],"sourcesContent":["import { DynamicBorder, getSelectListTheme, type Theme } from '@earendil-works/pi-coding-agent';\nimport {\n Container,\n Input,\n type KeybindingsManager,\n type SelectItem,\n SelectList,\n Spacer,\n Text,\n} from '@earendil-works/pi-tui';\n\nconst MAX_VISIBLE = 10;\nconst SINGLE_HINT = '↑↓ navigate enter select esc cancel type to filter';\nconst MULTI_HINT = '↑↓ navigate space toggle enter confirm esc cancel type to filter';\nconst SPACE = ' ';\nconst MARK_ON = '[x]';\nconst MARK_OFF = '[ ]';\n\nexport interface MatrixPickerOptions {\n title: string;\n /** Rows with plain labels. Selection markers are added by the component. */\n items: SelectItem[];\n /** Values selected on open. In single mode the first one is the cursor row. */\n selected: string[];\n multi: boolean;\n}\n\n/**\n * Rows carrying their current selection marker.\n *\n * Multi-select only. A single-select list is a button group: the cursor is the\n * selection, so adding checkboxes would show two competing indicators.\n */\nexport function markItems(items: SelectItem[], selected: string[], multi: boolean): SelectItem[] {\n if (!multi) return items;\n return items.map((item) => ({\n ...item,\n label: `${selected.includes(item.value) ? MARK_ON : MARK_OFF} ${item.label}`,\n }));\n}\n\n/** SelectList filters on a value prefix, so the visible rows follow that rule. */\nexport function visibleValues(items: SelectItem[], filter: string): string[] {\n return items.filter((item) => item.value.toLowerCase().startsWith(filter.toLowerCase())).map((item) => item.value);\n}\n\nexport function toggleValue(selected: string[], value: string): string[] {\n return selected.includes(value) ? selected.filter((entry) => entry !== value) : [...selected, value];\n}\n\n/**\n * Filterable picker with preselected rows and optional multi-select.\n *\n * Pi's built-in ui.select() offers none of that: it takes a bare string[], has\n * no filter input, hardcodes selectedIndex = 0 (extension-selector.js:12), and\n * resolves on the first enter. This drives SelectList directly instead.\n */\nexport class MatrixPickerComponent extends Container {\n private readonly filterInput = new Input();\n private readonly items: SelectItem[];\n private readonly multi: boolean;\n private readonly title: string;\n private readonly theme: Theme;\n // Declared and assigned separately, never as parameter properties: Pi loads\n // extensions through Node's strip-only type stripping, which rejects those.\n private readonly keybindings: KeybindingsManager;\n private readonly done: (value: string[] | undefined) => void;\n private list: SelectList;\n private selected: string[];\n private cursorValue: string | undefined;\n private settled = false;\n\n constructor(\n options: MatrixPickerOptions,\n theme: Theme,\n keybindings: KeybindingsManager,\n done: (value: string[] | undefined) => void,\n ) {\n super();\n this.items = options.items;\n this.multi = options.multi;\n this.title = options.title;\n this.theme = theme;\n this.keybindings = keybindings;\n this.done = done;\n this.selected = [...options.selected];\n this.cursorValue = options.selected[0];\n\n // Focus is set here rather than by the TUI, which focuses the component it\n // was handed (this container) and not the input nested inside it.\n this.filterInput.focused = true;\n this.filterInput.onEscape = () => this.settle(undefined);\n\n this.list = this.buildList();\n this.render_();\n }\n\n private buildList(): SelectList {\n const list = new SelectList(markItems(this.items, this.selected, this.multi), MAX_VISIBLE, getSelectListTheme());\n const filter = this.filterInput.getValue();\n if (filter) list.setFilter(filter);\n\n const visible = visibleValues(this.items, filter);\n const index = this.cursorValue ? visible.indexOf(this.cursorValue) : -1;\n if (index >= 0) list.setSelectedIndex(index);\n\n list.onSelectionChange = (item) => {\n this.cursorValue = item.value;\n };\n list.onSelect = (item) => (this.multi ? this.settle(this.selected) : this.settle([item.value]));\n list.onCancel = () => this.settle(undefined);\n return list;\n }\n\n /**\n * SelectList takes its rows at construction and exposes no setter, so a\n * toggled marker means rebuilding the list and the container around it.\n */\n private render_(): void {\n this.clear();\n this.addChild(new DynamicBorder());\n this.addChild(new Spacer(1));\n this.addChild(new Text(this.theme.fg('accent', this.theme.bold(this.title)), 1, 0));\n this.addChild(new Spacer(1));\n this.addChild(this.filterInput);\n this.addChild(this.list);\n this.addChild(new Spacer(1));\n this.addChild(new Text(this.theme.fg('muted', this.multi ? MULTI_HINT : SINGLE_HINT), 1, 0));\n this.addChild(new Spacer(1));\n this.addChild(new DynamicBorder());\n }\n\n private rebuild(): void {\n this.list = this.buildList();\n this.render_();\n }\n\n private settle(value: string[] | undefined): void {\n // The list can fire onSelect after a cancel has already resolved, and\n // resolving twice would drop the first answer on the floor.\n if (this.settled) return;\n this.settled = true;\n this.done(value);\n }\n\n handleInput(data: string): void {\n if (this.multi && data === SPACE) {\n const current = this.list.getSelectedItem();\n if (current) {\n this.selected = toggleValue(this.selected, current.value);\n this.cursorValue = current.value;\n this.rebuild();\n }\n return;\n }\n\n // Navigation keys belong to the list, everything else edits the filter, so\n // typing narrows the list without stealing the arrow keys from it.\n const navigational =\n this.keybindings.matches(data, 'tui.select.up') ||\n this.keybindings.matches(data, 'tui.select.down') ||\n this.keybindings.matches(data, 'tui.select.confirm') ||\n this.keybindings.matches(data, 'tui.select.cancel');\n\n if (navigational) {\n this.list.handleInput(data);\n return;\n }\n\n const before = this.filterInput.getValue();\n this.filterInput.handleInput(data);\n const after = this.filterInput.getValue();\n if (after !== before) this.list.setFilter(after);\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"matrixPicker.cjs","names":["Container","Input","SelectList","Spacer","Text"],"sources":["../../../../src/extensions/entries/components/matrixPicker.ts"],"sourcesContent":["import type { Theme } from '@earendil-works/pi-coding-agent';\nimport {\n type Component,\n Container,\n Input,\n type KeybindingsManager,\n type SelectItem,\n SelectList,\n type SelectListTheme,\n Spacer,\n Text,\n} from '@earendil-works/pi-tui';\n\nconst MAX_VISIBLE = 10;\nconst SINGLE_HINT = '↑↓ navigate enter select esc cancel type to filter';\nconst MULTI_HINT = '↑↓ navigate space toggle enter confirm esc cancel type to filter';\nconst SPACE = ' ';\nconst MARK_ON = '[x]';\nconst MARK_OFF = '[ ]';\nconst HORIZONTAL_BORDER = '─';\n\nfunction doomSelectListTheme(theme: Theme): SelectListTheme {\n return {\n selectedPrefix: (text) => theme.fg('accent', text),\n selectedText: (text) => theme.bg('selectedBg', theme.fg('text', text)),\n description: (text) => theme.fg('dim', text),\n scrollInfo: (text) => theme.fg('muted', text),\n noMatch: (text) => theme.fg('warning', text),\n };\n}\n\nclass DoomPickerBorder implements Component {\n constructor(private readonly theme: Theme) {}\n\n render(width: number): string[] {\n return width > 0 ? [this.theme.fg('borderAccent', HORIZONTAL_BORDER.repeat(width))] : [];\n }\n\n invalidate(): void {\n // The host rebuilds the picker when the theme changes.\n }\n}\n\nexport interface MatrixPickerOptions {\n title: string;\n /** Rows with plain labels. Selection markers are added by the component. */\n items: SelectItem[];\n /** Values selected on open. In single mode the first one is the cursor row. */\n selected: string[];\n multi: boolean;\n}\n\n/**\n * Rows carrying their current selection marker.\n *\n * Multi-select only. A single-select list is a button group: the cursor is the\n * selection, so adding checkboxes would show two competing indicators.\n */\nexport function markItems(items: SelectItem[], selected: string[], multi: boolean): SelectItem[] {\n if (!multi) return items;\n return items.map((item) => ({\n ...item,\n label: `${selected.includes(item.value) ? MARK_ON : MARK_OFF} ${item.label}`,\n }));\n}\n\n/** SelectList filters on a value prefix, so the visible rows follow that rule. */\nexport function visibleValues(items: SelectItem[], filter: string): string[] {\n return items.filter((item) => item.value.toLowerCase().startsWith(filter.toLowerCase())).map((item) => item.value);\n}\n\nexport function toggleValue(selected: string[], value: string): string[] {\n return selected.includes(value) ? selected.filter((entry) => entry !== value) : [...selected, value];\n}\n\n/**\n * Filterable picker with preselected rows and optional multi-select.\n *\n * Pi's built-in ui.select() offers none of that: it takes a bare string[], has\n * no filter input, hardcodes selectedIndex = 0 (extension-selector.js:12), and\n * resolves on the first enter. This drives SelectList directly instead.\n */\nexport class MatrixPickerComponent extends Container {\n private readonly filterInput = new Input();\n private readonly items: SelectItem[];\n private readonly multi: boolean;\n private readonly title: string;\n private readonly theme: Theme;\n // Declared and assigned separately, never as parameter properties: Pi loads\n // extensions through Node's strip-only type stripping, which rejects those.\n private readonly keybindings: KeybindingsManager;\n private readonly done: (value: string[] | undefined) => void;\n private list: SelectList;\n private selected: string[];\n private cursorValue: string | undefined;\n private settled = false;\n\n constructor(\n options: MatrixPickerOptions,\n theme: Theme,\n keybindings: KeybindingsManager,\n done: (value: string[] | undefined) => void,\n ) {\n super();\n this.items = options.items;\n this.multi = options.multi;\n this.title = options.title;\n this.theme = theme;\n this.keybindings = keybindings;\n this.done = done;\n this.selected = [...options.selected];\n this.cursorValue = options.selected[0];\n\n // Focus is set here rather than by the TUI, which focuses the component it\n // was handed (this container) and not the input nested inside it.\n this.filterInput.focused = true;\n this.filterInput.onEscape = () => this.settle(undefined);\n\n this.list = this.buildList();\n this.render_();\n }\n\n private buildList(): SelectList {\n const list = new SelectList(\n markItems(this.items, this.selected, this.multi),\n MAX_VISIBLE,\n doomSelectListTheme(this.theme),\n );\n const filter = this.filterInput.getValue();\n if (filter) list.setFilter(filter);\n\n const visible = visibleValues(this.items, filter);\n const index = this.cursorValue ? visible.indexOf(this.cursorValue) : -1;\n if (index >= 0) list.setSelectedIndex(index);\n\n list.onSelectionChange = (item) => {\n this.cursorValue = item.value;\n };\n list.onSelect = (item) => (this.multi ? this.settle(this.selected) : this.settle([item.value]));\n list.onCancel = () => this.settle(undefined);\n return list;\n }\n\n /**\n * SelectList takes its rows at construction and exposes no setter, so a\n * toggled marker means rebuilding the list and the container around it.\n */\n private render_(): void {\n this.clear();\n this.addChild(new DoomPickerBorder(this.theme));\n this.addChild(new Spacer(1));\n this.addChild(new Text(this.theme.fg('mdHeading', this.theme.bold(this.title)), 1, 0));\n this.addChild(new Spacer(1));\n this.addChild(this.filterInput);\n this.addChild(this.list);\n this.addChild(new Spacer(1));\n this.addChild(new Text(this.theme.fg('muted', this.multi ? MULTI_HINT : SINGLE_HINT), 1, 0));\n this.addChild(new Spacer(1));\n this.addChild(new DoomPickerBorder(this.theme));\n }\n\n private rebuild(): void {\n this.list = this.buildList();\n this.render_();\n }\n\n private settle(value: string[] | undefined): void {\n // The list can fire onSelect after a cancel has already resolved, and\n // resolving twice would drop the first answer on the floor.\n if (this.settled) return;\n this.settled = true;\n this.done(value);\n }\n\n handleInput(data: string): void {\n if (this.multi && data === SPACE) {\n const current = this.list.getSelectedItem();\n if (current) {\n this.selected = toggleValue(this.selected, current.value);\n this.cursorValue = current.value;\n this.rebuild();\n }\n return;\n }\n\n // Navigation keys belong to the list, everything else edits the filter, so\n // typing narrows the list without stealing the arrow keys from it.\n const navigational =\n this.keybindings.matches(data, 'tui.select.up') ||\n this.keybindings.matches(data, 'tui.select.down') ||\n this.keybindings.matches(data, 'tui.select.confirm') ||\n this.keybindings.matches(data, 'tui.select.cancel');\n\n if (navigational) {\n this.list.handleInput(data);\n return;\n }\n\n const before = this.filterInput.getValue();\n this.filterInput.handleInput(data);\n const after = this.filterInput.getValue();\n if (after !== before) this.list.setFilter(after);\n }\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"matrixPicker.d.cts","names":[],"sources":["../../../../src/extensions/entries/components/matrixPicker.ts"],"mappings":";;;;UAkBiB,mBAAA;EACf,KAAA;EADe;EAGf,KAAA,EAAO,UAAA;;EAEP,QAAA;EACA,KAAA;AAAA;;;;;;AASF;iBAAgB,SAAA,CAAU,KAAA,EAAO,UAAA,IAAc,QAAA,YAAoB,KAAA,YAAiB,UAAA;;iBASpE,aAAA,CAAc,KAAA,EAAO,UAAA,IAAc,MAAA;AAAA,iBAInC,WAAA,CAAY,QAAA,YAAoB,KAAA;;;;;;;AAJhD;cAea,qBAAA,SAA8B,SAAA;EAAA,iBACxB,WAAA;EAAA,iBACA,KAAA;EAAA,iBACA,KAAA;EAAA,iBACA,KAAA;EAAA,iBACA,KAAA;EAAA,iBAGA,WAAA;EAAA,iBACA,IAAA;EAAA,QACT,IAAA;EAAA,QACA,QAAA;EAAA,QACA,WAAA;EAAA,QACA,OAAA;cAGN,OAAA,EAAS,mBAAA,EACT,KAAA,EAAO,KAAA,EACP,WAAA,EAAa,kBAAA,EACb,IAAA,GAAO,KAAA;EAAA,QAqBD,SAAA;EAxCyB;;;;EAAA,QA6DzB,OAAA;EAAA,QAcA,OAAA;EAAA,QAKA,MAAA;EAQR,WAAA,CAAY,IAAA;AAAA"}
1
+ {"version":3,"file":"matrixPicker.d.cts","names":[],"sources":["../../../../src/extensions/entries/components/matrixPicker.ts"],"mappings":";;;;UA2CiB,mBAAA;EACf,KAAA;EADe;EAGf,KAAA,EAAO,UAAA;;EAEP,QAAA;EACA,KAAA;AAAA;;;;;;AASF;iBAAgB,SAAA,CAAU,KAAA,EAAO,UAAA,IAAc,QAAA,YAAoB,KAAA,YAAiB,UAAA;;iBASpE,aAAA,CAAc,KAAA,EAAO,UAAA,IAAc,MAAA;AAAA,iBAInC,WAAA,CAAY,QAAA,YAAoB,KAAA;;;;;;;AAJhD;cAea,qBAAA,SAA8B,SAAA;EAAA,iBACxB,WAAA;EAAA,iBACA,KAAA;EAAA,iBACA,KAAA;EAAA,iBACA,KAAA;EAAA,iBACA,KAAA;EAAA,iBAGA,WAAA;EAAA,iBACA,IAAA;EAAA,QACT,IAAA;EAAA,QACA,QAAA;EAAA,QACA,WAAA;EAAA,QACA,OAAA;cAGN,OAAA,EAAS,mBAAA,EACT,KAAA,EAAO,KAAA,EACP,WAAA,EAAa,kBAAA,EACb,IAAA,GAAO,KAAA;EAAA,QAqBD,SAAA;EAxCyB;;;;EAAA,QAiEzB,OAAA;EAAA,QAcA,OAAA;EAAA,QAKA,MAAA;EAQR,WAAA,CAAY,IAAA;AAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"matrixPicker.d.mts","names":[],"sources":["../../../../src/extensions/entries/components/matrixPicker.ts"],"mappings":";;;;UAkBiB,mBAAA;EACf,KAAA;EADe;EAGf,KAAA,EAAO,UAAA;;EAEP,QAAA;EACA,KAAA;AAAA;;;;;;AASF;iBAAgB,SAAA,CAAU,KAAA,EAAO,UAAA,IAAc,QAAA,YAAoB,KAAA,YAAiB,UAAA;;iBASpE,aAAA,CAAc,KAAA,EAAO,UAAA,IAAc,MAAA;AAAA,iBAInC,WAAA,CAAY,QAAA,YAAoB,KAAA;;;;;;;AAJhD;cAea,qBAAA,SAA8B,SAAA;EAAA,iBACxB,WAAA;EAAA,iBACA,KAAA;EAAA,iBACA,KAAA;EAAA,iBACA,KAAA;EAAA,iBACA,KAAA;EAAA,iBAGA,WAAA;EAAA,iBACA,IAAA;EAAA,QACT,IAAA;EAAA,QACA,QAAA;EAAA,QACA,WAAA;EAAA,QACA,OAAA;cAGN,OAAA,EAAS,mBAAA,EACT,KAAA,EAAO,KAAA,EACP,WAAA,EAAa,kBAAA,EACb,IAAA,GAAO,KAAA;EAAA,QAqBD,SAAA;EAxCyB;;;;EAAA,QA6DzB,OAAA;EAAA,QAcA,OAAA;EAAA,QAKA,MAAA;EAQR,WAAA,CAAY,IAAA;AAAA"}
1
+ {"version":3,"file":"matrixPicker.d.mts","names":[],"sources":["../../../../src/extensions/entries/components/matrixPicker.ts"],"mappings":";;;;UA2CiB,mBAAA;EACf,KAAA;EADe;EAGf,KAAA,EAAO,UAAA;;EAEP,QAAA;EACA,KAAA;AAAA;;;;;;AASF;iBAAgB,SAAA,CAAU,KAAA,EAAO,UAAA,IAAc,QAAA,YAAoB,KAAA,YAAiB,UAAA;;iBASpE,aAAA,CAAc,KAAA,EAAO,UAAA,IAAc,MAAA;AAAA,iBAInC,WAAA,CAAY,QAAA,YAAoB,KAAA;;;;;;;AAJhD;cAea,qBAAA,SAA8B,SAAA;EAAA,iBACxB,WAAA;EAAA,iBACA,KAAA;EAAA,iBACA,KAAA;EAAA,iBACA,KAAA;EAAA,iBACA,KAAA;EAAA,iBAGA,WAAA;EAAA,iBACA,IAAA;EAAA,QACT,IAAA;EAAA,QACA,QAAA;EAAA,QACA,WAAA;EAAA,QACA,OAAA;cAGN,OAAA,EAAS,mBAAA,EACT,KAAA,EAAO,KAAA,EACP,WAAA,EAAa,kBAAA,EACb,IAAA,GAAO,KAAA;EAAA,QAqBD,SAAA;EAxCyB;;;;EAAA,QAiEzB,OAAA;EAAA,QAcA,OAAA;EAAA,QAKA,MAAA;EAQR,WAAA,CAAY,IAAA;AAAA"}
@@ -1,2 +1,2 @@
1
- import{DynamicBorder as e,getSelectListTheme as t}from"@earendil-works/pi-coding-agent";import{Container as n,Input as r,SelectList as i,Spacer as a,Text as o}from"@earendil-works/pi-tui";function s(e,t,n){return n?e.map(e=>({...e,label:`${t.includes(e.value)?`[x]`:`[ ]`} ${e.label}`})):e}function c(e,t){return e.filter(e=>e.value.toLowerCase().startsWith(t.toLowerCase())).map(e=>e.value)}function l(e,t){return e.includes(t)?e.filter(e=>e!==t):[...e,t]}var u=class extends n{filterInput=new r;items;multi;title;theme;keybindings;done;list;selected;cursorValue;settled=!1;constructor(e,t,n,r){super(),this.items=e.items,this.multi=e.multi,this.title=e.title,this.theme=t,this.keybindings=n,this.done=r,this.selected=[...e.selected],this.cursorValue=e.selected[0],this.filterInput.focused=!0,this.filterInput.onEscape=()=>this.settle(void 0),this.list=this.buildList(),this.render_()}buildList(){let e=new i(s(this.items,this.selected,this.multi),10,t()),n=this.filterInput.getValue();n&&e.setFilter(n);let r=c(this.items,n),a=this.cursorValue?r.indexOf(this.cursorValue):-1;return a>=0&&e.setSelectedIndex(a),e.onSelectionChange=e=>{this.cursorValue=e.value},e.onSelect=e=>this.multi?this.settle(this.selected):this.settle([e.value]),e.onCancel=()=>this.settle(void 0),e}render_(){this.clear(),this.addChild(new e),this.addChild(new a(1)),this.addChild(new o(this.theme.fg(`accent`,this.theme.bold(this.title)),1,0)),this.addChild(new a(1)),this.addChild(this.filterInput),this.addChild(this.list),this.addChild(new a(1)),this.addChild(new o(this.theme.fg(`muted`,this.multi?`↑↓ navigate space toggle enter confirm esc cancel type to filter`:`↑↓ navigate enter select esc cancel type to filter`),1,0)),this.addChild(new a(1)),this.addChild(new e)}rebuild(){this.list=this.buildList(),this.render_()}settle(e){this.settled||(this.settled=!0,this.done(e))}handleInput(e){if(this.multi&&e===` `){let e=this.list.getSelectedItem();e&&(this.selected=l(this.selected,e.value),this.cursorValue=e.value,this.rebuild());return}if(this.keybindings.matches(e,`tui.select.up`)||this.keybindings.matches(e,`tui.select.down`)||this.keybindings.matches(e,`tui.select.confirm`)||this.keybindings.matches(e,`tui.select.cancel`)){this.list.handleInput(e);return}let t=this.filterInput.getValue();this.filterInput.handleInput(e);let n=this.filterInput.getValue();n!==t&&this.list.setFilter(n)}};export{u as MatrixPickerComponent,s as markItems,l as toggleValue,c as visibleValues};
1
+ import{Container as e,Input as t,SelectList as n,Spacer as r,Text as i}from"@earendil-works/pi-tui";function a(e){return{selectedPrefix:t=>e.fg(`accent`,t),selectedText:t=>e.bg(`selectedBg`,e.fg(`text`,t)),description:t=>e.fg(`dim`,t),scrollInfo:t=>e.fg(`muted`,t),noMatch:t=>e.fg(`warning`,t)}}var o=class{constructor(e){this.theme=e}render(e){return e>0?[this.theme.fg(`borderAccent`,`─`.repeat(e))]:[]}invalidate(){}};function s(e,t,n){return n?e.map(e=>({...e,label:`${t.includes(e.value)?`[x]`:`[ ]`} ${e.label}`})):e}function c(e,t){return e.filter(e=>e.value.toLowerCase().startsWith(t.toLowerCase())).map(e=>e.value)}function l(e,t){return e.includes(t)?e.filter(e=>e!==t):[...e,t]}var u=class extends e{filterInput=new t;items;multi;title;theme;keybindings;done;list;selected;cursorValue;settled=!1;constructor(e,t,n,r){super(),this.items=e.items,this.multi=e.multi,this.title=e.title,this.theme=t,this.keybindings=n,this.done=r,this.selected=[...e.selected],this.cursorValue=e.selected[0],this.filterInput.focused=!0,this.filterInput.onEscape=()=>this.settle(void 0),this.list=this.buildList(),this.render_()}buildList(){let e=new n(s(this.items,this.selected,this.multi),10,a(this.theme)),t=this.filterInput.getValue();t&&e.setFilter(t);let r=c(this.items,t),i=this.cursorValue?r.indexOf(this.cursorValue):-1;return i>=0&&e.setSelectedIndex(i),e.onSelectionChange=e=>{this.cursorValue=e.value},e.onSelect=e=>this.multi?this.settle(this.selected):this.settle([e.value]),e.onCancel=()=>this.settle(void 0),e}render_(){this.clear(),this.addChild(new o(this.theme)),this.addChild(new r(1)),this.addChild(new i(this.theme.fg(`mdHeading`,this.theme.bold(this.title)),1,0)),this.addChild(new r(1)),this.addChild(this.filterInput),this.addChild(this.list),this.addChild(new r(1)),this.addChild(new i(this.theme.fg(`muted`,this.multi?`↑↓ navigate space toggle enter confirm esc cancel type to filter`:`↑↓ navigate enter select esc cancel type to filter`),1,0)),this.addChild(new r(1)),this.addChild(new o(this.theme))}rebuild(){this.list=this.buildList(),this.render_()}settle(e){this.settled||(this.settled=!0,this.done(e))}handleInput(e){if(this.multi&&e===` `){let e=this.list.getSelectedItem();e&&(this.selected=l(this.selected,e.value),this.cursorValue=e.value,this.rebuild());return}if(this.keybindings.matches(e,`tui.select.up`)||this.keybindings.matches(e,`tui.select.down`)||this.keybindings.matches(e,`tui.select.confirm`)||this.keybindings.matches(e,`tui.select.cancel`)){this.list.handleInput(e);return}let t=this.filterInput.getValue();this.filterInput.handleInput(e);let n=this.filterInput.getValue();n!==t&&this.list.setFilter(n)}};export{u as MatrixPickerComponent,s as markItems,l as toggleValue,c as visibleValues};
2
2
  //# sourceMappingURL=matrixPicker.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"matrixPicker.mjs","names":[],"sources":["../../../../src/extensions/entries/components/matrixPicker.ts"],"sourcesContent":["import { DynamicBorder, getSelectListTheme, type Theme } from '@earendil-works/pi-coding-agent';\nimport {\n Container,\n Input,\n type KeybindingsManager,\n type SelectItem,\n SelectList,\n Spacer,\n Text,\n} from '@earendil-works/pi-tui';\n\nconst MAX_VISIBLE = 10;\nconst SINGLE_HINT = '↑↓ navigate enter select esc cancel type to filter';\nconst MULTI_HINT = '↑↓ navigate space toggle enter confirm esc cancel type to filter';\nconst SPACE = ' ';\nconst MARK_ON = '[x]';\nconst MARK_OFF = '[ ]';\n\nexport interface MatrixPickerOptions {\n title: string;\n /** Rows with plain labels. Selection markers are added by the component. */\n items: SelectItem[];\n /** Values selected on open. In single mode the first one is the cursor row. */\n selected: string[];\n multi: boolean;\n}\n\n/**\n * Rows carrying their current selection marker.\n *\n * Multi-select only. A single-select list is a button group: the cursor is the\n * selection, so adding checkboxes would show two competing indicators.\n */\nexport function markItems(items: SelectItem[], selected: string[], multi: boolean): SelectItem[] {\n if (!multi) return items;\n return items.map((item) => ({\n ...item,\n label: `${selected.includes(item.value) ? MARK_ON : MARK_OFF} ${item.label}`,\n }));\n}\n\n/** SelectList filters on a value prefix, so the visible rows follow that rule. */\nexport function visibleValues(items: SelectItem[], filter: string): string[] {\n return items.filter((item) => item.value.toLowerCase().startsWith(filter.toLowerCase())).map((item) => item.value);\n}\n\nexport function toggleValue(selected: string[], value: string): string[] {\n return selected.includes(value) ? selected.filter((entry) => entry !== value) : [...selected, value];\n}\n\n/**\n * Filterable picker with preselected rows and optional multi-select.\n *\n * Pi's built-in ui.select() offers none of that: it takes a bare string[], has\n * no filter input, hardcodes selectedIndex = 0 (extension-selector.js:12), and\n * resolves on the first enter. This drives SelectList directly instead.\n */\nexport class MatrixPickerComponent extends Container {\n private readonly filterInput = new Input();\n private readonly items: SelectItem[];\n private readonly multi: boolean;\n private readonly title: string;\n private readonly theme: Theme;\n // Declared and assigned separately, never as parameter properties: Pi loads\n // extensions through Node's strip-only type stripping, which rejects those.\n private readonly keybindings: KeybindingsManager;\n private readonly done: (value: string[] | undefined) => void;\n private list: SelectList;\n private selected: string[];\n private cursorValue: string | undefined;\n private settled = false;\n\n constructor(\n options: MatrixPickerOptions,\n theme: Theme,\n keybindings: KeybindingsManager,\n done: (value: string[] | undefined) => void,\n ) {\n super();\n this.items = options.items;\n this.multi = options.multi;\n this.title = options.title;\n this.theme = theme;\n this.keybindings = keybindings;\n this.done = done;\n this.selected = [...options.selected];\n this.cursorValue = options.selected[0];\n\n // Focus is set here rather than by the TUI, which focuses the component it\n // was handed (this container) and not the input nested inside it.\n this.filterInput.focused = true;\n this.filterInput.onEscape = () => this.settle(undefined);\n\n this.list = this.buildList();\n this.render_();\n }\n\n private buildList(): SelectList {\n const list = new SelectList(markItems(this.items, this.selected, this.multi), MAX_VISIBLE, getSelectListTheme());\n const filter = this.filterInput.getValue();\n if (filter) list.setFilter(filter);\n\n const visible = visibleValues(this.items, filter);\n const index = this.cursorValue ? visible.indexOf(this.cursorValue) : -1;\n if (index >= 0) list.setSelectedIndex(index);\n\n list.onSelectionChange = (item) => {\n this.cursorValue = item.value;\n };\n list.onSelect = (item) => (this.multi ? this.settle(this.selected) : this.settle([item.value]));\n list.onCancel = () => this.settle(undefined);\n return list;\n }\n\n /**\n * SelectList takes its rows at construction and exposes no setter, so a\n * toggled marker means rebuilding the list and the container around it.\n */\n private render_(): void {\n this.clear();\n this.addChild(new DynamicBorder());\n this.addChild(new Spacer(1));\n this.addChild(new Text(this.theme.fg('accent', this.theme.bold(this.title)), 1, 0));\n this.addChild(new Spacer(1));\n this.addChild(this.filterInput);\n this.addChild(this.list);\n this.addChild(new Spacer(1));\n this.addChild(new Text(this.theme.fg('muted', this.multi ? MULTI_HINT : SINGLE_HINT), 1, 0));\n this.addChild(new Spacer(1));\n this.addChild(new DynamicBorder());\n }\n\n private rebuild(): void {\n this.list = this.buildList();\n this.render_();\n }\n\n private settle(value: string[] | undefined): void {\n // The list can fire onSelect after a cancel has already resolved, and\n // resolving twice would drop the first answer on the floor.\n if (this.settled) return;\n this.settled = true;\n this.done(value);\n }\n\n handleInput(data: string): void {\n if (this.multi && data === SPACE) {\n const current = this.list.getSelectedItem();\n if (current) {\n this.selected = toggleValue(this.selected, current.value);\n this.cursorValue = current.value;\n this.rebuild();\n }\n return;\n }\n\n // Navigation keys belong to the list, everything else edits the filter, so\n // typing narrows the list without stealing the arrow keys from it.\n const navigational =\n this.keybindings.matches(data, 'tui.select.up') ||\n this.keybindings.matches(data, 'tui.select.down') ||\n this.keybindings.matches(data, 'tui.select.confirm') ||\n this.keybindings.matches(data, 'tui.select.cancel');\n\n if (navigational) {\n this.list.handleInput(data);\n return;\n }\n\n const before = this.filterInput.getValue();\n this.filterInput.handleInput(data);\n const after = this.filterInput.getValue();\n if (after !== before) this.list.setFilter(after);\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"matrixPicker.mjs","names":[],"sources":["../../../../src/extensions/entries/components/matrixPicker.ts"],"sourcesContent":["import type { Theme } from '@earendil-works/pi-coding-agent';\nimport {\n type Component,\n Container,\n Input,\n type KeybindingsManager,\n type SelectItem,\n SelectList,\n type SelectListTheme,\n Spacer,\n Text,\n} from '@earendil-works/pi-tui';\n\nconst MAX_VISIBLE = 10;\nconst SINGLE_HINT = '↑↓ navigate enter select esc cancel type to filter';\nconst MULTI_HINT = '↑↓ navigate space toggle enter confirm esc cancel type to filter';\nconst SPACE = ' ';\nconst MARK_ON = '[x]';\nconst MARK_OFF = '[ ]';\nconst HORIZONTAL_BORDER = '─';\n\nfunction doomSelectListTheme(theme: Theme): SelectListTheme {\n return {\n selectedPrefix: (text) => theme.fg('accent', text),\n selectedText: (text) => theme.bg('selectedBg', theme.fg('text', text)),\n description: (text) => theme.fg('dim', text),\n scrollInfo: (text) => theme.fg('muted', text),\n noMatch: (text) => theme.fg('warning', text),\n };\n}\n\nclass DoomPickerBorder implements Component {\n constructor(private readonly theme: Theme) {}\n\n render(width: number): string[] {\n return width > 0 ? [this.theme.fg('borderAccent', HORIZONTAL_BORDER.repeat(width))] : [];\n }\n\n invalidate(): void {\n // The host rebuilds the picker when the theme changes.\n }\n}\n\nexport interface MatrixPickerOptions {\n title: string;\n /** Rows with plain labels. Selection markers are added by the component. */\n items: SelectItem[];\n /** Values selected on open. In single mode the first one is the cursor row. */\n selected: string[];\n multi: boolean;\n}\n\n/**\n * Rows carrying their current selection marker.\n *\n * Multi-select only. A single-select list is a button group: the cursor is the\n * selection, so adding checkboxes would show two competing indicators.\n */\nexport function markItems(items: SelectItem[], selected: string[], multi: boolean): SelectItem[] {\n if (!multi) return items;\n return items.map((item) => ({\n ...item,\n label: `${selected.includes(item.value) ? MARK_ON : MARK_OFF} ${item.label}`,\n }));\n}\n\n/** SelectList filters on a value prefix, so the visible rows follow that rule. */\nexport function visibleValues(items: SelectItem[], filter: string): string[] {\n return items.filter((item) => item.value.toLowerCase().startsWith(filter.toLowerCase())).map((item) => item.value);\n}\n\nexport function toggleValue(selected: string[], value: string): string[] {\n return selected.includes(value) ? selected.filter((entry) => entry !== value) : [...selected, value];\n}\n\n/**\n * Filterable picker with preselected rows and optional multi-select.\n *\n * Pi's built-in ui.select() offers none of that: it takes a bare string[], has\n * no filter input, hardcodes selectedIndex = 0 (extension-selector.js:12), and\n * resolves on the first enter. This drives SelectList directly instead.\n */\nexport class MatrixPickerComponent extends Container {\n private readonly filterInput = new Input();\n private readonly items: SelectItem[];\n private readonly multi: boolean;\n private readonly title: string;\n private readonly theme: Theme;\n // Declared and assigned separately, never as parameter properties: Pi loads\n // extensions through Node's strip-only type stripping, which rejects those.\n private readonly keybindings: KeybindingsManager;\n private readonly done: (value: string[] | undefined) => void;\n private list: SelectList;\n private selected: string[];\n private cursorValue: string | undefined;\n private settled = false;\n\n constructor(\n options: MatrixPickerOptions,\n theme: Theme,\n keybindings: KeybindingsManager,\n done: (value: string[] | undefined) => void,\n ) {\n super();\n this.items = options.items;\n this.multi = options.multi;\n this.title = options.title;\n this.theme = theme;\n this.keybindings = keybindings;\n this.done = done;\n this.selected = [...options.selected];\n this.cursorValue = options.selected[0];\n\n // Focus is set here rather than by the TUI, which focuses the component it\n // was handed (this container) and not the input nested inside it.\n this.filterInput.focused = true;\n this.filterInput.onEscape = () => this.settle(undefined);\n\n this.list = this.buildList();\n this.render_();\n }\n\n private buildList(): SelectList {\n const list = new SelectList(\n markItems(this.items, this.selected, this.multi),\n MAX_VISIBLE,\n doomSelectListTheme(this.theme),\n );\n const filter = this.filterInput.getValue();\n if (filter) list.setFilter(filter);\n\n const visible = visibleValues(this.items, filter);\n const index = this.cursorValue ? visible.indexOf(this.cursorValue) : -1;\n if (index >= 0) list.setSelectedIndex(index);\n\n list.onSelectionChange = (item) => {\n this.cursorValue = item.value;\n };\n list.onSelect = (item) => (this.multi ? this.settle(this.selected) : this.settle([item.value]));\n list.onCancel = () => this.settle(undefined);\n return list;\n }\n\n /**\n * SelectList takes its rows at construction and exposes no setter, so a\n * toggled marker means rebuilding the list and the container around it.\n */\n private render_(): void {\n this.clear();\n this.addChild(new DoomPickerBorder(this.theme));\n this.addChild(new Spacer(1));\n this.addChild(new Text(this.theme.fg('mdHeading', this.theme.bold(this.title)), 1, 0));\n this.addChild(new Spacer(1));\n this.addChild(this.filterInput);\n this.addChild(this.list);\n this.addChild(new Spacer(1));\n this.addChild(new Text(this.theme.fg('muted', this.multi ? MULTI_HINT : SINGLE_HINT), 1, 0));\n this.addChild(new Spacer(1));\n this.addChild(new DoomPickerBorder(this.theme));\n }\n\n private rebuild(): void {\n this.list = this.buildList();\n this.render_();\n }\n\n private settle(value: string[] | undefined): void {\n // The list can fire onSelect after a cancel has already resolved, and\n // resolving twice would drop the first answer on the floor.\n if (this.settled) return;\n this.settled = true;\n this.done(value);\n }\n\n handleInput(data: string): void {\n if (this.multi && data === SPACE) {\n const current = this.list.getSelectedItem();\n if (current) {\n this.selected = toggleValue(this.selected, current.value);\n this.cursorValue = current.value;\n this.rebuild();\n }\n return;\n }\n\n // Navigation keys belong to the list, everything else edits the filter, so\n // typing narrows the list without stealing the arrow keys from it.\n const navigational =\n this.keybindings.matches(data, 'tui.select.up') ||\n this.keybindings.matches(data, 'tui.select.down') ||\n this.keybindings.matches(data, 'tui.select.confirm') ||\n this.keybindings.matches(data, 'tui.select.cancel');\n\n if (navigational) {\n this.list.handleInput(data);\n return;\n }\n\n const before = this.filterInput.getValue();\n this.filterInput.handleInput(data);\n const after = this.filterInput.getValue();\n if (after !== before) this.list.setFilter(after);\n }\n}\n"],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- const e=require(`../../../_virtual/_rolldown/runtime.cjs`);let t=require(`node:fs`);t=e.__toESM(t,1);let n=require(`node:path`);n=e.__toESM(n,1);let r=require(`@earendil-works/pi-tui`),i=require(`@agimon-ai/doompi-ui/components/doomOverlay`),a=require(`@agimon-ai/doompi-ui/rendering`);const o=`CATALOG`,s=[[`↑↓`,`select`],[`enter`,`use skill`],[`tab`,`detail pane`],[`/`,`filter`],[`esc`,`close`]],c=[[`pgup/pgdn`,`scroll`],[`tab`,`tree`],[`esc`,`close`]],l=[[`type`,`to filter`],[`enter`,`keep`],[`esc`,`clear`]];function u(e,t){let n=(0,r.truncateToWidth)(e,Math.max(0,t),`…`);return n+` `.repeat(Math.max(0,t-(0,r.visibleWidth)(n)))}function d(e,t){if(t<=0)return[];let n=[],i=``;for(let a of e.split(/\s+/).filter(Boolean))i.length===0?i=a:(0,r.visibleWidth)(i)+1+(0,r.visibleWidth)(a)<=t?i+=` ${a}`:(n.push(i),i=a);return i.length>0&&n.push(i),n}function f(e){let t=e.match(/^---\r?\n[\s\S]*?\r?\n---\r?\n?([\s\S]*)$/);return(t?t[1]??``:e).replace(/^\s+/,``)}function p(e,t){let n=t.toLowerCase();return e.name.toLowerCase().includes(n)||e.description.toLowerCase().includes(n)}var m=class extends i.DoomOverlay{options;done;readFile;bodyCache=new Map;selected=0;focus=`tree`;filtering=!1;query=``;bodyOffset=0;constructor(e,n,r,i){super(e,n),this.options=r,this.done=i,this.readFile=r.readFile??(e=>t.default.readFileSync(e,`utf8`))}rows(){let e=this.query.trim(),t=[];for(let n of this.options.catalog.groups){let r=e?n.owners.map(t=>({...t,skills:t.skills.filter(t=>p(t,e))})).filter(e=>e.skills.length>0):n.owners;if(r.length===0)continue;let i=r.reduce((e,t)=>e+t.skills.length,0);t.push({kind:`group`,depth:0,label:n.label,tag:String(i)});for(let e of r){t.push({kind:`owner`,depth:1,label:e.owner,tag:String(e.skills.length)});for(let n of e.skills)t.push({kind:`skill`,depth:2,label:n.name,tag:``,skill:n})}}return t}selectable(e){let t=[];for(let[n,r]of e.entries())r.skill&&t.push(n);return t}currentRow(){let e=this.rows(),t=this.selectable(e);if(t.length!==0)return e[t[Math.min(this.selected,t.length-1)]]}move(e){let t=this.selectable(this.rows()).length;t!==0&&(this.selected=Math.max(0,Math.min(t-1,this.selected+e)),this.bodyOffset=0,this.invalidate())}bodyLines(e,t){let n=this.bodyCache.get(e.filePath);if(!n){let t;try{t=f(this.readFile(e.filePath))}catch{t=`Could not read SKILL.md.`}n=t.split(/\r?\n/),this.bodyCache.set(e.filePath,n)}return n.flatMap(e=>e.trim()===``?[``]:d(e,t))}handleInput(e){if(this.filtering)return this.handleFilterInput(e);if((0,r.matchesKey)(e,`escape`)){if(this.query){this.query=``,this.selected=0,this.invalidate();return}this.done(void 0);return}if(e===` `||(0,r.matchesKey)(e,`tab`)){this.focus=this.focus===`tree`?`body`:`tree`,this.invalidate();return}if((0,r.matchesKey)(e,`pageUp`)){this.bodyOffset=Math.max(0,this.bodyOffset-10),this.invalidate();return}if((0,r.matchesKey)(e,`pageDown`)){this.bodyOffset+=10,this.invalidate();return}if(this.focus!==`body`){if(e===`/`){this.filtering=!0,this.invalidate();return}if((0,r.matchesKey)(e,`up`)||(0,r.matchesKey)(e,`k`))return this.move(-1);if((0,r.matchesKey)(e,`down`)||(0,r.matchesKey)(e,`j`))return this.move(1);if(e===`\r`||e===`
1
+ const e=require(`../../../_virtual/_rolldown/runtime.cjs`);let t=require(`node:fs`);t=e.__toESM(t,1);let n=require(`node:path`);n=e.__toESM(n,1);let r=require(`@earendil-works/pi-tui`),i=require(`@agimon-ai/doompi-ui/components/doomOverlay`),a=require(`@agimon-ai/doompi-ui/rendering`);const o=`CATALOG`,s=[[`↑↓`,`select`],[`enter`,`use skill`],[`tab`,`detail pane`],[`/`,`filter`],[`esc`,`close`]],c=[[`pgup/pgdn`,`scroll`],[`tab`,`tree`],[`esc`,`close`]],l=[[`type`,`to filter`],[`enter`,`keep`],[`esc`,`clear`]];function u(e,t){return(0,a.fitStyledLine)(e,t,`…`)}function d(e,t){if(t<=0)return[];let n=[],i=``;for(let a of e.split(/\s+/).filter(Boolean))i.length===0?i=a:(0,r.visibleWidth)(i)+1+(0,r.visibleWidth)(a)<=t?i+=` ${a}`:(n.push(i),i=a);return i.length>0&&n.push(i),n}function f(e){let t=e.match(/^---\r?\n[\s\S]*?\r?\n---\r?\n?([\s\S]*)$/);return(t?t[1]??``:e).replace(/^\s+/,``)}function p(e,t){let n=t.toLowerCase();return e.name.toLowerCase().includes(n)||e.description.toLowerCase().includes(n)}var m=class extends i.DoomOverlay{options;done;readFile;bodyCache=new Map;selected=0;focus=`tree`;filtering=!1;query=``;bodyOffset=0;constructor(e,n,r,i){super(e,n),this.options=r,this.done=i,this.readFile=r.readFile??(e=>t.default.readFileSync(e,`utf8`))}rows(){let e=this.query.trim(),t=[];for(let n of this.options.catalog.groups){let r=e?n.owners.map(t=>({...t,skills:t.skills.filter(t=>p(t,e))})).filter(e=>e.skills.length>0):n.owners;if(r.length===0)continue;let i=r.reduce((e,t)=>e+t.skills.length,0);t.push({kind:`group`,depth:0,label:n.label,tag:String(i)});for(let e of r){t.push({kind:`owner`,depth:1,label:e.owner,tag:String(e.skills.length)});for(let n of e.skills)t.push({kind:`skill`,depth:2,label:n.name,tag:``,skill:n})}}return t}selectable(e){let t=[];for(let[n,r]of e.entries())r.skill&&t.push(n);return t}currentRow(){let e=this.rows(),t=this.selectable(e);if(t.length!==0)return e[t[Math.min(this.selected,t.length-1)]]}move(e){let t=this.selectable(this.rows()).length;t!==0&&(this.selected=Math.max(0,Math.min(t-1,this.selected+e)),this.bodyOffset=0,this.invalidate())}bodyLines(e,t){let n=this.bodyCache.get(e.filePath);if(!n){let t;try{t=f(this.readFile(e.filePath))}catch{t=`Could not read SKILL.md.`}n=t.split(/\r?\n/),this.bodyCache.set(e.filePath,n)}return n.flatMap(e=>e.trim()===``?[``]:d(e,t))}handleInput(e){if(this.filtering){this.handleFilterInput(e);return}if((0,r.matchesKey)(e,`escape`)){if(this.query){this.query=``,this.selected=0,this.invalidate();return}this.done(void 0);return}if(e===` `||(0,r.matchesKey)(e,`tab`)){this.focus=this.focus===`tree`?`body`:`tree`,this.invalidate();return}if((0,r.matchesKey)(e,`pageUp`)){this.bodyOffset=Math.max(0,this.bodyOffset-10),this.invalidate();return}if((0,r.matchesKey)(e,`pageDown`)){this.bodyOffset+=10,this.invalidate();return}if(this.focus!==`body`){if(e===`/`){this.filtering=!0,this.invalidate();return}if((0,r.matchesKey)(e,`up`)||(0,r.matchesKey)(e,`k`)){this.move(-1);return}if((0,r.matchesKey)(e,`down`)||(0,r.matchesKey)(e,`j`)){this.move(1);return}if(e===`\r`||e===`
2
2
  `||(0,r.matchesKey)(e,`enter`)){let e=this.currentRow()?.skill;e&&this.done({kind:`invoke`,skill:e})}}}handleFilterInput(e){if((0,r.matchesKey)(e,`escape`)){this.filtering=!1,this.query=``,this.selected=0,this.invalidate();return}if(e===`\r`||e===`
3
3
  `){this.filtering=!1,this.invalidate();return}e===``?this.query=this.query.slice(0,-1):e.length===1&&e>=` `&&(this.query+=e),this.selected=0,this.invalidate()}getChrome(){let{skillCount:e,promptTokens:t,bodyTokens:n}=this.options.catalog,r=this.options.catalog.groups.filter(e=>e.owners.length>0).length,o=this.filtering?l:this.focus===`body`?c:s;return{title:`SKILLS`,accent:i.DOOM_OVERLAY_ACCENT,breadcrumb:`SPC › e / extension › s / skills`,headerRight:`${e} skills · ${r} sources · ${(0,a.formatTokens)(t)} always-on · ${(0,a.formatTokens)(n)} full`,footer:this.hintLine(o)}}badge(e,t,n=`text`){return this.theme.bg(t,this.theme.fg(n,` ${e} `))}hintLine(e){return e.map(([e,t])=>`${this.badge(e,`selectedBg`)}${this.theme.fg(`dim`,` ${t}`)}`).join(this.theme.fg(`dim`,` `))}renderBody(e,t){if(e<60)return this.renderStacked(e,t);let n=Math.max(20,Math.floor((e-1)*.3333333333333333)),i=Math.max(1,e-n-1),a=Math.max(1,n-1),o=Math.max(1,i-1),s=this.treeLines(a,t),c=this.detailLines(o,t),l=this.theme.fg(`borderMuted`,`│`);return Array.from({length:t},(t,n)=>(0,r.truncateToWidth)(`${u(s[n]??``,a)} ${l} ${u(c[n]??``,o)}`,e,`…`))}renderStacked(e,t){let n=Math.max(1,Math.floor((t-1)/2)),r=Math.max(0,t-n-1),i=this.treeLines(e,n),a=this.detailLines(e,r);return[...i.slice(0,n),...Array.from({length:Math.max(0,n-i.length)},()=>``),this.theme.fg(`borderMuted`,`─`.repeat(e)),...a.slice(0,r)]}treeLines(e,t){let n=this.filtering||this.query?`${o} /${this.query}`:o,i=[this.theme.bold(this.theme.fg(`accent`,n)),``],a=this.rows();if(a.length===0)return i.push(this.theme.fg(`dim`,`No skills discovered.`)),i;let s=Math.max(1,t-i.length),c=this.selectable(a),l=c[Math.min(this.selected,c.length-1)]??-1,d=Math.max(0,Math.min(l-s+1,Math.max(0,a.length-s)));for(let[t,n]of a.slice(d,d+s).entries()){let a=d+t===l,o=a?this.theme.fg(`accent`,`›`):` `,s=` `.repeat(n.depth*2),c=n.kind===`group`?`accent`:n.kind===`owner`?`dim`:`text`,f=this.theme.fg(c,`${s}${n.label}`);(n.kind===`group`||a)&&(f=this.theme.bold(f));let p=n.tag?(0,r.truncateToWidth)(n.tag,Math.floor(e*.4),`…`):``,m=p?(0,r.visibleWidth)(p)+1:0,h=u(`${o} ${f}`,Math.max(1,e-m)),g=p?`${h} ${this.theme.fg(`dim`,p)}`:h;i.push(a?this.theme.bg(`selectedBg`,u(g,e)):g)}return i}field(e,t,n,r){return d(t,Math.max(1,r-13)).map((t,r)=>`${this.theme.fg(`dim`,u(r===0?e:``,13))}${this.theme.fg(n,t)}`)}detailLines(e,t){let r=this.currentRow()?.skill;if(!r)return[``,``,this.theme.fg(`dim`,`Select a skill to read it.`)];let i=n.default.relative(this.options.repoRoot,r.filePath)||r.filePath,a=[this.theme.bold(this.theme.fg(`accent`,r.name)),``,...this.field(`description`,r.description,`text`,e),...this.field(`source`,`${r.group} › ${r.owner}`,`success`,e),...this.field(`path`,i,`text`,e),...this.field(`invocable`,r.modelInvocable?`model · /skill:${r.name}`:`/skill:${r.name} only`,`success`,e)],o=this.bodyLines(r,e),s=Math.max(0,t-a.length-2);this.bodyOffset=Math.min(this.bodyOffset,Math.max(0,o.length-s));let c=o.slice(this.bodyOffset,this.bodyOffset+s),l=o.length>s?`lines ${this.bodyOffset+1}-${this.bodyOffset+c.length} / ${o.length}`:`SKILL.md`,u=this.focus===`body`?this.theme.fg(`accent`,` ◀ focused`):``;a.push(``,`${this.theme.fg(`dim`,l)}${u}`);for(let e of c)a.push(this.theme.fg(`dim`,e));return a}};async function h(e,t){return e.ui.custom((e,n,r,i)=>new m(e,n,t,i),i.DOOM_FULLSCREEN_UI_OPTIONS)}exports.SkillsOverlayComponent=m,exports.openSkillsOverlay=h,exports.skillBody=f;
4
4
  //# sourceMappingURL=skillsOverlay.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"skillsOverlay.cjs","names":["DoomOverlay","fs","DOOM_OVERLAY_ACCENT","path","DOOM_FULLSCREEN_UI_OPTIONS"],"sources":["../../../../src/extensions/entries/components/skillsOverlay.ts"],"sourcesContent":["/**\n * Skills: the `SPC e s` overlay.\n *\n * A tree of the skills this session loaded on the left, the selected skill's\n * frontmatter and body on the right. Everything listed is loaded, so the\n * surface answers \"what do I have, and what does it cost\" rather than doubling\n * as a domain browser.\n *\n * The tree is flat and always open: groups and owners are headings, only skills\n * take the selection. Rendering is pure: the catalogue is resolved once by the\n * caller and passed in, so `render(width)` returns lines with no side effects\n * and the whole surface can be asserted as text without a live terminal.\n */\n\nimport fs from 'node:fs';\nimport path from 'node:path';\nimport {\n DOOM_FULLSCREEN_UI_OPTIONS,\n DOOM_OVERLAY_ACCENT,\n DoomOverlay,\n type DoomOverlayChrome,\n type DoomOverlayTui,\n} from '@agimon-ai/doompi-ui/components/doomOverlay';\nimport { formatTokens } from '@agimon-ai/doompi-ui/rendering';\nimport type { ExtensionContext, Theme } from '@earendil-works/pi-coding-agent';\nimport { matchesKey, truncateToWidth, visibleWidth } from '@earendil-works/pi-tui';\nimport type { SkillCatalog, SkillEntry } from '../../../adapters/skillCatalog.ts';\n\ntype ThemeColor = Parameters<Theme['fg']>[0];\ntype ThemeBg = Parameters<Theme['bg']>[0];\n\nconst MIN_WIDTH = 60;\n/** A third for the tree, two thirds for the skill being read. */\nconst TREE_PANE_RATIO = 1 / 3;\n/** One blank column each side of the pane divider so neither column touches it. */\nconst PANE_GUTTER = 1;\nconst MIN_PANE_WIDTH = 20;\nconst LABEL_COLUMN = 13;\n/** Ceiling on the columns a row's right-hand tag may take from its label. */\nconst TAG_SHARE = 0.4;\nconst INDENT = 2;\nconst ELLIPSIS = '…';\nconst SELECTION_MARKER = '›';\nconst RULE = '─';\nconst KEY_TAB = '\\t';\nconst KEY_ENTER = '\\r';\nconst KEY_NEWLINE = '\\n';\nconst KEY_BACKSPACE = '\\x7f';\nconst FILTER_KEY = '/';\nconst BODY_PAGE = 10;\n\nconst TITLE = 'SKILLS';\nconst BREADCRUMB = 'SPC › e / extension › s / skills';\nconst HEADING = 'CATALOG';\nconst EMPTY_TREE = 'No skills discovered.';\nconst NO_SELECTION = 'Select a skill to read it.';\nconst UNREADABLE = 'Could not read SKILL.md.';\n\nconst TREE_HINTS: readonly (readonly [string, string])[] = [\n ['↑↓', 'select'],\n ['enter', 'use skill'],\n ['tab', 'detail pane'],\n ['/', 'filter'],\n ['esc', 'close'],\n];\nconst BODY_HINTS: readonly (readonly [string, string])[] = [\n ['pgup/pgdn', 'scroll'],\n ['tab', 'tree'],\n ['esc', 'close'],\n];\nconst FILTER_HINTS: readonly (readonly [string, string])[] = [\n ['type', 'to filter'],\n ['enter', 'keep'],\n ['esc', 'clear'],\n];\n\nexport type SkillsOverlayResult = { kind: 'invoke'; skill: SkillEntry } | undefined;\n\nexport interface SkillsOverlayOptions {\n catalog: SkillCatalog;\n /** Repo root, so paths render relative rather than as absolute noise. */\n repoRoot: string;\n readFile?: (filePath: string) => string;\n}\n\ntype RowKind = 'group' | 'owner' | 'skill';\n\ninterface TreeRow {\n kind: RowKind;\n depth: number;\n label: string;\n tag: string;\n /** Only a skill row carries one; group and owner rows are headings. */\n skill?: SkillEntry;\n}\n\nfunction fit(text: string, width: number): string {\n const clipped = truncateToWidth(text, Math.max(0, width), ELLIPSIS);\n return clipped + ' '.repeat(Math.max(0, width - visibleWidth(clipped)));\n}\n\nfunction wrap(text: string, width: number): string[] {\n if (width <= 0) return [];\n const lines: string[] = [];\n let current = '';\n for (const word of text.split(/\\s+/).filter(Boolean)) {\n if (current.length === 0) current = word;\n else if (visibleWidth(current) + 1 + visibleWidth(word) <= width) current += ` ${word}`;\n else {\n lines.push(current);\n current = word;\n }\n }\n if (current.length > 0) lines.push(current);\n return lines;\n}\n\n/** Drops YAML frontmatter so the preview starts at the prose, as Pi does. */\nexport function skillBody(content: string): string {\n const match = content.match(/^---\\r?\\n[\\s\\S]*?\\r?\\n---\\r?\\n?([\\s\\S]*)$/);\n return (match ? (match[1] ?? '') : content).replace(/^\\s+/, '');\n}\n\nfunction matches(skill: SkillEntry, query: string): boolean {\n const needle = query.toLowerCase();\n return skill.name.toLowerCase().includes(needle) || skill.description.toLowerCase().includes(needle);\n}\n\nexport class SkillsOverlayComponent extends DoomOverlay {\n private readonly options: SkillsOverlayOptions;\n private readonly done: (result: SkillsOverlayResult) => void;\n private readonly readFile: (filePath: string) => string;\n private readonly bodyCache = new Map<string, string[]>();\n\n /** Index into the skill rows, not into every row: headings are not selectable. */\n private selected = 0;\n private focus: 'tree' | 'body' = 'tree';\n private filtering = false;\n private query = '';\n private bodyOffset = 0;\n\n constructor(\n tui: DoomOverlayTui,\n theme: Theme,\n options: SkillsOverlayOptions,\n done: (result: SkillsOverlayResult) => void,\n ) {\n super(tui, theme);\n this.options = options;\n this.done = done;\n this.readFile = options.readFile ?? ((filePath) => fs.readFileSync(filePath, 'utf8'));\n }\n\n /** Every row, honouring the active filter. The tree does not fold. */\n private rows(): TreeRow[] {\n const query = this.query.trim();\n const rows: TreeRow[] = [];\n for (const group of this.options.catalog.groups) {\n const owners = query\n ? group.owners\n .map((owner) => ({ ...owner, skills: owner.skills.filter((skill) => matches(skill, query)) }))\n .filter((owner) => owner.skills.length > 0)\n : group.owners;\n if (owners.length === 0) continue;\n const count = owners.reduce((total, owner) => total + owner.skills.length, 0);\n rows.push({ kind: 'group', depth: 0, label: group.label, tag: String(count) });\n for (const owner of owners) {\n rows.push({ kind: 'owner', depth: 1, label: owner.owner, tag: String(owner.skills.length) });\n for (const skill of owner.skills) {\n rows.push({ kind: 'skill', depth: 2, label: skill.name, tag: '', skill });\n }\n }\n }\n return rows;\n }\n\n /**\n * Where the skill rows sit in `rows()`.\n *\n * The selection indexes this list rather than `rows()`, which is what keeps\n * headings unselectable without the key handler having to hunt past them, and\n * what puts the opening selection on a skill rather than on the first group.\n */\n private selectable(rows: TreeRow[]): number[] {\n const indexes: number[] = [];\n for (const [index, row] of rows.entries()) if (row.skill) indexes.push(index);\n return indexes;\n }\n\n private currentRow(): TreeRow | undefined {\n const rows = this.rows();\n const indexes = this.selectable(rows);\n if (indexes.length === 0) return undefined;\n return rows[indexes[Math.min(this.selected, indexes.length - 1)] as number];\n }\n\n private move(delta: number): void {\n const count = this.selectable(this.rows()).length;\n if (count === 0) return;\n this.selected = Math.max(0, Math.min(count - 1, this.selected + delta));\n this.bodyOffset = 0;\n this.invalidate();\n }\n\n private bodyLines(skill: SkillEntry, width: number): string[] {\n let raw = this.bodyCache.get(skill.filePath);\n if (!raw) {\n let source: string;\n try {\n source = skillBody(this.readFile(skill.filePath));\n } catch {\n source = UNREADABLE;\n }\n raw = source.split(/\\r?\\n/);\n this.bodyCache.set(skill.filePath, raw);\n }\n // Wrapped at render width rather than at read time, so a resize reflows the\n // same cached file instead of re-reading it.\n return raw.flatMap((line) => (line.trim() === '' ? [''] : wrap(line, width)));\n }\n\n handleInput(data: string): void {\n if (this.filtering) return this.handleFilterInput(data);\n\n if (matchesKey(data, 'escape')) {\n // The filter clears before the overlay closes: escaping straight out of a\n // narrowed tree loses the search with no way to see what it hid.\n if (this.query) {\n this.query = '';\n this.selected = 0;\n this.invalidate();\n return;\n }\n this.done(undefined);\n return;\n }\n if (data === KEY_TAB || matchesKey(data, 'tab')) {\n this.focus = this.focus === 'tree' ? 'body' : 'tree';\n this.invalidate();\n return;\n }\n if (matchesKey(data, 'pageUp')) {\n this.bodyOffset = Math.max(0, this.bodyOffset - BODY_PAGE);\n this.invalidate();\n return;\n }\n if (matchesKey(data, 'pageDown')) {\n this.bodyOffset += BODY_PAGE;\n this.invalidate();\n return;\n }\n if (this.focus === 'body') return;\n\n if (data === FILTER_KEY) {\n this.filtering = true;\n this.invalidate();\n return;\n }\n if (matchesKey(data, 'up') || matchesKey(data, 'k')) return this.move(-1);\n if (matchesKey(data, 'down') || matchesKey(data, 'j')) return this.move(1);\n\n if (data === KEY_ENTER || data === KEY_NEWLINE || matchesKey(data, 'enter')) {\n const skill = this.currentRow()?.skill;\n if (skill) this.done({ kind: 'invoke', skill });\n }\n }\n\n private handleFilterInput(data: string): void {\n if (matchesKey(data, 'escape')) {\n this.filtering = false;\n this.query = '';\n this.selected = 0;\n this.invalidate();\n return;\n }\n if (data === KEY_ENTER || data === KEY_NEWLINE) {\n this.filtering = false;\n this.invalidate();\n return;\n }\n if (data === KEY_BACKSPACE) this.query = this.query.slice(0, -1);\n // Control sequences are dropped rather than appended: an arrow key would\n // otherwise land in the query as a raw escape sequence.\n else if (data.length === 1 && data >= ' ') this.query += data;\n this.selected = 0;\n this.invalidate();\n }\n\n protected getChrome(): DoomOverlayChrome {\n const { skillCount, promptTokens, bodyTokens } = this.options.catalog;\n const sources = this.options.catalog.groups.filter((group) => group.owners.length > 0).length;\n const hints = this.filtering ? FILTER_HINTS : this.focus === 'body' ? BODY_HINTS : TREE_HINTS;\n return {\n title: TITLE,\n accent: DOOM_OVERLAY_ACCENT,\n breadcrumb: BREADCRUMB,\n // Two token figures, because they answer different questions: what the\n // catalogue costs on every request, and what reading all of it would cost.\n headerRight: `${skillCount} skills · ${sources} sources · ${formatTokens(promptTokens)} always-on · ${formatTokens(bodyTokens)} full`,\n footer: this.hintLine(hints),\n };\n }\n\n private badge(text: string, background: ThemeBg, colour: ThemeColor = 'text'): string {\n return this.theme.bg(background, this.theme.fg(colour, ` ${text} `));\n }\n\n /** Footer hints as capped keys: `[ enter ] use skill`, per the mockup. */\n private hintLine(hints: readonly (readonly [string, string])[]): string {\n return hints\n .map(([key, label]) => `${this.badge(key, 'selectedBg')}${this.theme.fg('dim', ` ${label}`)}`)\n .join(this.theme.fg('dim', ' '));\n }\n\n protected renderBody(width: number, height: number): string[] {\n if (width < MIN_WIDTH) return this.renderStacked(width, height);\n\n const treeWidth = Math.max(MIN_PANE_WIDTH, Math.floor((width - 1) * TREE_PANE_RATIO));\n const detailWidth = Math.max(1, width - treeWidth - 1);\n const treeContent = Math.max(1, treeWidth - PANE_GUTTER);\n const detailContent = Math.max(1, detailWidth - PANE_GUTTER);\n\n const left = this.treeLines(treeContent, height);\n const right = this.detailLines(detailContent, height);\n const divider = this.theme.fg('borderMuted', '│');\n return Array.from({ length: height }, (_, index) => {\n const row = `${fit(left[index] ?? '', treeContent)} ${divider} ${fit(right[index] ?? '', detailContent)}`;\n return truncateToWidth(row, width, ELLIPSIS);\n });\n }\n\n private renderStacked(width: number, height: number): string[] {\n const topHeight = Math.max(1, Math.floor((height - 1) / 2));\n const bottomHeight = Math.max(0, height - topHeight - 1);\n const top = this.treeLines(width, topHeight);\n const bottom = this.detailLines(width, bottomHeight);\n return [\n ...top.slice(0, topHeight),\n ...Array.from({ length: Math.max(0, topHeight - top.length) }, () => ''),\n this.theme.fg('borderMuted', RULE.repeat(width)),\n ...bottom.slice(0, bottomHeight),\n ];\n }\n\n private treeLines(width: number, height: number): string[] {\n const heading = this.filtering || this.query ? `${HEADING} ${FILTER_KEY}${this.query}` : HEADING;\n const lines = [this.theme.bold(this.theme.fg('accent', heading)), ''];\n const rows = this.rows();\n if (rows.length === 0) {\n lines.push(this.theme.fg('dim', EMPTY_TREE));\n return lines;\n }\n\n const budget = Math.max(1, height - lines.length);\n const indexes = this.selectable(rows);\n // No skill rows means the filter matched nothing, so nothing is highlighted.\n const selected = indexes[Math.min(this.selected, indexes.length - 1)] ?? -1;\n const start = Math.max(0, Math.min(selected - budget + 1, Math.max(0, rows.length - budget)));\n for (const [offset, row] of rows.slice(start, start + budget).entries()) {\n const isSelected = start + offset === selected;\n const marker = isSelected ? this.theme.fg('accent', SELECTION_MARKER) : ' ';\n const indent = ' '.repeat(row.depth * INDENT);\n // Three tiers rather than two: the group heading leads, the owner beneath\n // it recedes to a caption, and the skills it holds carry the reading\n // weight because they are the only rows that do anything.\n const colour: ThemeColor = row.kind === 'group' ? 'accent' : row.kind === 'owner' ? 'dim' : 'text';\n let label = this.theme.fg(colour, `${indent}${row.label}`);\n if (row.kind === 'group' || isSelected) label = this.theme.bold(label);\n // The tag is capped before it claims columns: an owner in many domains\n // carries a long `+a,b,c` hint that would otherwise leave the name with\n // no room at all and render the row as a bare count.\n const tag = row.tag ? truncateToWidth(row.tag, Math.floor(width * TAG_SHARE), ELLIPSIS) : '';\n const tagWidth = tag ? visibleWidth(tag) + 1 : 0;\n const body = fit(`${marker} ${label}`, Math.max(1, width - tagWidth));\n const line = tag ? `${body} ${this.theme.fg('dim', tag)}` : body;\n lines.push(isSelected ? this.theme.bg('selectedBg', fit(line, width)) : line);\n }\n return lines;\n }\n\n private field(label: string, value: string, colour: ThemeColor, width: number): string[] {\n const valueWidth = Math.max(1, width - LABEL_COLUMN);\n return wrap(value, valueWidth).map(\n (line, index) =>\n `${this.theme.fg('dim', fit(index === 0 ? label : '', LABEL_COLUMN))}${this.theme.fg(colour, line)}`,\n );\n }\n\n private detailLines(width: number, height: number): string[] {\n const skill = this.currentRow()?.skill;\n if (!skill) return ['', '', this.theme.fg('dim', NO_SELECTION)];\n\n const relative = path.relative(this.options.repoRoot, skill.filePath) || skill.filePath;\n const lines = [\n this.theme.bold(this.theme.fg('accent', skill.name)),\n '',\n ...this.field('description', skill.description, 'text', width),\n ...this.field('source', `${skill.group} › ${skill.owner}`, 'success', width),\n ...this.field('path', relative, 'text', width),\n ...this.field(\n 'invocable',\n skill.modelInvocable ? `model · /skill:${skill.name}` : `/skill:${skill.name} only`,\n 'success',\n width,\n ),\n ];\n\n const body = this.bodyLines(skill, width);\n const remaining = Math.max(0, height - lines.length - 2);\n // Clamped here rather than in the key handler, because how far the body can\n // scroll depends on the width it has just wrapped to.\n this.bodyOffset = Math.min(this.bodyOffset, Math.max(0, body.length - remaining));\n const shown = body.slice(this.bodyOffset, this.bodyOffset + remaining);\n const position =\n body.length > remaining\n ? `lines ${this.bodyOffset + 1}-${this.bodyOffset + shown.length} / ${body.length}`\n : 'SKILL.md';\n const focus = this.focus === 'body' ? this.theme.fg('accent', ' ◀ focused') : '';\n\n lines.push('', `${this.theme.fg('dim', position)}${focus}`);\n for (const line of shown) lines.push(this.theme.fg('dim', line));\n return lines;\n }\n}\n\nexport async function openSkillsOverlay(\n ctx: ExtensionContext,\n options: SkillsOverlayOptions,\n): Promise<SkillsOverlayResult> {\n return ctx.ui.custom<SkillsOverlayResult>(\n (tui, theme, _keybindings, done) => new SkillsOverlayComponent(tui, theme, options, done),\n DOOM_FULLSCREEN_UI_OPTIONS,\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"skillsOverlay.cjs","names":["DoomOverlay","fs","DOOM_OVERLAY_ACCENT","path","DOOM_FULLSCREEN_UI_OPTIONS"],"sources":["../../../../src/extensions/entries/components/skillsOverlay.ts"],"sourcesContent":["/**\n * Skills: the `SPC e s` overlay.\n *\n * A tree of the skills this session loaded on the left, the selected skill's\n * frontmatter and body on the right. Everything listed is loaded, so the\n * surface answers \"what do I have, and what does it cost\" rather than doubling\n * as a domain browser.\n *\n * The tree is flat and always open: groups and owners are headings, only skills\n * take the selection. Rendering is pure: the catalogue is resolved once by the\n * caller and passed in, so `render(width)` returns lines with no side effects\n * and the whole surface can be asserted as text without a live terminal.\n */\n\nimport fs from 'node:fs';\nimport path from 'node:path';\nimport {\n DOOM_FULLSCREEN_UI_OPTIONS,\n DOOM_OVERLAY_ACCENT,\n DoomOverlay,\n type DoomOverlayChrome,\n type DoomOverlayTui,\n} from '@agimon-ai/doompi-ui/components/doomOverlay';\nimport { fitStyledLine, formatTokens } from '@agimon-ai/doompi-ui/rendering';\nimport type { ExtensionContext, Theme } from '@earendil-works/pi-coding-agent';\nimport { matchesKey, truncateToWidth, visibleWidth } from '@earendil-works/pi-tui';\nimport type { SkillCatalog, SkillEntry } from '../../../adapters/skillCatalog.ts';\n\ntype ThemeColor = Parameters<Theme['fg']>[0];\ntype ThemeBg = Parameters<Theme['bg']>[0];\n\nconst MIN_WIDTH = 60;\n/** A third for the tree, two thirds for the skill being read. */\nconst TREE_PANE_RATIO = 1 / 3;\n/** One blank column each side of the pane divider so neither column touches it. */\nconst PANE_GUTTER = 1;\nconst MIN_PANE_WIDTH = 20;\nconst LABEL_COLUMN = 13;\n/** Ceiling on the columns a row's right-hand tag may take from its label. */\nconst TAG_SHARE = 0.4;\nconst INDENT = 2;\nconst ELLIPSIS = '…';\nconst SELECTION_MARKER = '›';\nconst RULE = '─';\nconst KEY_TAB = '\\t';\nconst KEY_ENTER = '\\r';\nconst KEY_NEWLINE = '\\n';\nconst KEY_BACKSPACE = '\\x7f';\nconst FILTER_KEY = '/';\nconst BODY_PAGE = 10;\n\nconst TITLE = 'SKILLS';\nconst BREADCRUMB = 'SPC › e / extension › s / skills';\nconst HEADING = 'CATALOG';\nconst EMPTY_TREE = 'No skills discovered.';\nconst NO_SELECTION = 'Select a skill to read it.';\nconst UNREADABLE = 'Could not read SKILL.md.';\n\nconst TREE_HINTS: readonly (readonly [string, string])[] = [\n ['↑↓', 'select'],\n ['enter', 'use skill'],\n ['tab', 'detail pane'],\n ['/', 'filter'],\n ['esc', 'close'],\n];\nconst BODY_HINTS: readonly (readonly [string, string])[] = [\n ['pgup/pgdn', 'scroll'],\n ['tab', 'tree'],\n ['esc', 'close'],\n];\nconst FILTER_HINTS: readonly (readonly [string, string])[] = [\n ['type', 'to filter'],\n ['enter', 'keep'],\n ['esc', 'clear'],\n];\n\nexport type SkillsOverlayResult = { kind: 'invoke'; skill: SkillEntry } | undefined;\n\nexport interface SkillsOverlayOptions {\n catalog: SkillCatalog;\n /** Repo root, so paths render relative rather than as absolute noise. */\n repoRoot: string;\n readFile?: (filePath: string) => string;\n}\n\ntype RowKind = 'group' | 'owner' | 'skill';\n\ninterface TreeRow {\n kind: RowKind;\n depth: number;\n label: string;\n tag: string;\n /** Only a skill row carries one; group and owner rows are headings. */\n skill?: SkillEntry;\n}\n\nfunction fitRow(text: string, width: number): string {\n return fitStyledLine(text, width, ELLIPSIS);\n}\n\nfunction wrap(text: string, width: number): string[] {\n if (width <= 0) return [];\n const lines: string[] = [];\n let current = '';\n for (const word of text.split(/\\s+/).filter(Boolean)) {\n if (current.length === 0) current = word;\n else if (visibleWidth(current) + 1 + visibleWidth(word) <= width) current += ` ${word}`;\n else {\n lines.push(current);\n current = word;\n }\n }\n if (current.length > 0) lines.push(current);\n return lines;\n}\n\n/** Drops YAML frontmatter so the preview starts at the prose, as Pi does. */\nexport function skillBody(content: string): string {\n const match = content.match(/^---\\r?\\n[\\s\\S]*?\\r?\\n---\\r?\\n?([\\s\\S]*)$/);\n return (match ? (match[1] ?? '') : content).replace(/^\\s+/, '');\n}\n\nfunction matches(skill: SkillEntry, query: string): boolean {\n const needle = query.toLowerCase();\n return skill.name.toLowerCase().includes(needle) || skill.description.toLowerCase().includes(needle);\n}\n\nexport class SkillsOverlayComponent extends DoomOverlay {\n private readonly options: SkillsOverlayOptions;\n private readonly done: (result: SkillsOverlayResult) => void;\n private readonly readFile: (filePath: string) => string;\n private readonly bodyCache = new Map<string, string[]>();\n\n /** Index into the skill rows, not into every row: headings are not selectable. */\n private selected = 0;\n private focus: 'tree' | 'body' = 'tree';\n private filtering = false;\n private query = '';\n private bodyOffset = 0;\n\n constructor(\n tui: DoomOverlayTui,\n theme: Theme,\n options: SkillsOverlayOptions,\n done: (result: SkillsOverlayResult) => void,\n ) {\n super(tui, theme);\n this.options = options;\n this.done = done;\n this.readFile = options.readFile ?? ((filePath) => fs.readFileSync(filePath, 'utf8'));\n }\n\n /** Every row, honouring the active filter. The tree does not fold. */\n private rows(): TreeRow[] {\n const query = this.query.trim();\n const rows: TreeRow[] = [];\n for (const group of this.options.catalog.groups) {\n const owners = query\n ? group.owners\n .map((owner) => ({ ...owner, skills: owner.skills.filter((skill) => matches(skill, query)) }))\n .filter((owner) => owner.skills.length > 0)\n : group.owners;\n if (owners.length === 0) continue;\n const count = owners.reduce((total, owner) => total + owner.skills.length, 0);\n rows.push({ kind: 'group', depth: 0, label: group.label, tag: String(count) });\n for (const owner of owners) {\n rows.push({ kind: 'owner', depth: 1, label: owner.owner, tag: String(owner.skills.length) });\n for (const skill of owner.skills) {\n rows.push({ kind: 'skill', depth: 2, label: skill.name, tag: '', skill });\n }\n }\n }\n return rows;\n }\n\n /**\n * Where the skill rows sit in `rows()`.\n *\n * The selection indexes this list rather than `rows()`, which is what keeps\n * headings unselectable without the key handler having to hunt past them, and\n * what puts the opening selection on a skill rather than on the first group.\n */\n private selectable(rows: TreeRow[]): number[] {\n const indexes: number[] = [];\n for (const [index, row] of rows.entries()) if (row.skill) indexes.push(index);\n return indexes;\n }\n\n private currentRow(): TreeRow | undefined {\n const rows = this.rows();\n const indexes = this.selectable(rows);\n if (indexes.length === 0) return undefined;\n return rows[indexes[Math.min(this.selected, indexes.length - 1)] as number];\n }\n\n private move(delta: number): void {\n const count = this.selectable(this.rows()).length;\n if (count === 0) return;\n this.selected = Math.max(0, Math.min(count - 1, this.selected + delta));\n this.bodyOffset = 0;\n this.invalidate();\n }\n\n private bodyLines(skill: SkillEntry, width: number): string[] {\n let raw = this.bodyCache.get(skill.filePath);\n if (!raw) {\n let source: string;\n try {\n source = skillBody(this.readFile(skill.filePath));\n } catch {\n source = UNREADABLE;\n }\n raw = source.split(/\\r?\\n/);\n this.bodyCache.set(skill.filePath, raw);\n }\n // Wrapped at render width rather than at read time, so a resize reflows the\n // same cached file instead of re-reading it.\n return raw.flatMap((line) => (line.trim() === '' ? [''] : wrap(line, width)));\n }\n\n handleInput(data: string): void {\n if (this.filtering) {\n this.handleFilterInput(data);\n return;\n }\n\n if (matchesKey(data, 'escape')) {\n // The filter clears before the overlay closes: escaping straight out of a\n // narrowed tree loses the search with no way to see what it hid.\n if (this.query) {\n this.query = '';\n this.selected = 0;\n this.invalidate();\n return;\n }\n this.done(undefined);\n return;\n }\n if (data === KEY_TAB || matchesKey(data, 'tab')) {\n this.focus = this.focus === 'tree' ? 'body' : 'tree';\n this.invalidate();\n return;\n }\n if (matchesKey(data, 'pageUp')) {\n this.bodyOffset = Math.max(0, this.bodyOffset - BODY_PAGE);\n this.invalidate();\n return;\n }\n if (matchesKey(data, 'pageDown')) {\n this.bodyOffset += BODY_PAGE;\n this.invalidate();\n return;\n }\n if (this.focus === 'body') return;\n\n if (data === FILTER_KEY) {\n this.filtering = true;\n this.invalidate();\n return;\n }\n if (matchesKey(data, 'up') || matchesKey(data, 'k')) {\n this.move(-1);\n return;\n }\n if (matchesKey(data, 'down') || matchesKey(data, 'j')) {\n this.move(1);\n return;\n }\n\n if (data === KEY_ENTER || data === KEY_NEWLINE || matchesKey(data, 'enter')) {\n const skill = this.currentRow()?.skill;\n if (skill) this.done({ kind: 'invoke', skill });\n }\n }\n\n private handleFilterInput(data: string): void {\n if (matchesKey(data, 'escape')) {\n this.filtering = false;\n this.query = '';\n this.selected = 0;\n this.invalidate();\n return;\n }\n if (data === KEY_ENTER || data === KEY_NEWLINE) {\n this.filtering = false;\n this.invalidate();\n return;\n }\n if (data === KEY_BACKSPACE) this.query = this.query.slice(0, -1);\n // Control sequences are dropped rather than appended: an arrow key would\n // otherwise land in the query as a raw escape sequence.\n else if (data.length === 1 && data >= ' ') this.query += data;\n this.selected = 0;\n this.invalidate();\n }\n\n protected getChrome(): DoomOverlayChrome {\n const { skillCount, promptTokens, bodyTokens } = this.options.catalog;\n const sources = this.options.catalog.groups.filter((group) => group.owners.length > 0).length;\n const hints = this.filtering ? FILTER_HINTS : this.focus === 'body' ? BODY_HINTS : TREE_HINTS;\n return {\n title: TITLE,\n accent: DOOM_OVERLAY_ACCENT,\n breadcrumb: BREADCRUMB,\n // Two token figures, because they answer different questions: what the\n // catalogue costs on every request, and what reading all of it would cost.\n headerRight: `${skillCount} skills · ${sources} sources · ${formatTokens(promptTokens)} always-on · ${formatTokens(bodyTokens)} full`,\n footer: this.hintLine(hints),\n };\n }\n\n private badge(text: string, background: ThemeBg, colour: ThemeColor = 'text'): string {\n return this.theme.bg(background, this.theme.fg(colour, ` ${text} `));\n }\n\n /** Footer hints as capped keys: `[ enter ] use skill`, per the mockup. */\n private hintLine(hints: readonly (readonly [string, string])[]): string {\n return hints\n .map(([key, label]) => `${this.badge(key, 'selectedBg')}${this.theme.fg('dim', ` ${label}`)}`)\n .join(this.theme.fg('dim', ' '));\n }\n\n protected renderBody(width: number, height: number): string[] {\n if (width < MIN_WIDTH) return this.renderStacked(width, height);\n\n const treeWidth = Math.max(MIN_PANE_WIDTH, Math.floor((width - 1) * TREE_PANE_RATIO));\n const detailWidth = Math.max(1, width - treeWidth - 1);\n const treeContent = Math.max(1, treeWidth - PANE_GUTTER);\n const detailContent = Math.max(1, detailWidth - PANE_GUTTER);\n\n const left = this.treeLines(treeContent, height);\n const right = this.detailLines(detailContent, height);\n const divider = this.theme.fg('borderMuted', '│');\n return Array.from({ length: height }, (_, index) => {\n const row = `${fitRow(left[index] ?? '', treeContent)} ${divider} ${fitRow(right[index] ?? '', detailContent)}`;\n return truncateToWidth(row, width, ELLIPSIS);\n });\n }\n\n private renderStacked(width: number, height: number): string[] {\n const topHeight = Math.max(1, Math.floor((height - 1) / 2));\n const bottomHeight = Math.max(0, height - topHeight - 1);\n const top = this.treeLines(width, topHeight);\n const bottom = this.detailLines(width, bottomHeight);\n return [\n ...top.slice(0, topHeight),\n ...Array.from({ length: Math.max(0, topHeight - top.length) }, () => ''),\n this.theme.fg('borderMuted', RULE.repeat(width)),\n ...bottom.slice(0, bottomHeight),\n ];\n }\n\n private treeLines(width: number, height: number): string[] {\n const heading = this.filtering || this.query ? `${HEADING} ${FILTER_KEY}${this.query}` : HEADING;\n const lines = [this.theme.bold(this.theme.fg('accent', heading)), ''];\n const rows = this.rows();\n if (rows.length === 0) {\n lines.push(this.theme.fg('dim', EMPTY_TREE));\n return lines;\n }\n\n const budget = Math.max(1, height - lines.length);\n const indexes = this.selectable(rows);\n // No skill rows means the filter matched nothing, so nothing is highlighted.\n const selected = indexes[Math.min(this.selected, indexes.length - 1)] ?? -1;\n const start = Math.max(0, Math.min(selected - budget + 1, Math.max(0, rows.length - budget)));\n for (const [offset, row] of rows.slice(start, start + budget).entries()) {\n const isSelected = start + offset === selected;\n const marker = isSelected ? this.theme.fg('accent', SELECTION_MARKER) : ' ';\n const indent = ' '.repeat(row.depth * INDENT);\n // Three tiers rather than two: the group heading leads, the owner beneath\n // it recedes to a caption, and the skills it holds carry the reading\n // weight because they are the only rows that do anything.\n const colour: ThemeColor = row.kind === 'group' ? 'accent' : row.kind === 'owner' ? 'dim' : 'text';\n let label = this.theme.fg(colour, `${indent}${row.label}`);\n if (row.kind === 'group' || isSelected) label = this.theme.bold(label);\n // The tag is capped before it claims columns: an owner in many domains\n // carries a long `+a,b,c` hint that would otherwise leave the name with\n // no room at all and render the row as a bare count.\n const tag = row.tag ? truncateToWidth(row.tag, Math.floor(width * TAG_SHARE), ELLIPSIS) : '';\n const tagWidth = tag ? visibleWidth(tag) + 1 : 0;\n const body = fitRow(`${marker} ${label}`, Math.max(1, width - tagWidth));\n const line = tag ? `${body} ${this.theme.fg('dim', tag)}` : body;\n lines.push(isSelected ? this.theme.bg('selectedBg', fitRow(line, width)) : line);\n }\n return lines;\n }\n\n private field(label: string, value: string, colour: ThemeColor, width: number): string[] {\n const valueWidth = Math.max(1, width - LABEL_COLUMN);\n return wrap(value, valueWidth).map(\n (line, index) =>\n `${this.theme.fg('dim', fitRow(index === 0 ? label : '', LABEL_COLUMN))}${this.theme.fg(colour, line)}`,\n );\n }\n\n private detailLines(width: number, height: number): string[] {\n const skill = this.currentRow()?.skill;\n if (!skill) return ['', '', this.theme.fg('dim', NO_SELECTION)];\n\n const relative = path.relative(this.options.repoRoot, skill.filePath) || skill.filePath;\n const lines = [\n this.theme.bold(this.theme.fg('accent', skill.name)),\n '',\n ...this.field('description', skill.description, 'text', width),\n ...this.field('source', `${skill.group} › ${skill.owner}`, 'success', width),\n ...this.field('path', relative, 'text', width),\n ...this.field(\n 'invocable',\n skill.modelInvocable ? `model · /skill:${skill.name}` : `/skill:${skill.name} only`,\n 'success',\n width,\n ),\n ];\n\n const body = this.bodyLines(skill, width);\n const remaining = Math.max(0, height - lines.length - 2);\n // Clamped here rather than in the key handler, because how far the body can\n // scroll depends on the width it has just wrapped to.\n this.bodyOffset = Math.min(this.bodyOffset, Math.max(0, body.length - remaining));\n const shown = body.slice(this.bodyOffset, this.bodyOffset + remaining);\n const position =\n body.length > remaining\n ? `lines ${this.bodyOffset + 1}-${this.bodyOffset + shown.length} / ${body.length}`\n : 'SKILL.md';\n const focus = this.focus === 'body' ? this.theme.fg('accent', ' ◀ focused') : '';\n\n lines.push('', `${this.theme.fg('dim', position)}${focus}`);\n for (const line of shown) lines.push(this.theme.fg('dim', line));\n return lines;\n }\n}\n\nexport async function openSkillsOverlay(\n ctx: ExtensionContext,\n options: SkillsOverlayOptions,\n): Promise<SkillsOverlayResult> {\n return ctx.ui.custom<SkillsOverlayResult>(\n (tui, theme, _keybindings, done) => new SkillsOverlayComponent(tui, theme, options, done),\n DOOM_FULLSCREEN_UI_OPTIONS,\n );\n}\n"],"mappings":"8RA+BA,MAsBM,EAAU,UAKV,EAAqD,CACzD,CAAC,KAAM,SAAS,CAChB,CAAC,QAAS,YAAY,CACtB,CAAC,MAAO,cAAc,CACtB,CAAC,IAAK,SAAS,CACf,CAAC,MAAO,QAAQ,CACjB,CACK,EAAqD,CACzD,CAAC,YAAa,SAAS,CACvB,CAAC,MAAO,OAAO,CACf,CAAC,MAAO,QAAQ,CACjB,CACK,EAAuD,CAC3D,CAAC,OAAQ,YAAY,CACrB,CAAC,QAAS,OAAO,CACjB,CAAC,MAAO,QAAQ,CACjB,CAsBD,SAAS,EAAO,EAAc,EAAuB,CACnD,OAAA,EAAA,EAAA,eAAqB,EAAM,EAAO,IAAS,CAG7C,SAAS,EAAK,EAAc,EAAyB,CACnD,GAAI,GAAS,EAAG,MAAO,EAAE,CACzB,IAAM,EAAkB,EAAE,CACtB,EAAU,GACd,IAAK,IAAM,KAAQ,EAAK,MAAM,MAAM,CAAC,OAAO,QAAQ,CAC9C,EAAQ,SAAW,EAAG,EAAU,qBACd,EAAQ,CAAG,GAAA,EAAA,EAAA,cAAiB,EAAK,EAAI,EAAO,GAAW,IAAI,KAE/E,EAAM,KAAK,EAAQ,CACnB,EAAU,GAId,OADI,EAAQ,OAAS,GAAG,EAAM,KAAK,EAAQ,CACpC,EAIT,SAAgB,EAAU,EAAyB,CACjD,IAAM,EAAQ,EAAQ,MAAM,4CAA4C,CACxE,OAAQ,EAAS,EAAM,IAAM,GAAM,GAAS,QAAQ,OAAQ,GAAG,CAGjE,SAAS,EAAQ,EAAmB,EAAwB,CAC1D,IAAM,EAAS,EAAM,aAAa,CAClC,OAAO,EAAM,KAAK,aAAa,CAAC,SAAS,EAAO,EAAI,EAAM,YAAY,aAAa,CAAC,SAAS,EAAO,CAGtG,IAAa,EAAb,cAA4CA,EAAAA,WAAY,CACtD,QACA,KACA,SACA,UAA6B,IAAI,IAGjC,SAAmB,EACnB,MAAiC,OACjC,UAAoB,GACpB,MAAgB,GAChB,WAAqB,EAErB,YACE,EACA,EACA,EACA,EACA,CACA,MAAM,EAAK,EAAM,CACjB,KAAK,QAAU,EACf,KAAK,KAAO,EACZ,KAAK,SAAW,EAAQ,WAAc,GAAaC,EAAAA,QAAG,aAAa,EAAU,OAAO,EAItF,MAA0B,CACxB,IAAM,EAAQ,KAAK,MAAM,MAAM,CACzB,EAAkB,EAAE,CAC1B,IAAK,IAAM,KAAS,KAAK,QAAQ,QAAQ,OAAQ,CAC/C,IAAM,EAAS,EACX,EAAM,OACH,IAAK,IAAW,CAAE,GAAG,EAAO,OAAQ,EAAM,OAAO,OAAQ,GAAU,EAAQ,EAAO,EAAM,CAAC,CAAE,EAAE,CAC7F,OAAQ,GAAU,EAAM,OAAO,OAAS,EAAE,CAC7C,EAAM,OACV,GAAI,EAAO,SAAW,EAAG,SACzB,IAAM,EAAQ,EAAO,QAAQ,EAAO,IAAU,EAAQ,EAAM,OAAO,OAAQ,EAAE,CAC7E,EAAK,KAAK,CAAE,KAAM,QAAS,MAAO,EAAG,MAAO,EAAM,MAAO,IAAK,OAAO,EAAM,CAAE,CAAC,CAC9E,IAAK,IAAM,KAAS,EAAQ,CAC1B,EAAK,KAAK,CAAE,KAAM,QAAS,MAAO,EAAG,MAAO,EAAM,MAAO,IAAK,OAAO,EAAM,OAAO,OAAO,CAAE,CAAC,CAC5F,IAAK,IAAM,KAAS,EAAM,OACxB,EAAK,KAAK,CAAE,KAAM,QAAS,MAAO,EAAG,MAAO,EAAM,KAAM,IAAK,GAAI,QAAO,CAAC,EAI/E,OAAO,EAUT,WAAmB,EAA2B,CAC5C,IAAM,EAAoB,EAAE,CAC5B,IAAK,GAAM,CAAC,EAAO,KAAQ,EAAK,SAAS,CAAM,EAAI,OAAO,EAAQ,KAAK,EAAM,CAC7E,OAAO,EAGT,YAA0C,CACxC,IAAM,EAAO,KAAK,MAAM,CAClB,EAAU,KAAK,WAAW,EAAK,CACjC,KAAQ,SAAW,EACvB,OAAO,EAAK,EAAQ,KAAK,IAAI,KAAK,SAAU,EAAQ,OAAS,EAAE,GAGjE,KAAa,EAAqB,CAChC,IAAM,EAAQ,KAAK,WAAW,KAAK,MAAM,CAAC,CAAC,OACvC,IAAU,IACd,KAAK,SAAW,KAAK,IAAI,EAAG,KAAK,IAAI,EAAQ,EAAG,KAAK,SAAW,EAAM,CAAC,CACvE,KAAK,WAAa,EAClB,KAAK,YAAY,EAGnB,UAAkB,EAAmB,EAAyB,CAC5D,IAAI,EAAM,KAAK,UAAU,IAAI,EAAM,SAAS,CAC5C,GAAI,CAAC,EAAK,CACR,IAAI,EACJ,GAAI,CACF,EAAS,EAAU,KAAK,SAAS,EAAM,SAAS,CAAC,MAC3C,CACN,EAAS,2BAEX,EAAM,EAAO,MAAM,QAAQ,CAC3B,KAAK,UAAU,IAAI,EAAM,SAAU,EAAI,CAIzC,OAAO,EAAI,QAAS,GAAU,EAAK,MAAM,GAAK,GAAK,CAAC,GAAG,CAAG,EAAK,EAAM,EAAM,CAAE,CAG/E,YAAY,EAAoB,CAC9B,GAAI,KAAK,UAAW,CAClB,KAAK,kBAAkB,EAAK,CAC5B,OAGF,IAAA,EAAA,EAAA,YAAe,EAAM,SAAS,CAAE,CAG9B,GAAI,KAAK,MAAO,CACd,KAAK,MAAQ,GACb,KAAK,SAAW,EAChB,KAAK,YAAY,CACjB,OAEF,KAAK,KAAK,IAAA,GAAU,CACpB,OAEF,GAAI,IAAS,MAAA,EAAA,EAAA,YAAsB,EAAM,MAAM,CAAE,CAC/C,KAAK,MAAQ,KAAK,QAAU,OAAS,OAAS,OAC9C,KAAK,YAAY,CACjB,OAEF,IAAA,EAAA,EAAA,YAAe,EAAM,SAAS,CAAE,CAC9B,KAAK,WAAa,KAAK,IAAI,EAAG,KAAK,WAAa,GAAU,CAC1D,KAAK,YAAY,CACjB,OAEF,IAAA,EAAA,EAAA,YAAe,EAAM,WAAW,CAAE,CAChC,KAAK,YAAc,GACnB,KAAK,YAAY,CACjB,OAEE,QAAK,QAAU,OAEnB,IAAI,IAAS,IAAY,CACvB,KAAK,UAAY,GACjB,KAAK,YAAY,CACjB,OAEF,IAAA,EAAA,EAAA,YAAe,EAAM,KAAK,GAAA,EAAA,EAAA,YAAe,EAAM,IAAI,CAAE,CACnD,KAAK,KAAK,GAAG,CACb,OAEF,IAAA,EAAA,EAAA,YAAe,EAAM,OAAO,GAAA,EAAA,EAAA,YAAe,EAAM,IAAI,CAAE,CACrD,KAAK,KAAK,EAAE,CACZ,OAGF,GAAI,IAAS,MAAa,IAAS;IAAA,EAAA,EAAA,YAA0B,EAAM,QAAQ,CAAE,CAC3E,IAAM,EAAQ,KAAK,YAAY,EAAE,MAC7B,GAAO,KAAK,KAAK,CAAE,KAAM,SAAU,QAAO,CAAC,GAInD,kBAA0B,EAAoB,CAC5C,IAAA,EAAA,EAAA,YAAe,EAAM,SAAS,CAAE,CAC9B,KAAK,UAAY,GACjB,KAAK,MAAQ,GACb,KAAK,SAAW,EAChB,KAAK,YAAY,CACjB,OAEF,GAAI,IAAS,MAAa,IAAS;EAAa,CAC9C,KAAK,UAAY,GACjB,KAAK,YAAY,CACjB,OAEE,IAAS,IAAe,KAAK,MAAQ,KAAK,MAAM,MAAM,EAAG,GAAG,CAGvD,EAAK,SAAW,GAAK,GAAQ,MAAK,KAAK,OAAS,GACzD,KAAK,SAAW,EAChB,KAAK,YAAY,CAGnB,WAAyC,CACvC,GAAM,CAAE,aAAY,eAAc,cAAe,KAAK,QAAQ,QACxD,EAAU,KAAK,QAAQ,QAAQ,OAAO,OAAQ,GAAU,EAAM,OAAO,OAAS,EAAE,CAAC,OACjF,EAAQ,KAAK,UAAY,EAAe,KAAK,QAAU,OAAS,EAAa,EACnF,MAAO,CACL,MAAO,SACP,OAAQC,EAAAA,oBACR,WAAY,mCAGZ,YAAa,GAAG,EAAW,YAAY,EAAQ,cAAA,EAAA,EAAA,cAA0B,EAAa,CAAC,gBAAA,EAAA,EAAA,cAA4B,EAAW,CAAC,OAC/H,OAAQ,KAAK,SAAS,EAAM,CAC7B,CAGH,MAAc,EAAc,EAAqB,EAAqB,OAAgB,CACpF,OAAO,KAAK,MAAM,GAAG,EAAY,KAAK,MAAM,GAAG,EAAQ,IAAI,EAAK,GAAG,CAAC,CAItE,SAAiB,EAAuD,CACtE,OAAO,EACJ,KAAK,CAAC,EAAK,KAAW,GAAG,KAAK,MAAM,EAAK,aAAa,GAAG,KAAK,MAAM,GAAG,MAAO,IAAI,IAAQ,GAAG,CAC7F,KAAK,KAAK,MAAM,GAAG,MAAO,MAAM,CAAC,CAGtC,WAAqB,EAAe,EAA0B,CAC5D,GAAI,EAAQ,GAAW,OAAO,KAAK,cAAc,EAAO,EAAO,CAE/D,IAAM,EAAY,KAAK,IAAI,GAAgB,KAAK,OAAO,EAAQ,GAAK,kBAAgB,CAAC,CAC/E,EAAc,KAAK,IAAI,EAAG,EAAQ,EAAY,EAAE,CAChD,EAAc,KAAK,IAAI,EAAG,EAAY,EAAY,CAClD,EAAgB,KAAK,IAAI,EAAG,EAAc,EAAY,CAEtD,EAAO,KAAK,UAAU,EAAa,EAAO,CAC1C,EAAQ,KAAK,YAAY,EAAe,EAAO,CAC/C,EAAU,KAAK,MAAM,GAAG,cAAe,IAAI,CACjD,OAAO,MAAM,KAAK,CAAE,OAAQ,EAAQ,EAAG,EAAG,KAExC,EAAA,EAAA,iBAAuB,GADR,EAAO,EAAK,IAAU,GAAI,EAAY,CAAC,GAAG,EAAQ,GAAG,EAAO,EAAM,IAAU,GAAI,EAAc,GACjF,EAAO,IAAS,CAC5C,CAGJ,cAAsB,EAAe,EAA0B,CAC7D,IAAM,EAAY,KAAK,IAAI,EAAG,KAAK,OAAO,EAAS,GAAK,EAAE,CAAC,CACrD,EAAe,KAAK,IAAI,EAAG,EAAS,EAAY,EAAE,CAClD,EAAM,KAAK,UAAU,EAAO,EAAU,CACtC,EAAS,KAAK,YAAY,EAAO,EAAa,CACpD,MAAO,CACL,GAAG,EAAI,MAAM,EAAG,EAAU,CAC1B,GAAG,MAAM,KAAK,CAAE,OAAQ,KAAK,IAAI,EAAG,EAAY,EAAI,OAAO,CAAE,KAAQ,GAAG,CACxE,KAAK,MAAM,GAAG,cAAe,IAAK,OAAO,EAAM,CAAC,CAChD,GAAG,EAAO,MAAM,EAAG,EAAa,CACjC,CAGH,UAAkB,EAAe,EAA0B,CACzD,IAAM,EAAU,KAAK,WAAa,KAAK,MAAQ,GAAG,EAAQ,KAAiB,KAAK,QAAU,EACpF,EAAQ,CAAC,KAAK,MAAM,KAAK,KAAK,MAAM,GAAG,SAAU,EAAQ,CAAC,CAAE,GAAG,CAC/D,EAAO,KAAK,MAAM,CACxB,GAAI,EAAK,SAAW,EAElB,OADA,EAAM,KAAK,KAAK,MAAM,GAAG,MAAO,wBAAW,CAAC,CACrC,EAGT,IAAM,EAAS,KAAK,IAAI,EAAG,EAAS,EAAM,OAAO,CAC3C,EAAU,KAAK,WAAW,EAAK,CAE/B,EAAW,EAAQ,KAAK,IAAI,KAAK,SAAU,EAAQ,OAAS,EAAE,GAAK,GACnE,EAAQ,KAAK,IAAI,EAAG,KAAK,IAAI,EAAW,EAAS,EAAG,KAAK,IAAI,EAAG,EAAK,OAAS,EAAO,CAAC,CAAC,CAC7F,IAAK,GAAM,CAAC,EAAQ,KAAQ,EAAK,MAAM,EAAO,EAAQ,EAAO,CAAC,SAAS,CAAE,CACvE,IAAM,EAAa,EAAQ,IAAW,EAChC,EAAS,EAAa,KAAK,MAAM,GAAG,SAAU,IAAiB,CAAG,IAClE,EAAS,IAAI,OAAO,EAAI,MAAQ,EAAO,CAIvC,EAAqB,EAAI,OAAS,QAAU,SAAW,EAAI,OAAS,QAAU,MAAQ,OACxF,EAAQ,KAAK,MAAM,GAAG,EAAQ,GAAG,IAAS,EAAI,QAAQ,EACtD,EAAI,OAAS,SAAW,KAAY,EAAQ,KAAK,MAAM,KAAK,EAAM,EAItE,IAAM,EAAM,EAAI,KAAA,EAAA,EAAA,iBAAsB,EAAI,IAAK,KAAK,MAAM,EAAQ,GAAU,CAAE,IAAS,CAAG,GACpF,EAAW,GAAA,EAAA,EAAA,cAAmB,EAAI,CAAG,EAAI,EACzC,EAAO,EAAO,GAAG,EAAO,GAAG,IAAS,KAAK,IAAI,EAAG,EAAQ,EAAS,CAAC,CAClE,EAAO,EAAM,GAAG,EAAK,GAAG,KAAK,MAAM,GAAG,MAAO,EAAI,GAAK,EAC5D,EAAM,KAAK,EAAa,KAAK,MAAM,GAAG,aAAc,EAAO,EAAM,EAAM,CAAC,CAAG,EAAK,CAElF,OAAO,EAGT,MAAc,EAAe,EAAe,EAAoB,EAAyB,CAEvF,OAAO,EAAK,EADO,KAAK,IAAI,EAAG,EAAQ,GACV,CAAC,CAAC,KAC5B,EAAM,IACL,GAAG,KAAK,MAAM,GAAG,MAAO,EAAO,IAAU,EAAI,EAAQ,GAAI,GAAa,CAAC,GAAG,KAAK,MAAM,GAAG,EAAQ,EAAK,GACxG,CAGH,YAAoB,EAAe,EAA0B,CAC3D,IAAM,EAAQ,KAAK,YAAY,EAAE,MACjC,GAAI,CAAC,EAAO,MAAO,CAAC,GAAI,GAAI,KAAK,MAAM,GAAG,MAAO,6BAAa,CAAC,CAE/D,IAAM,EAAWC,EAAAA,QAAK,SAAS,KAAK,QAAQ,SAAU,EAAM,SAAS,EAAI,EAAM,SACzE,EAAQ,CACZ,KAAK,MAAM,KAAK,KAAK,MAAM,GAAG,SAAU,EAAM,KAAK,CAAC,CACpD,GACA,GAAG,KAAK,MAAM,cAAe,EAAM,YAAa,OAAQ,EAAM,CAC9D,GAAG,KAAK,MAAM,SAAU,GAAG,EAAM,MAAM,KAAK,EAAM,QAAS,UAAW,EAAM,CAC5E,GAAG,KAAK,MAAM,OAAQ,EAAU,OAAQ,EAAM,CAC9C,GAAG,KAAK,MACN,YACA,EAAM,eAAiB,oBAAoB,EAAM,OAAS,UAAU,EAAM,KAAK,OAC/E,UACA,EACD,CACF,CAEK,EAAO,KAAK,UAAU,EAAO,EAAM,CACnC,EAAY,KAAK,IAAI,EAAG,EAAS,EAAM,OAAS,EAAE,CAGxD,KAAK,WAAa,KAAK,IAAI,KAAK,WAAY,KAAK,IAAI,EAAG,EAAK,OAAS,EAAU,CAAC,CACjF,IAAM,EAAQ,EAAK,MAAM,KAAK,WAAY,KAAK,WAAa,EAAU,CAChE,EACJ,EAAK,OAAS,EACV,SAAS,KAAK,WAAa,EAAE,GAAG,KAAK,WAAa,EAAM,OAAO,KAAK,EAAK,SACzE,WACA,EAAQ,KAAK,QAAU,OAAS,KAAK,MAAM,GAAG,SAAU,aAAa,CAAG,GAE9E,EAAM,KAAK,GAAI,GAAG,KAAK,MAAM,GAAG,MAAO,EAAS,GAAG,IAAQ,CAC3D,IAAK,IAAM,KAAQ,EAAO,EAAM,KAAK,KAAK,MAAM,GAAG,MAAO,EAAK,CAAC,CAChE,OAAO,IAIX,eAAsB,EACpB,EACA,EAC8B,CAC9B,OAAO,EAAI,GAAG,QACX,EAAK,EAAO,EAAc,IAAS,IAAI,EAAuB,EAAK,EAAO,EAAS,EAAK,CACzFC,EAAAA,2BACD"}
@@ -1 +1 @@
1
- {"version":3,"file":"skillsOverlay.d.cts","names":[],"sources":["../../../../src/extensions/entries/components/skillsOverlay.ts"],"mappings":";;;;;KA4EY,mBAAA;EAAwB,IAAA;EAAgB,KAAA,EAAO,UAAA;AAAA;AAAA,UAE1C,oBAAA;EACf,OAAA,EAAS,YAAA;EAGG;EADZ,QAAA;EACA,QAAA,IAAY,QAAA;AAAA;;iBAoCE,SAAA,CAAU,OAAA;AAAA,cAUb,sBAAA,SAA+B,WAAA;EAAA,iBACzB,OAAA;EAAA,iBACA,IAAA;EAAA,iBACA,QAAA;EAAA,iBACA,SAAA;EAUV;EAAA,QAPC,QAAA;EAAA,QACA,KAAA;EAAA,QACA,SAAA;EAAA,QACA,KAAA;EAAA,QACA,UAAA;cAGN,GAAA,EAAK,cAAA,EACL,KAAA,EAAO,KAAA,EACP,OAAA,EAAS,oBAAA,EACT,IAAA,GAAO,MAAA,EAAQ,mBAAA;EAjBoC;EAAA,QA0B7C,IAAA;EAzBS;;;;;;;EAAA,QAsDT,UAAA;EAAA,QAMA,UAAA;EAAA,QAOA,IAAA;EAAA,QAQA,SAAA;EAiBR,WAAA,CAAY,IAAA;EAAA,QA8CJ,iBAAA;EAAA,UAqBE,SAAA,CAAA,GAAa,iBAAA;EAAA,QAef,KAAA;EA/JN;EAAA,QAoKM,QAAA;EAAA,UAME,UAAA,CAAW,KAAA,UAAe,MAAA;EAAA,QAiB5B,aAAA;EAAA,QAaA,SAAA;EAAA,QAoCA,KAAA;EAAA,QAQA,WAAA;AAAA;AAAA,iBAqCY,iBAAA,CACpB,GAAA,EAAK,gBAAA,EACL,OAAA,EAAS,oBAAA,GACR,OAAA,CAAQ,mBAAA"}
1
+ {"version":3,"file":"skillsOverlay.d.cts","names":[],"sources":["../../../../src/extensions/entries/components/skillsOverlay.ts"],"mappings":";;;;;KA4EY,mBAAA;EAAwB,IAAA;EAAgB,KAAA,EAAO,UAAA;AAAA;AAAA,UAE1C,oBAAA;EACf,OAAA,EAAS,YAAA;EAGG;EADZ,QAAA;EACA,QAAA,IAAY,QAAA;AAAA;;iBAmCE,SAAA,CAAU,OAAA;AAAA,cAUb,sBAAA,SAA+B,WAAA;EAAA,iBACzB,OAAA;EAAA,iBACA,IAAA;EAAA,iBACA,QAAA;EAAA,iBACA,SAAA;EAUV;EAAA,QAPC,QAAA;EAAA,QACA,KAAA;EAAA,QACA,SAAA;EAAA,QACA,KAAA;EAAA,QACA,UAAA;cAGN,GAAA,EAAK,cAAA,EACL,KAAA,EAAO,KAAA,EACP,OAAA,EAAS,oBAAA,EACT,IAAA,GAAO,MAAA,EAAQ,mBAAA;EAjBoC;EAAA,QA0B7C,IAAA;EAzBS;;;;;;;EAAA,QAsDT,UAAA;EAAA,QAMA,UAAA;EAAA,QAOA,IAAA;EAAA,QAQA,SAAA;EAiBR,WAAA,CAAY,IAAA;EAAA,QAuDJ,iBAAA;EAAA,UAqBE,SAAA,CAAA,GAAa,iBAAA;EAAA,QAef,KAAA;EAxKN;EAAA,QA6KM,QAAA;EAAA,UAME,UAAA,CAAW,KAAA,UAAe,MAAA;EAAA,QAiB5B,aAAA;EAAA,QAaA,SAAA;EAAA,QAoCA,KAAA;EAAA,QAQA,WAAA;AAAA;AAAA,iBAqCY,iBAAA,CACpB,GAAA,EAAK,gBAAA,EACL,OAAA,EAAS,oBAAA,GACR,OAAA,CAAQ,mBAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"skillsOverlay.d.mts","names":[],"sources":["../../../../src/extensions/entries/components/skillsOverlay.ts"],"mappings":";;;;;KA4EY,mBAAA;EAAwB,IAAA;EAAgB,KAAA,EAAO,UAAA;AAAA;AAAA,UAE1C,oBAAA;EACf,OAAA,EAAS,YAAA;EAGG;EADZ,QAAA;EACA,QAAA,IAAY,QAAA;AAAA;;iBAoCE,SAAA,CAAU,OAAA;AAAA,cAUb,sBAAA,SAA+B,WAAA;EAAA,iBACzB,OAAA;EAAA,iBACA,IAAA;EAAA,iBACA,QAAA;EAAA,iBACA,SAAA;EAUV;EAAA,QAPC,QAAA;EAAA,QACA,KAAA;EAAA,QACA,SAAA;EAAA,QACA,KAAA;EAAA,QACA,UAAA;cAGN,GAAA,EAAK,cAAA,EACL,KAAA,EAAO,KAAA,EACP,OAAA,EAAS,oBAAA,EACT,IAAA,GAAO,MAAA,EAAQ,mBAAA;EAjBoC;EAAA,QA0B7C,IAAA;EAzBS;;;;;;;EAAA,QAsDT,UAAA;EAAA,QAMA,UAAA;EAAA,QAOA,IAAA;EAAA,QAQA,SAAA;EAiBR,WAAA,CAAY,IAAA;EAAA,QA8CJ,iBAAA;EAAA,UAqBE,SAAA,CAAA,GAAa,iBAAA;EAAA,QAef,KAAA;EA/JN;EAAA,QAoKM,QAAA;EAAA,UAME,UAAA,CAAW,KAAA,UAAe,MAAA;EAAA,QAiB5B,aAAA;EAAA,QAaA,SAAA;EAAA,QAoCA,KAAA;EAAA,QAQA,WAAA;AAAA;AAAA,iBAqCY,iBAAA,CACpB,GAAA,EAAK,gBAAA,EACL,OAAA,EAAS,oBAAA,GACR,OAAA,CAAQ,mBAAA"}
1
+ {"version":3,"file":"skillsOverlay.d.mts","names":[],"sources":["../../../../src/extensions/entries/components/skillsOverlay.ts"],"mappings":";;;;;KA4EY,mBAAA;EAAwB,IAAA;EAAgB,KAAA,EAAO,UAAA;AAAA;AAAA,UAE1C,oBAAA;EACf,OAAA,EAAS,YAAA;EAGG;EADZ,QAAA;EACA,QAAA,IAAY,QAAA;AAAA;;iBAmCE,SAAA,CAAU,OAAA;AAAA,cAUb,sBAAA,SAA+B,WAAA;EAAA,iBACzB,OAAA;EAAA,iBACA,IAAA;EAAA,iBACA,QAAA;EAAA,iBACA,SAAA;EAUV;EAAA,QAPC,QAAA;EAAA,QACA,KAAA;EAAA,QACA,SAAA;EAAA,QACA,KAAA;EAAA,QACA,UAAA;cAGN,GAAA,EAAK,cAAA,EACL,KAAA,EAAO,KAAA,EACP,OAAA,EAAS,oBAAA,EACT,IAAA,GAAO,MAAA,EAAQ,mBAAA;EAjBoC;EAAA,QA0B7C,IAAA;EAzBS;;;;;;;EAAA,QAsDT,UAAA;EAAA,QAMA,UAAA;EAAA,QAOA,IAAA;EAAA,QAQA,SAAA;EAiBR,WAAA,CAAY,IAAA;EAAA,QAuDJ,iBAAA;EAAA,UAqBE,SAAA,CAAA,GAAa,iBAAA;EAAA,QAef,KAAA;EAxKN;EAAA,QA6KM,QAAA;EAAA,UAME,UAAA,CAAW,KAAA,UAAe,MAAA;EAAA,QAiB5B,aAAA;EAAA,QAaA,SAAA;EAAA,QAoCA,KAAA;EAAA,QAQA,WAAA;AAAA;AAAA,iBAqCY,iBAAA,CACpB,GAAA,EAAK,gBAAA,EACL,OAAA,EAAS,oBAAA,GACR,OAAA,CAAQ,mBAAA"}
@@ -1,4 +1,4 @@
1
- import e from"node:fs";import t from"node:path";import{matchesKey as n,truncateToWidth as r,visibleWidth as i}from"@earendil-works/pi-tui";import{DOOM_FULLSCREEN_UI_OPTIONS as a,DOOM_OVERLAY_ACCENT as o,DoomOverlay as s}from"@agimon-ai/doompi-ui/components/doomOverlay";import{formatTokens as c}from"@agimon-ai/doompi-ui/rendering";const l=`CATALOG`,u=[[`↑↓`,`select`],[`enter`,`use skill`],[`tab`,`detail pane`],[`/`,`filter`],[`esc`,`close`]],d=[[`pgup/pgdn`,`scroll`],[`tab`,`tree`],[`esc`,`close`]],f=[[`type`,`to filter`],[`enter`,`keep`],[`esc`,`clear`]];function p(e,t){let n=r(e,Math.max(0,t),`…`);return n+` `.repeat(Math.max(0,t-i(n)))}function m(e,t){if(t<=0)return[];let n=[],r=``;for(let a of e.split(/\s+/).filter(Boolean))r.length===0?r=a:i(r)+1+i(a)<=t?r+=` ${a}`:(n.push(r),r=a);return r.length>0&&n.push(r),n}function h(e){let t=e.match(/^---\r?\n[\s\S]*?\r?\n---\r?\n?([\s\S]*)$/);return(t?t[1]??``:e).replace(/^\s+/,``)}function g(e,t){let n=t.toLowerCase();return e.name.toLowerCase().includes(n)||e.description.toLowerCase().includes(n)}var _=class extends s{options;done;readFile;bodyCache=new Map;selected=0;focus=`tree`;filtering=!1;query=``;bodyOffset=0;constructor(t,n,r,i){super(t,n),this.options=r,this.done=i,this.readFile=r.readFile??(t=>e.readFileSync(t,`utf8`))}rows(){let e=this.query.trim(),t=[];for(let n of this.options.catalog.groups){let r=e?n.owners.map(t=>({...t,skills:t.skills.filter(t=>g(t,e))})).filter(e=>e.skills.length>0):n.owners;if(r.length===0)continue;let i=r.reduce((e,t)=>e+t.skills.length,0);t.push({kind:`group`,depth:0,label:n.label,tag:String(i)});for(let e of r){t.push({kind:`owner`,depth:1,label:e.owner,tag:String(e.skills.length)});for(let n of e.skills)t.push({kind:`skill`,depth:2,label:n.name,tag:``,skill:n})}}return t}selectable(e){let t=[];for(let[n,r]of e.entries())r.skill&&t.push(n);return t}currentRow(){let e=this.rows(),t=this.selectable(e);if(t.length!==0)return e[t[Math.min(this.selected,t.length-1)]]}move(e){let t=this.selectable(this.rows()).length;t!==0&&(this.selected=Math.max(0,Math.min(t-1,this.selected+e)),this.bodyOffset=0,this.invalidate())}bodyLines(e,t){let n=this.bodyCache.get(e.filePath);if(!n){let t;try{t=h(this.readFile(e.filePath))}catch{t=`Could not read SKILL.md.`}n=t.split(/\r?\n/),this.bodyCache.set(e.filePath,n)}return n.flatMap(e=>e.trim()===``?[``]:m(e,t))}handleInput(e){if(this.filtering)return this.handleFilterInput(e);if(n(e,`escape`)){if(this.query){this.query=``,this.selected=0,this.invalidate();return}this.done(void 0);return}if(e===` `||n(e,`tab`)){this.focus=this.focus===`tree`?`body`:`tree`,this.invalidate();return}if(n(e,`pageUp`)){this.bodyOffset=Math.max(0,this.bodyOffset-10),this.invalidate();return}if(n(e,`pageDown`)){this.bodyOffset+=10,this.invalidate();return}if(this.focus!==`body`){if(e===`/`){this.filtering=!0,this.invalidate();return}if(n(e,`up`)||n(e,`k`))return this.move(-1);if(n(e,`down`)||n(e,`j`))return this.move(1);if(e===`\r`||e===`
1
+ import e from"node:fs";import t from"node:path";import{matchesKey as n,truncateToWidth as r,visibleWidth as i}from"@earendil-works/pi-tui";import{DOOM_FULLSCREEN_UI_OPTIONS as a,DOOM_OVERLAY_ACCENT as o,DoomOverlay as s}from"@agimon-ai/doompi-ui/components/doomOverlay";import{fitStyledLine as c,formatTokens as l}from"@agimon-ai/doompi-ui/rendering";const u=`CATALOG`,d=[[`↑↓`,`select`],[`enter`,`use skill`],[`tab`,`detail pane`],[`/`,`filter`],[`esc`,`close`]],f=[[`pgup/pgdn`,`scroll`],[`tab`,`tree`],[`esc`,`close`]],p=[[`type`,`to filter`],[`enter`,`keep`],[`esc`,`clear`]];function m(e,t){return c(e,t,`…`)}function h(e,t){if(t<=0)return[];let n=[],r=``;for(let a of e.split(/\s+/).filter(Boolean))r.length===0?r=a:i(r)+1+i(a)<=t?r+=` ${a}`:(n.push(r),r=a);return r.length>0&&n.push(r),n}function g(e){let t=e.match(/^---\r?\n[\s\S]*?\r?\n---\r?\n?([\s\S]*)$/);return(t?t[1]??``:e).replace(/^\s+/,``)}function _(e,t){let n=t.toLowerCase();return e.name.toLowerCase().includes(n)||e.description.toLowerCase().includes(n)}var v=class extends s{options;done;readFile;bodyCache=new Map;selected=0;focus=`tree`;filtering=!1;query=``;bodyOffset=0;constructor(t,n,r,i){super(t,n),this.options=r,this.done=i,this.readFile=r.readFile??(t=>e.readFileSync(t,`utf8`))}rows(){let e=this.query.trim(),t=[];for(let n of this.options.catalog.groups){let r=e?n.owners.map(t=>({...t,skills:t.skills.filter(t=>_(t,e))})).filter(e=>e.skills.length>0):n.owners;if(r.length===0)continue;let i=r.reduce((e,t)=>e+t.skills.length,0);t.push({kind:`group`,depth:0,label:n.label,tag:String(i)});for(let e of r){t.push({kind:`owner`,depth:1,label:e.owner,tag:String(e.skills.length)});for(let n of e.skills)t.push({kind:`skill`,depth:2,label:n.name,tag:``,skill:n})}}return t}selectable(e){let t=[];for(let[n,r]of e.entries())r.skill&&t.push(n);return t}currentRow(){let e=this.rows(),t=this.selectable(e);if(t.length!==0)return e[t[Math.min(this.selected,t.length-1)]]}move(e){let t=this.selectable(this.rows()).length;t!==0&&(this.selected=Math.max(0,Math.min(t-1,this.selected+e)),this.bodyOffset=0,this.invalidate())}bodyLines(e,t){let n=this.bodyCache.get(e.filePath);if(!n){let t;try{t=g(this.readFile(e.filePath))}catch{t=`Could not read SKILL.md.`}n=t.split(/\r?\n/),this.bodyCache.set(e.filePath,n)}return n.flatMap(e=>e.trim()===``?[``]:h(e,t))}handleInput(e){if(this.filtering){this.handleFilterInput(e);return}if(n(e,`escape`)){if(this.query){this.query=``,this.selected=0,this.invalidate();return}this.done(void 0);return}if(e===` `||n(e,`tab`)){this.focus=this.focus===`tree`?`body`:`tree`,this.invalidate();return}if(n(e,`pageUp`)){this.bodyOffset=Math.max(0,this.bodyOffset-10),this.invalidate();return}if(n(e,`pageDown`)){this.bodyOffset+=10,this.invalidate();return}if(this.focus!==`body`){if(e===`/`){this.filtering=!0,this.invalidate();return}if(n(e,`up`)||n(e,`k`)){this.move(-1);return}if(n(e,`down`)||n(e,`j`)){this.move(1);return}if(e===`\r`||e===`
2
2
  `||n(e,`enter`)){let e=this.currentRow()?.skill;e&&this.done({kind:`invoke`,skill:e})}}}handleFilterInput(e){if(n(e,`escape`)){this.filtering=!1,this.query=``,this.selected=0,this.invalidate();return}if(e===`\r`||e===`
3
- `){this.filtering=!1,this.invalidate();return}e===``?this.query=this.query.slice(0,-1):e.length===1&&e>=` `&&(this.query+=e),this.selected=0,this.invalidate()}getChrome(){let{skillCount:e,promptTokens:t,bodyTokens:n}=this.options.catalog,r=this.options.catalog.groups.filter(e=>e.owners.length>0).length,i=this.filtering?f:this.focus===`body`?d:u;return{title:`SKILLS`,accent:o,breadcrumb:`SPC › e / extension › s / skills`,headerRight:`${e} skills · ${r} sources · ${c(t)} always-on · ${c(n)} full`,footer:this.hintLine(i)}}badge(e,t,n=`text`){return this.theme.bg(t,this.theme.fg(n,` ${e} `))}hintLine(e){return e.map(([e,t])=>`${this.badge(e,`selectedBg`)}${this.theme.fg(`dim`,` ${t}`)}`).join(this.theme.fg(`dim`,` `))}renderBody(e,t){if(e<60)return this.renderStacked(e,t);let n=Math.max(20,Math.floor((e-1)*.3333333333333333)),i=Math.max(1,e-n-1),a=Math.max(1,n-1),o=Math.max(1,i-1),s=this.treeLines(a,t),c=this.detailLines(o,t),l=this.theme.fg(`borderMuted`,`│`);return Array.from({length:t},(t,n)=>r(`${p(s[n]??``,a)} ${l} ${p(c[n]??``,o)}`,e,`…`))}renderStacked(e,t){let n=Math.max(1,Math.floor((t-1)/2)),r=Math.max(0,t-n-1),i=this.treeLines(e,n),a=this.detailLines(e,r);return[...i.slice(0,n),...Array.from({length:Math.max(0,n-i.length)},()=>``),this.theme.fg(`borderMuted`,`─`.repeat(e)),...a.slice(0,r)]}treeLines(e,t){let n=this.filtering||this.query?`${l} /${this.query}`:l,a=[this.theme.bold(this.theme.fg(`accent`,n)),``],o=this.rows();if(o.length===0)return a.push(this.theme.fg(`dim`,`No skills discovered.`)),a;let s=Math.max(1,t-a.length),c=this.selectable(o),u=c[Math.min(this.selected,c.length-1)]??-1,d=Math.max(0,Math.min(u-s+1,Math.max(0,o.length-s)));for(let[t,n]of o.slice(d,d+s).entries()){let o=d+t===u,s=o?this.theme.fg(`accent`,`›`):` `,c=` `.repeat(n.depth*2),l=n.kind===`group`?`accent`:n.kind===`owner`?`dim`:`text`,f=this.theme.fg(l,`${c}${n.label}`);(n.kind===`group`||o)&&(f=this.theme.bold(f));let m=n.tag?r(n.tag,Math.floor(e*.4),`…`):``,h=m?i(m)+1:0,g=p(`${s} ${f}`,Math.max(1,e-h)),_=m?`${g} ${this.theme.fg(`dim`,m)}`:g;a.push(o?this.theme.bg(`selectedBg`,p(_,e)):_)}return a}field(e,t,n,r){return m(t,Math.max(1,r-13)).map((t,r)=>`${this.theme.fg(`dim`,p(r===0?e:``,13))}${this.theme.fg(n,t)}`)}detailLines(e,n){let r=this.currentRow()?.skill;if(!r)return[``,``,this.theme.fg(`dim`,`Select a skill to read it.`)];let i=t.relative(this.options.repoRoot,r.filePath)||r.filePath,a=[this.theme.bold(this.theme.fg(`accent`,r.name)),``,...this.field(`description`,r.description,`text`,e),...this.field(`source`,`${r.group} › ${r.owner}`,`success`,e),...this.field(`path`,i,`text`,e),...this.field(`invocable`,r.modelInvocable?`model · /skill:${r.name}`:`/skill:${r.name} only`,`success`,e)],o=this.bodyLines(r,e),s=Math.max(0,n-a.length-2);this.bodyOffset=Math.min(this.bodyOffset,Math.max(0,o.length-s));let c=o.slice(this.bodyOffset,this.bodyOffset+s),l=o.length>s?`lines ${this.bodyOffset+1}-${this.bodyOffset+c.length} / ${o.length}`:`SKILL.md`,u=this.focus===`body`?this.theme.fg(`accent`,` ◀ focused`):``;a.push(``,`${this.theme.fg(`dim`,l)}${u}`);for(let e of c)a.push(this.theme.fg(`dim`,e));return a}};async function v(e,t){return e.ui.custom((e,n,r,i)=>new _(e,n,t,i),a)}export{_ as SkillsOverlayComponent,v as openSkillsOverlay,h as skillBody};
3
+ `){this.filtering=!1,this.invalidate();return}e===``?this.query=this.query.slice(0,-1):e.length===1&&e>=` `&&(this.query+=e),this.selected=0,this.invalidate()}getChrome(){let{skillCount:e,promptTokens:t,bodyTokens:n}=this.options.catalog,r=this.options.catalog.groups.filter(e=>e.owners.length>0).length,i=this.filtering?p:this.focus===`body`?f:d;return{title:`SKILLS`,accent:o,breadcrumb:`SPC › e / extension › s / skills`,headerRight:`${e} skills · ${r} sources · ${l(t)} always-on · ${l(n)} full`,footer:this.hintLine(i)}}badge(e,t,n=`text`){return this.theme.bg(t,this.theme.fg(n,` ${e} `))}hintLine(e){return e.map(([e,t])=>`${this.badge(e,`selectedBg`)}${this.theme.fg(`dim`,` ${t}`)}`).join(this.theme.fg(`dim`,` `))}renderBody(e,t){if(e<60)return this.renderStacked(e,t);let n=Math.max(20,Math.floor((e-1)*.3333333333333333)),i=Math.max(1,e-n-1),a=Math.max(1,n-1),o=Math.max(1,i-1),s=this.treeLines(a,t),c=this.detailLines(o,t),l=this.theme.fg(`borderMuted`,`│`);return Array.from({length:t},(t,n)=>r(`${m(s[n]??``,a)} ${l} ${m(c[n]??``,o)}`,e,`…`))}renderStacked(e,t){let n=Math.max(1,Math.floor((t-1)/2)),r=Math.max(0,t-n-1),i=this.treeLines(e,n),a=this.detailLines(e,r);return[...i.slice(0,n),...Array.from({length:Math.max(0,n-i.length)},()=>``),this.theme.fg(`borderMuted`,`─`.repeat(e)),...a.slice(0,r)]}treeLines(e,t){let n=this.filtering||this.query?`${u} /${this.query}`:u,a=[this.theme.bold(this.theme.fg(`accent`,n)),``],o=this.rows();if(o.length===0)return a.push(this.theme.fg(`dim`,`No skills discovered.`)),a;let s=Math.max(1,t-a.length),c=this.selectable(o),l=c[Math.min(this.selected,c.length-1)]??-1,d=Math.max(0,Math.min(l-s+1,Math.max(0,o.length-s)));for(let[t,n]of o.slice(d,d+s).entries()){let o=d+t===l,s=o?this.theme.fg(`accent`,`›`):` `,c=` `.repeat(n.depth*2),u=n.kind===`group`?`accent`:n.kind===`owner`?`dim`:`text`,f=this.theme.fg(u,`${c}${n.label}`);(n.kind===`group`||o)&&(f=this.theme.bold(f));let p=n.tag?r(n.tag,Math.floor(e*.4),`…`):``,h=p?i(p)+1:0,g=m(`${s} ${f}`,Math.max(1,e-h)),_=p?`${g} ${this.theme.fg(`dim`,p)}`:g;a.push(o?this.theme.bg(`selectedBg`,m(_,e)):_)}return a}field(e,t,n,r){return h(t,Math.max(1,r-13)).map((t,r)=>`${this.theme.fg(`dim`,m(r===0?e:``,13))}${this.theme.fg(n,t)}`)}detailLines(e,n){let r=this.currentRow()?.skill;if(!r)return[``,``,this.theme.fg(`dim`,`Select a skill to read it.`)];let i=t.relative(this.options.repoRoot,r.filePath)||r.filePath,a=[this.theme.bold(this.theme.fg(`accent`,r.name)),``,...this.field(`description`,r.description,`text`,e),...this.field(`source`,`${r.group} › ${r.owner}`,`success`,e),...this.field(`path`,i,`text`,e),...this.field(`invocable`,r.modelInvocable?`model · /skill:${r.name}`:`/skill:${r.name} only`,`success`,e)],o=this.bodyLines(r,e),s=Math.max(0,n-a.length-2);this.bodyOffset=Math.min(this.bodyOffset,Math.max(0,o.length-s));let c=o.slice(this.bodyOffset,this.bodyOffset+s),l=o.length>s?`lines ${this.bodyOffset+1}-${this.bodyOffset+c.length} / ${o.length}`:`SKILL.md`,u=this.focus===`body`?this.theme.fg(`accent`,` ◀ focused`):``;a.push(``,`${this.theme.fg(`dim`,l)}${u}`);for(let e of c)a.push(this.theme.fg(`dim`,e));return a}};async function y(e,t){return e.ui.custom((e,n,r,i)=>new v(e,n,t,i),a)}export{v as SkillsOverlayComponent,y as openSkillsOverlay,g as skillBody};
4
4
  //# sourceMappingURL=skillsOverlay.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"skillsOverlay.mjs","names":[],"sources":["../../../../src/extensions/entries/components/skillsOverlay.ts"],"sourcesContent":["/**\n * Skills: the `SPC e s` overlay.\n *\n * A tree of the skills this session loaded on the left, the selected skill's\n * frontmatter and body on the right. Everything listed is loaded, so the\n * surface answers \"what do I have, and what does it cost\" rather than doubling\n * as a domain browser.\n *\n * The tree is flat and always open: groups and owners are headings, only skills\n * take the selection. Rendering is pure: the catalogue is resolved once by the\n * caller and passed in, so `render(width)` returns lines with no side effects\n * and the whole surface can be asserted as text without a live terminal.\n */\n\nimport fs from 'node:fs';\nimport path from 'node:path';\nimport {\n DOOM_FULLSCREEN_UI_OPTIONS,\n DOOM_OVERLAY_ACCENT,\n DoomOverlay,\n type DoomOverlayChrome,\n type DoomOverlayTui,\n} from '@agimon-ai/doompi-ui/components/doomOverlay';\nimport { formatTokens } from '@agimon-ai/doompi-ui/rendering';\nimport type { ExtensionContext, Theme } from '@earendil-works/pi-coding-agent';\nimport { matchesKey, truncateToWidth, visibleWidth } from '@earendil-works/pi-tui';\nimport type { SkillCatalog, SkillEntry } from '../../../adapters/skillCatalog.ts';\n\ntype ThemeColor = Parameters<Theme['fg']>[0];\ntype ThemeBg = Parameters<Theme['bg']>[0];\n\nconst MIN_WIDTH = 60;\n/** A third for the tree, two thirds for the skill being read. */\nconst TREE_PANE_RATIO = 1 / 3;\n/** One blank column each side of the pane divider so neither column touches it. */\nconst PANE_GUTTER = 1;\nconst MIN_PANE_WIDTH = 20;\nconst LABEL_COLUMN = 13;\n/** Ceiling on the columns a row's right-hand tag may take from its label. */\nconst TAG_SHARE = 0.4;\nconst INDENT = 2;\nconst ELLIPSIS = '…';\nconst SELECTION_MARKER = '›';\nconst RULE = '─';\nconst KEY_TAB = '\\t';\nconst KEY_ENTER = '\\r';\nconst KEY_NEWLINE = '\\n';\nconst KEY_BACKSPACE = '\\x7f';\nconst FILTER_KEY = '/';\nconst BODY_PAGE = 10;\n\nconst TITLE = 'SKILLS';\nconst BREADCRUMB = 'SPC › e / extension › s / skills';\nconst HEADING = 'CATALOG';\nconst EMPTY_TREE = 'No skills discovered.';\nconst NO_SELECTION = 'Select a skill to read it.';\nconst UNREADABLE = 'Could not read SKILL.md.';\n\nconst TREE_HINTS: readonly (readonly [string, string])[] = [\n ['↑↓', 'select'],\n ['enter', 'use skill'],\n ['tab', 'detail pane'],\n ['/', 'filter'],\n ['esc', 'close'],\n];\nconst BODY_HINTS: readonly (readonly [string, string])[] = [\n ['pgup/pgdn', 'scroll'],\n ['tab', 'tree'],\n ['esc', 'close'],\n];\nconst FILTER_HINTS: readonly (readonly [string, string])[] = [\n ['type', 'to filter'],\n ['enter', 'keep'],\n ['esc', 'clear'],\n];\n\nexport type SkillsOverlayResult = { kind: 'invoke'; skill: SkillEntry } | undefined;\n\nexport interface SkillsOverlayOptions {\n catalog: SkillCatalog;\n /** Repo root, so paths render relative rather than as absolute noise. */\n repoRoot: string;\n readFile?: (filePath: string) => string;\n}\n\ntype RowKind = 'group' | 'owner' | 'skill';\n\ninterface TreeRow {\n kind: RowKind;\n depth: number;\n label: string;\n tag: string;\n /** Only a skill row carries one; group and owner rows are headings. */\n skill?: SkillEntry;\n}\n\nfunction fit(text: string, width: number): string {\n const clipped = truncateToWidth(text, Math.max(0, width), ELLIPSIS);\n return clipped + ' '.repeat(Math.max(0, width - visibleWidth(clipped)));\n}\n\nfunction wrap(text: string, width: number): string[] {\n if (width <= 0) return [];\n const lines: string[] = [];\n let current = '';\n for (const word of text.split(/\\s+/).filter(Boolean)) {\n if (current.length === 0) current = word;\n else if (visibleWidth(current) + 1 + visibleWidth(word) <= width) current += ` ${word}`;\n else {\n lines.push(current);\n current = word;\n }\n }\n if (current.length > 0) lines.push(current);\n return lines;\n}\n\n/** Drops YAML frontmatter so the preview starts at the prose, as Pi does. */\nexport function skillBody(content: string): string {\n const match = content.match(/^---\\r?\\n[\\s\\S]*?\\r?\\n---\\r?\\n?([\\s\\S]*)$/);\n return (match ? (match[1] ?? '') : content).replace(/^\\s+/, '');\n}\n\nfunction matches(skill: SkillEntry, query: string): boolean {\n const needle = query.toLowerCase();\n return skill.name.toLowerCase().includes(needle) || skill.description.toLowerCase().includes(needle);\n}\n\nexport class SkillsOverlayComponent extends DoomOverlay {\n private readonly options: SkillsOverlayOptions;\n private readonly done: (result: SkillsOverlayResult) => void;\n private readonly readFile: (filePath: string) => string;\n private readonly bodyCache = new Map<string, string[]>();\n\n /** Index into the skill rows, not into every row: headings are not selectable. */\n private selected = 0;\n private focus: 'tree' | 'body' = 'tree';\n private filtering = false;\n private query = '';\n private bodyOffset = 0;\n\n constructor(\n tui: DoomOverlayTui,\n theme: Theme,\n options: SkillsOverlayOptions,\n done: (result: SkillsOverlayResult) => void,\n ) {\n super(tui, theme);\n this.options = options;\n this.done = done;\n this.readFile = options.readFile ?? ((filePath) => fs.readFileSync(filePath, 'utf8'));\n }\n\n /** Every row, honouring the active filter. The tree does not fold. */\n private rows(): TreeRow[] {\n const query = this.query.trim();\n const rows: TreeRow[] = [];\n for (const group of this.options.catalog.groups) {\n const owners = query\n ? group.owners\n .map((owner) => ({ ...owner, skills: owner.skills.filter((skill) => matches(skill, query)) }))\n .filter((owner) => owner.skills.length > 0)\n : group.owners;\n if (owners.length === 0) continue;\n const count = owners.reduce((total, owner) => total + owner.skills.length, 0);\n rows.push({ kind: 'group', depth: 0, label: group.label, tag: String(count) });\n for (const owner of owners) {\n rows.push({ kind: 'owner', depth: 1, label: owner.owner, tag: String(owner.skills.length) });\n for (const skill of owner.skills) {\n rows.push({ kind: 'skill', depth: 2, label: skill.name, tag: '', skill });\n }\n }\n }\n return rows;\n }\n\n /**\n * Where the skill rows sit in `rows()`.\n *\n * The selection indexes this list rather than `rows()`, which is what keeps\n * headings unselectable without the key handler having to hunt past them, and\n * what puts the opening selection on a skill rather than on the first group.\n */\n private selectable(rows: TreeRow[]): number[] {\n const indexes: number[] = [];\n for (const [index, row] of rows.entries()) if (row.skill) indexes.push(index);\n return indexes;\n }\n\n private currentRow(): TreeRow | undefined {\n const rows = this.rows();\n const indexes = this.selectable(rows);\n if (indexes.length === 0) return undefined;\n return rows[indexes[Math.min(this.selected, indexes.length - 1)] as number];\n }\n\n private move(delta: number): void {\n const count = this.selectable(this.rows()).length;\n if (count === 0) return;\n this.selected = Math.max(0, Math.min(count - 1, this.selected + delta));\n this.bodyOffset = 0;\n this.invalidate();\n }\n\n private bodyLines(skill: SkillEntry, width: number): string[] {\n let raw = this.bodyCache.get(skill.filePath);\n if (!raw) {\n let source: string;\n try {\n source = skillBody(this.readFile(skill.filePath));\n } catch {\n source = UNREADABLE;\n }\n raw = source.split(/\\r?\\n/);\n this.bodyCache.set(skill.filePath, raw);\n }\n // Wrapped at render width rather than at read time, so a resize reflows the\n // same cached file instead of re-reading it.\n return raw.flatMap((line) => (line.trim() === '' ? [''] : wrap(line, width)));\n }\n\n handleInput(data: string): void {\n if (this.filtering) return this.handleFilterInput(data);\n\n if (matchesKey(data, 'escape')) {\n // The filter clears before the overlay closes: escaping straight out of a\n // narrowed tree loses the search with no way to see what it hid.\n if (this.query) {\n this.query = '';\n this.selected = 0;\n this.invalidate();\n return;\n }\n this.done(undefined);\n return;\n }\n if (data === KEY_TAB || matchesKey(data, 'tab')) {\n this.focus = this.focus === 'tree' ? 'body' : 'tree';\n this.invalidate();\n return;\n }\n if (matchesKey(data, 'pageUp')) {\n this.bodyOffset = Math.max(0, this.bodyOffset - BODY_PAGE);\n this.invalidate();\n return;\n }\n if (matchesKey(data, 'pageDown')) {\n this.bodyOffset += BODY_PAGE;\n this.invalidate();\n return;\n }\n if (this.focus === 'body') return;\n\n if (data === FILTER_KEY) {\n this.filtering = true;\n this.invalidate();\n return;\n }\n if (matchesKey(data, 'up') || matchesKey(data, 'k')) return this.move(-1);\n if (matchesKey(data, 'down') || matchesKey(data, 'j')) return this.move(1);\n\n if (data === KEY_ENTER || data === KEY_NEWLINE || matchesKey(data, 'enter')) {\n const skill = this.currentRow()?.skill;\n if (skill) this.done({ kind: 'invoke', skill });\n }\n }\n\n private handleFilterInput(data: string): void {\n if (matchesKey(data, 'escape')) {\n this.filtering = false;\n this.query = '';\n this.selected = 0;\n this.invalidate();\n return;\n }\n if (data === KEY_ENTER || data === KEY_NEWLINE) {\n this.filtering = false;\n this.invalidate();\n return;\n }\n if (data === KEY_BACKSPACE) this.query = this.query.slice(0, -1);\n // Control sequences are dropped rather than appended: an arrow key would\n // otherwise land in the query as a raw escape sequence.\n else if (data.length === 1 && data >= ' ') this.query += data;\n this.selected = 0;\n this.invalidate();\n }\n\n protected getChrome(): DoomOverlayChrome {\n const { skillCount, promptTokens, bodyTokens } = this.options.catalog;\n const sources = this.options.catalog.groups.filter((group) => group.owners.length > 0).length;\n const hints = this.filtering ? FILTER_HINTS : this.focus === 'body' ? BODY_HINTS : TREE_HINTS;\n return {\n title: TITLE,\n accent: DOOM_OVERLAY_ACCENT,\n breadcrumb: BREADCRUMB,\n // Two token figures, because they answer different questions: what the\n // catalogue costs on every request, and what reading all of it would cost.\n headerRight: `${skillCount} skills · ${sources} sources · ${formatTokens(promptTokens)} always-on · ${formatTokens(bodyTokens)} full`,\n footer: this.hintLine(hints),\n };\n }\n\n private badge(text: string, background: ThemeBg, colour: ThemeColor = 'text'): string {\n return this.theme.bg(background, this.theme.fg(colour, ` ${text} `));\n }\n\n /** Footer hints as capped keys: `[ enter ] use skill`, per the mockup. */\n private hintLine(hints: readonly (readonly [string, string])[]): string {\n return hints\n .map(([key, label]) => `${this.badge(key, 'selectedBg')}${this.theme.fg('dim', ` ${label}`)}`)\n .join(this.theme.fg('dim', ' '));\n }\n\n protected renderBody(width: number, height: number): string[] {\n if (width < MIN_WIDTH) return this.renderStacked(width, height);\n\n const treeWidth = Math.max(MIN_PANE_WIDTH, Math.floor((width - 1) * TREE_PANE_RATIO));\n const detailWidth = Math.max(1, width - treeWidth - 1);\n const treeContent = Math.max(1, treeWidth - PANE_GUTTER);\n const detailContent = Math.max(1, detailWidth - PANE_GUTTER);\n\n const left = this.treeLines(treeContent, height);\n const right = this.detailLines(detailContent, height);\n const divider = this.theme.fg('borderMuted', '│');\n return Array.from({ length: height }, (_, index) => {\n const row = `${fit(left[index] ?? '', treeContent)} ${divider} ${fit(right[index] ?? '', detailContent)}`;\n return truncateToWidth(row, width, ELLIPSIS);\n });\n }\n\n private renderStacked(width: number, height: number): string[] {\n const topHeight = Math.max(1, Math.floor((height - 1) / 2));\n const bottomHeight = Math.max(0, height - topHeight - 1);\n const top = this.treeLines(width, topHeight);\n const bottom = this.detailLines(width, bottomHeight);\n return [\n ...top.slice(0, topHeight),\n ...Array.from({ length: Math.max(0, topHeight - top.length) }, () => ''),\n this.theme.fg('borderMuted', RULE.repeat(width)),\n ...bottom.slice(0, bottomHeight),\n ];\n }\n\n private treeLines(width: number, height: number): string[] {\n const heading = this.filtering || this.query ? `${HEADING} ${FILTER_KEY}${this.query}` : HEADING;\n const lines = [this.theme.bold(this.theme.fg('accent', heading)), ''];\n const rows = this.rows();\n if (rows.length === 0) {\n lines.push(this.theme.fg('dim', EMPTY_TREE));\n return lines;\n }\n\n const budget = Math.max(1, height - lines.length);\n const indexes = this.selectable(rows);\n // No skill rows means the filter matched nothing, so nothing is highlighted.\n const selected = indexes[Math.min(this.selected, indexes.length - 1)] ?? -1;\n const start = Math.max(0, Math.min(selected - budget + 1, Math.max(0, rows.length - budget)));\n for (const [offset, row] of rows.slice(start, start + budget).entries()) {\n const isSelected = start + offset === selected;\n const marker = isSelected ? this.theme.fg('accent', SELECTION_MARKER) : ' ';\n const indent = ' '.repeat(row.depth * INDENT);\n // Three tiers rather than two: the group heading leads, the owner beneath\n // it recedes to a caption, and the skills it holds carry the reading\n // weight because they are the only rows that do anything.\n const colour: ThemeColor = row.kind === 'group' ? 'accent' : row.kind === 'owner' ? 'dim' : 'text';\n let label = this.theme.fg(colour, `${indent}${row.label}`);\n if (row.kind === 'group' || isSelected) label = this.theme.bold(label);\n // The tag is capped before it claims columns: an owner in many domains\n // carries a long `+a,b,c` hint that would otherwise leave the name with\n // no room at all and render the row as a bare count.\n const tag = row.tag ? truncateToWidth(row.tag, Math.floor(width * TAG_SHARE), ELLIPSIS) : '';\n const tagWidth = tag ? visibleWidth(tag) + 1 : 0;\n const body = fit(`${marker} ${label}`, Math.max(1, width - tagWidth));\n const line = tag ? `${body} ${this.theme.fg('dim', tag)}` : body;\n lines.push(isSelected ? this.theme.bg('selectedBg', fit(line, width)) : line);\n }\n return lines;\n }\n\n private field(label: string, value: string, colour: ThemeColor, width: number): string[] {\n const valueWidth = Math.max(1, width - LABEL_COLUMN);\n return wrap(value, valueWidth).map(\n (line, index) =>\n `${this.theme.fg('dim', fit(index === 0 ? label : '', LABEL_COLUMN))}${this.theme.fg(colour, line)}`,\n );\n }\n\n private detailLines(width: number, height: number): string[] {\n const skill = this.currentRow()?.skill;\n if (!skill) return ['', '', this.theme.fg('dim', NO_SELECTION)];\n\n const relative = path.relative(this.options.repoRoot, skill.filePath) || skill.filePath;\n const lines = [\n this.theme.bold(this.theme.fg('accent', skill.name)),\n '',\n ...this.field('description', skill.description, 'text', width),\n ...this.field('source', `${skill.group} › ${skill.owner}`, 'success', width),\n ...this.field('path', relative, 'text', width),\n ...this.field(\n 'invocable',\n skill.modelInvocable ? `model · /skill:${skill.name}` : `/skill:${skill.name} only`,\n 'success',\n width,\n ),\n ];\n\n const body = this.bodyLines(skill, width);\n const remaining = Math.max(0, height - lines.length - 2);\n // Clamped here rather than in the key handler, because how far the body can\n // scroll depends on the width it has just wrapped to.\n this.bodyOffset = Math.min(this.bodyOffset, Math.max(0, body.length - remaining));\n const shown = body.slice(this.bodyOffset, this.bodyOffset + remaining);\n const position =\n body.length > remaining\n ? `lines ${this.bodyOffset + 1}-${this.bodyOffset + shown.length} / ${body.length}`\n : 'SKILL.md';\n const focus = this.focus === 'body' ? this.theme.fg('accent', ' ◀ focused') : '';\n\n lines.push('', `${this.theme.fg('dim', position)}${focus}`);\n for (const line of shown) lines.push(this.theme.fg('dim', line));\n return lines;\n }\n}\n\nexport async function openSkillsOverlay(\n ctx: ExtensionContext,\n options: SkillsOverlayOptions,\n): Promise<SkillsOverlayResult> {\n return ctx.ui.custom<SkillsOverlayResult>(\n (tui, theme, _keybindings, done) => new SkillsOverlayComponent(tui, theme, options, done),\n DOOM_FULLSCREEN_UI_OPTIONS,\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"skillsOverlay.mjs","names":[],"sources":["../../../../src/extensions/entries/components/skillsOverlay.ts"],"sourcesContent":["/**\n * Skills: the `SPC e s` overlay.\n *\n * A tree of the skills this session loaded on the left, the selected skill's\n * frontmatter and body on the right. Everything listed is loaded, so the\n * surface answers \"what do I have, and what does it cost\" rather than doubling\n * as a domain browser.\n *\n * The tree is flat and always open: groups and owners are headings, only skills\n * take the selection. Rendering is pure: the catalogue is resolved once by the\n * caller and passed in, so `render(width)` returns lines with no side effects\n * and the whole surface can be asserted as text without a live terminal.\n */\n\nimport fs from 'node:fs';\nimport path from 'node:path';\nimport {\n DOOM_FULLSCREEN_UI_OPTIONS,\n DOOM_OVERLAY_ACCENT,\n DoomOverlay,\n type DoomOverlayChrome,\n type DoomOverlayTui,\n} from '@agimon-ai/doompi-ui/components/doomOverlay';\nimport { fitStyledLine, formatTokens } from '@agimon-ai/doompi-ui/rendering';\nimport type { ExtensionContext, Theme } from '@earendil-works/pi-coding-agent';\nimport { matchesKey, truncateToWidth, visibleWidth } from '@earendil-works/pi-tui';\nimport type { SkillCatalog, SkillEntry } from '../../../adapters/skillCatalog.ts';\n\ntype ThemeColor = Parameters<Theme['fg']>[0];\ntype ThemeBg = Parameters<Theme['bg']>[0];\n\nconst MIN_WIDTH = 60;\n/** A third for the tree, two thirds for the skill being read. */\nconst TREE_PANE_RATIO = 1 / 3;\n/** One blank column each side of the pane divider so neither column touches it. */\nconst PANE_GUTTER = 1;\nconst MIN_PANE_WIDTH = 20;\nconst LABEL_COLUMN = 13;\n/** Ceiling on the columns a row's right-hand tag may take from its label. */\nconst TAG_SHARE = 0.4;\nconst INDENT = 2;\nconst ELLIPSIS = '…';\nconst SELECTION_MARKER = '›';\nconst RULE = '─';\nconst KEY_TAB = '\\t';\nconst KEY_ENTER = '\\r';\nconst KEY_NEWLINE = '\\n';\nconst KEY_BACKSPACE = '\\x7f';\nconst FILTER_KEY = '/';\nconst BODY_PAGE = 10;\n\nconst TITLE = 'SKILLS';\nconst BREADCRUMB = 'SPC › e / extension › s / skills';\nconst HEADING = 'CATALOG';\nconst EMPTY_TREE = 'No skills discovered.';\nconst NO_SELECTION = 'Select a skill to read it.';\nconst UNREADABLE = 'Could not read SKILL.md.';\n\nconst TREE_HINTS: readonly (readonly [string, string])[] = [\n ['↑↓', 'select'],\n ['enter', 'use skill'],\n ['tab', 'detail pane'],\n ['/', 'filter'],\n ['esc', 'close'],\n];\nconst BODY_HINTS: readonly (readonly [string, string])[] = [\n ['pgup/pgdn', 'scroll'],\n ['tab', 'tree'],\n ['esc', 'close'],\n];\nconst FILTER_HINTS: readonly (readonly [string, string])[] = [\n ['type', 'to filter'],\n ['enter', 'keep'],\n ['esc', 'clear'],\n];\n\nexport type SkillsOverlayResult = { kind: 'invoke'; skill: SkillEntry } | undefined;\n\nexport interface SkillsOverlayOptions {\n catalog: SkillCatalog;\n /** Repo root, so paths render relative rather than as absolute noise. */\n repoRoot: string;\n readFile?: (filePath: string) => string;\n}\n\ntype RowKind = 'group' | 'owner' | 'skill';\n\ninterface TreeRow {\n kind: RowKind;\n depth: number;\n label: string;\n tag: string;\n /** Only a skill row carries one; group and owner rows are headings. */\n skill?: SkillEntry;\n}\n\nfunction fitRow(text: string, width: number): string {\n return fitStyledLine(text, width, ELLIPSIS);\n}\n\nfunction wrap(text: string, width: number): string[] {\n if (width <= 0) return [];\n const lines: string[] = [];\n let current = '';\n for (const word of text.split(/\\s+/).filter(Boolean)) {\n if (current.length === 0) current = word;\n else if (visibleWidth(current) + 1 + visibleWidth(word) <= width) current += ` ${word}`;\n else {\n lines.push(current);\n current = word;\n }\n }\n if (current.length > 0) lines.push(current);\n return lines;\n}\n\n/** Drops YAML frontmatter so the preview starts at the prose, as Pi does. */\nexport function skillBody(content: string): string {\n const match = content.match(/^---\\r?\\n[\\s\\S]*?\\r?\\n---\\r?\\n?([\\s\\S]*)$/);\n return (match ? (match[1] ?? '') : content).replace(/^\\s+/, '');\n}\n\nfunction matches(skill: SkillEntry, query: string): boolean {\n const needle = query.toLowerCase();\n return skill.name.toLowerCase().includes(needle) || skill.description.toLowerCase().includes(needle);\n}\n\nexport class SkillsOverlayComponent extends DoomOverlay {\n private readonly options: SkillsOverlayOptions;\n private readonly done: (result: SkillsOverlayResult) => void;\n private readonly readFile: (filePath: string) => string;\n private readonly bodyCache = new Map<string, string[]>();\n\n /** Index into the skill rows, not into every row: headings are not selectable. */\n private selected = 0;\n private focus: 'tree' | 'body' = 'tree';\n private filtering = false;\n private query = '';\n private bodyOffset = 0;\n\n constructor(\n tui: DoomOverlayTui,\n theme: Theme,\n options: SkillsOverlayOptions,\n done: (result: SkillsOverlayResult) => void,\n ) {\n super(tui, theme);\n this.options = options;\n this.done = done;\n this.readFile = options.readFile ?? ((filePath) => fs.readFileSync(filePath, 'utf8'));\n }\n\n /** Every row, honouring the active filter. The tree does not fold. */\n private rows(): TreeRow[] {\n const query = this.query.trim();\n const rows: TreeRow[] = [];\n for (const group of this.options.catalog.groups) {\n const owners = query\n ? group.owners\n .map((owner) => ({ ...owner, skills: owner.skills.filter((skill) => matches(skill, query)) }))\n .filter((owner) => owner.skills.length > 0)\n : group.owners;\n if (owners.length === 0) continue;\n const count = owners.reduce((total, owner) => total + owner.skills.length, 0);\n rows.push({ kind: 'group', depth: 0, label: group.label, tag: String(count) });\n for (const owner of owners) {\n rows.push({ kind: 'owner', depth: 1, label: owner.owner, tag: String(owner.skills.length) });\n for (const skill of owner.skills) {\n rows.push({ kind: 'skill', depth: 2, label: skill.name, tag: '', skill });\n }\n }\n }\n return rows;\n }\n\n /**\n * Where the skill rows sit in `rows()`.\n *\n * The selection indexes this list rather than `rows()`, which is what keeps\n * headings unselectable without the key handler having to hunt past them, and\n * what puts the opening selection on a skill rather than on the first group.\n */\n private selectable(rows: TreeRow[]): number[] {\n const indexes: number[] = [];\n for (const [index, row] of rows.entries()) if (row.skill) indexes.push(index);\n return indexes;\n }\n\n private currentRow(): TreeRow | undefined {\n const rows = this.rows();\n const indexes = this.selectable(rows);\n if (indexes.length === 0) return undefined;\n return rows[indexes[Math.min(this.selected, indexes.length - 1)] as number];\n }\n\n private move(delta: number): void {\n const count = this.selectable(this.rows()).length;\n if (count === 0) return;\n this.selected = Math.max(0, Math.min(count - 1, this.selected + delta));\n this.bodyOffset = 0;\n this.invalidate();\n }\n\n private bodyLines(skill: SkillEntry, width: number): string[] {\n let raw = this.bodyCache.get(skill.filePath);\n if (!raw) {\n let source: string;\n try {\n source = skillBody(this.readFile(skill.filePath));\n } catch {\n source = UNREADABLE;\n }\n raw = source.split(/\\r?\\n/);\n this.bodyCache.set(skill.filePath, raw);\n }\n // Wrapped at render width rather than at read time, so a resize reflows the\n // same cached file instead of re-reading it.\n return raw.flatMap((line) => (line.trim() === '' ? [''] : wrap(line, width)));\n }\n\n handleInput(data: string): void {\n if (this.filtering) {\n this.handleFilterInput(data);\n return;\n }\n\n if (matchesKey(data, 'escape')) {\n // The filter clears before the overlay closes: escaping straight out of a\n // narrowed tree loses the search with no way to see what it hid.\n if (this.query) {\n this.query = '';\n this.selected = 0;\n this.invalidate();\n return;\n }\n this.done(undefined);\n return;\n }\n if (data === KEY_TAB || matchesKey(data, 'tab')) {\n this.focus = this.focus === 'tree' ? 'body' : 'tree';\n this.invalidate();\n return;\n }\n if (matchesKey(data, 'pageUp')) {\n this.bodyOffset = Math.max(0, this.bodyOffset - BODY_PAGE);\n this.invalidate();\n return;\n }\n if (matchesKey(data, 'pageDown')) {\n this.bodyOffset += BODY_PAGE;\n this.invalidate();\n return;\n }\n if (this.focus === 'body') return;\n\n if (data === FILTER_KEY) {\n this.filtering = true;\n this.invalidate();\n return;\n }\n if (matchesKey(data, 'up') || matchesKey(data, 'k')) {\n this.move(-1);\n return;\n }\n if (matchesKey(data, 'down') || matchesKey(data, 'j')) {\n this.move(1);\n return;\n }\n\n if (data === KEY_ENTER || data === KEY_NEWLINE || matchesKey(data, 'enter')) {\n const skill = this.currentRow()?.skill;\n if (skill) this.done({ kind: 'invoke', skill });\n }\n }\n\n private handleFilterInput(data: string): void {\n if (matchesKey(data, 'escape')) {\n this.filtering = false;\n this.query = '';\n this.selected = 0;\n this.invalidate();\n return;\n }\n if (data === KEY_ENTER || data === KEY_NEWLINE) {\n this.filtering = false;\n this.invalidate();\n return;\n }\n if (data === KEY_BACKSPACE) this.query = this.query.slice(0, -1);\n // Control sequences are dropped rather than appended: an arrow key would\n // otherwise land in the query as a raw escape sequence.\n else if (data.length === 1 && data >= ' ') this.query += data;\n this.selected = 0;\n this.invalidate();\n }\n\n protected getChrome(): DoomOverlayChrome {\n const { skillCount, promptTokens, bodyTokens } = this.options.catalog;\n const sources = this.options.catalog.groups.filter((group) => group.owners.length > 0).length;\n const hints = this.filtering ? FILTER_HINTS : this.focus === 'body' ? BODY_HINTS : TREE_HINTS;\n return {\n title: TITLE,\n accent: DOOM_OVERLAY_ACCENT,\n breadcrumb: BREADCRUMB,\n // Two token figures, because they answer different questions: what the\n // catalogue costs on every request, and what reading all of it would cost.\n headerRight: `${skillCount} skills · ${sources} sources · ${formatTokens(promptTokens)} always-on · ${formatTokens(bodyTokens)} full`,\n footer: this.hintLine(hints),\n };\n }\n\n private badge(text: string, background: ThemeBg, colour: ThemeColor = 'text'): string {\n return this.theme.bg(background, this.theme.fg(colour, ` ${text} `));\n }\n\n /** Footer hints as capped keys: `[ enter ] use skill`, per the mockup. */\n private hintLine(hints: readonly (readonly [string, string])[]): string {\n return hints\n .map(([key, label]) => `${this.badge(key, 'selectedBg')}${this.theme.fg('dim', ` ${label}`)}`)\n .join(this.theme.fg('dim', ' '));\n }\n\n protected renderBody(width: number, height: number): string[] {\n if (width < MIN_WIDTH) return this.renderStacked(width, height);\n\n const treeWidth = Math.max(MIN_PANE_WIDTH, Math.floor((width - 1) * TREE_PANE_RATIO));\n const detailWidth = Math.max(1, width - treeWidth - 1);\n const treeContent = Math.max(1, treeWidth - PANE_GUTTER);\n const detailContent = Math.max(1, detailWidth - PANE_GUTTER);\n\n const left = this.treeLines(treeContent, height);\n const right = this.detailLines(detailContent, height);\n const divider = this.theme.fg('borderMuted', '│');\n return Array.from({ length: height }, (_, index) => {\n const row = `${fitRow(left[index] ?? '', treeContent)} ${divider} ${fitRow(right[index] ?? '', detailContent)}`;\n return truncateToWidth(row, width, ELLIPSIS);\n });\n }\n\n private renderStacked(width: number, height: number): string[] {\n const topHeight = Math.max(1, Math.floor((height - 1) / 2));\n const bottomHeight = Math.max(0, height - topHeight - 1);\n const top = this.treeLines(width, topHeight);\n const bottom = this.detailLines(width, bottomHeight);\n return [\n ...top.slice(0, topHeight),\n ...Array.from({ length: Math.max(0, topHeight - top.length) }, () => ''),\n this.theme.fg('borderMuted', RULE.repeat(width)),\n ...bottom.slice(0, bottomHeight),\n ];\n }\n\n private treeLines(width: number, height: number): string[] {\n const heading = this.filtering || this.query ? `${HEADING} ${FILTER_KEY}${this.query}` : HEADING;\n const lines = [this.theme.bold(this.theme.fg('accent', heading)), ''];\n const rows = this.rows();\n if (rows.length === 0) {\n lines.push(this.theme.fg('dim', EMPTY_TREE));\n return lines;\n }\n\n const budget = Math.max(1, height - lines.length);\n const indexes = this.selectable(rows);\n // No skill rows means the filter matched nothing, so nothing is highlighted.\n const selected = indexes[Math.min(this.selected, indexes.length - 1)] ?? -1;\n const start = Math.max(0, Math.min(selected - budget + 1, Math.max(0, rows.length - budget)));\n for (const [offset, row] of rows.slice(start, start + budget).entries()) {\n const isSelected = start + offset === selected;\n const marker = isSelected ? this.theme.fg('accent', SELECTION_MARKER) : ' ';\n const indent = ' '.repeat(row.depth * INDENT);\n // Three tiers rather than two: the group heading leads, the owner beneath\n // it recedes to a caption, and the skills it holds carry the reading\n // weight because they are the only rows that do anything.\n const colour: ThemeColor = row.kind === 'group' ? 'accent' : row.kind === 'owner' ? 'dim' : 'text';\n let label = this.theme.fg(colour, `${indent}${row.label}`);\n if (row.kind === 'group' || isSelected) label = this.theme.bold(label);\n // The tag is capped before it claims columns: an owner in many domains\n // carries a long `+a,b,c` hint that would otherwise leave the name with\n // no room at all and render the row as a bare count.\n const tag = row.tag ? truncateToWidth(row.tag, Math.floor(width * TAG_SHARE), ELLIPSIS) : '';\n const tagWidth = tag ? visibleWidth(tag) + 1 : 0;\n const body = fitRow(`${marker} ${label}`, Math.max(1, width - tagWidth));\n const line = tag ? `${body} ${this.theme.fg('dim', tag)}` : body;\n lines.push(isSelected ? this.theme.bg('selectedBg', fitRow(line, width)) : line);\n }\n return lines;\n }\n\n private field(label: string, value: string, colour: ThemeColor, width: number): string[] {\n const valueWidth = Math.max(1, width - LABEL_COLUMN);\n return wrap(value, valueWidth).map(\n (line, index) =>\n `${this.theme.fg('dim', fitRow(index === 0 ? label : '', LABEL_COLUMN))}${this.theme.fg(colour, line)}`,\n );\n }\n\n private detailLines(width: number, height: number): string[] {\n const skill = this.currentRow()?.skill;\n if (!skill) return ['', '', this.theme.fg('dim', NO_SELECTION)];\n\n const relative = path.relative(this.options.repoRoot, skill.filePath) || skill.filePath;\n const lines = [\n this.theme.bold(this.theme.fg('accent', skill.name)),\n '',\n ...this.field('description', skill.description, 'text', width),\n ...this.field('source', `${skill.group} › ${skill.owner}`, 'success', width),\n ...this.field('path', relative, 'text', width),\n ...this.field(\n 'invocable',\n skill.modelInvocable ? `model · /skill:${skill.name}` : `/skill:${skill.name} only`,\n 'success',\n width,\n ),\n ];\n\n const body = this.bodyLines(skill, width);\n const remaining = Math.max(0, height - lines.length - 2);\n // Clamped here rather than in the key handler, because how far the body can\n // scroll depends on the width it has just wrapped to.\n this.bodyOffset = Math.min(this.bodyOffset, Math.max(0, body.length - remaining));\n const shown = body.slice(this.bodyOffset, this.bodyOffset + remaining);\n const position =\n body.length > remaining\n ? `lines ${this.bodyOffset + 1}-${this.bodyOffset + shown.length} / ${body.length}`\n : 'SKILL.md';\n const focus = this.focus === 'body' ? this.theme.fg('accent', ' ◀ focused') : '';\n\n lines.push('', `${this.theme.fg('dim', position)}${focus}`);\n for (const line of shown) lines.push(this.theme.fg('dim', line));\n return lines;\n }\n}\n\nexport async function openSkillsOverlay(\n ctx: ExtensionContext,\n options: SkillsOverlayOptions,\n): Promise<SkillsOverlayResult> {\n return ctx.ui.custom<SkillsOverlayResult>(\n (tui, theme, _keybindings, done) => new SkillsOverlayComponent(tui, theme, options, done),\n DOOM_FULLSCREEN_UI_OPTIONS,\n );\n}\n"],"mappings":"+VA+BA,MAsBM,EAAU,UAKV,EAAqD,CACzD,CAAC,KAAM,SAAS,CAChB,CAAC,QAAS,YAAY,CACtB,CAAC,MAAO,cAAc,CACtB,CAAC,IAAK,SAAS,CACf,CAAC,MAAO,QAAQ,CACjB,CACK,EAAqD,CACzD,CAAC,YAAa,SAAS,CACvB,CAAC,MAAO,OAAO,CACf,CAAC,MAAO,QAAQ,CACjB,CACK,EAAuD,CAC3D,CAAC,OAAQ,YAAY,CACrB,CAAC,QAAS,OAAO,CACjB,CAAC,MAAO,QAAQ,CACjB,CAsBD,SAAS,EAAO,EAAc,EAAuB,CACnD,OAAO,EAAc,EAAM,EAAO,IAAS,CAG7C,SAAS,EAAK,EAAc,EAAyB,CACnD,GAAI,GAAS,EAAG,MAAO,EAAE,CACzB,IAAM,EAAkB,EAAE,CACtB,EAAU,GACd,IAAK,IAAM,KAAQ,EAAK,MAAM,MAAM,CAAC,OAAO,QAAQ,CAC9C,EAAQ,SAAW,EAAG,EAAU,EAC3B,EAAa,EAAQ,CAAG,EAAI,EAAa,EAAK,EAAI,EAAO,GAAW,IAAI,KAE/E,EAAM,KAAK,EAAQ,CACnB,EAAU,GAId,OADI,EAAQ,OAAS,GAAG,EAAM,KAAK,EAAQ,CACpC,EAIT,SAAgB,EAAU,EAAyB,CACjD,IAAM,EAAQ,EAAQ,MAAM,4CAA4C,CACxE,OAAQ,EAAS,EAAM,IAAM,GAAM,GAAS,QAAQ,OAAQ,GAAG,CAGjE,SAAS,EAAQ,EAAmB,EAAwB,CAC1D,IAAM,EAAS,EAAM,aAAa,CAClC,OAAO,EAAM,KAAK,aAAa,CAAC,SAAS,EAAO,EAAI,EAAM,YAAY,aAAa,CAAC,SAAS,EAAO,CAGtG,IAAa,EAAb,cAA4C,CAAY,CACtD,QACA,KACA,SACA,UAA6B,IAAI,IAGjC,SAAmB,EACnB,MAAiC,OACjC,UAAoB,GACpB,MAAgB,GAChB,WAAqB,EAErB,YACE,EACA,EACA,EACA,EACA,CACA,MAAM,EAAK,EAAM,CACjB,KAAK,QAAU,EACf,KAAK,KAAO,EACZ,KAAK,SAAW,EAAQ,WAAc,GAAa,EAAG,aAAa,EAAU,OAAO,EAItF,MAA0B,CACxB,IAAM,EAAQ,KAAK,MAAM,MAAM,CACzB,EAAkB,EAAE,CAC1B,IAAK,IAAM,KAAS,KAAK,QAAQ,QAAQ,OAAQ,CAC/C,IAAM,EAAS,EACX,EAAM,OACH,IAAK,IAAW,CAAE,GAAG,EAAO,OAAQ,EAAM,OAAO,OAAQ,GAAU,EAAQ,EAAO,EAAM,CAAC,CAAE,EAAE,CAC7F,OAAQ,GAAU,EAAM,OAAO,OAAS,EAAE,CAC7C,EAAM,OACV,GAAI,EAAO,SAAW,EAAG,SACzB,IAAM,EAAQ,EAAO,QAAQ,EAAO,IAAU,EAAQ,EAAM,OAAO,OAAQ,EAAE,CAC7E,EAAK,KAAK,CAAE,KAAM,QAAS,MAAO,EAAG,MAAO,EAAM,MAAO,IAAK,OAAO,EAAM,CAAE,CAAC,CAC9E,IAAK,IAAM,KAAS,EAAQ,CAC1B,EAAK,KAAK,CAAE,KAAM,QAAS,MAAO,EAAG,MAAO,EAAM,MAAO,IAAK,OAAO,EAAM,OAAO,OAAO,CAAE,CAAC,CAC5F,IAAK,IAAM,KAAS,EAAM,OACxB,EAAK,KAAK,CAAE,KAAM,QAAS,MAAO,EAAG,MAAO,EAAM,KAAM,IAAK,GAAI,QAAO,CAAC,EAI/E,OAAO,EAUT,WAAmB,EAA2B,CAC5C,IAAM,EAAoB,EAAE,CAC5B,IAAK,GAAM,CAAC,EAAO,KAAQ,EAAK,SAAS,CAAM,EAAI,OAAO,EAAQ,KAAK,EAAM,CAC7E,OAAO,EAGT,YAA0C,CACxC,IAAM,EAAO,KAAK,MAAM,CAClB,EAAU,KAAK,WAAW,EAAK,CACjC,KAAQ,SAAW,EACvB,OAAO,EAAK,EAAQ,KAAK,IAAI,KAAK,SAAU,EAAQ,OAAS,EAAE,GAGjE,KAAa,EAAqB,CAChC,IAAM,EAAQ,KAAK,WAAW,KAAK,MAAM,CAAC,CAAC,OACvC,IAAU,IACd,KAAK,SAAW,KAAK,IAAI,EAAG,KAAK,IAAI,EAAQ,EAAG,KAAK,SAAW,EAAM,CAAC,CACvE,KAAK,WAAa,EAClB,KAAK,YAAY,EAGnB,UAAkB,EAAmB,EAAyB,CAC5D,IAAI,EAAM,KAAK,UAAU,IAAI,EAAM,SAAS,CAC5C,GAAI,CAAC,EAAK,CACR,IAAI,EACJ,GAAI,CACF,EAAS,EAAU,KAAK,SAAS,EAAM,SAAS,CAAC,MAC3C,CACN,EAAS,2BAEX,EAAM,EAAO,MAAM,QAAQ,CAC3B,KAAK,UAAU,IAAI,EAAM,SAAU,EAAI,CAIzC,OAAO,EAAI,QAAS,GAAU,EAAK,MAAM,GAAK,GAAK,CAAC,GAAG,CAAG,EAAK,EAAM,EAAM,CAAE,CAG/E,YAAY,EAAoB,CAC9B,GAAI,KAAK,UAAW,CAClB,KAAK,kBAAkB,EAAK,CAC5B,OAGF,GAAI,EAAW,EAAM,SAAS,CAAE,CAG9B,GAAI,KAAK,MAAO,CACd,KAAK,MAAQ,GACb,KAAK,SAAW,EAChB,KAAK,YAAY,CACjB,OAEF,KAAK,KAAK,IAAA,GAAU,CACpB,OAEF,GAAI,IAAS,KAAW,EAAW,EAAM,MAAM,CAAE,CAC/C,KAAK,MAAQ,KAAK,QAAU,OAAS,OAAS,OAC9C,KAAK,YAAY,CACjB,OAEF,GAAI,EAAW,EAAM,SAAS,CAAE,CAC9B,KAAK,WAAa,KAAK,IAAI,EAAG,KAAK,WAAa,GAAU,CAC1D,KAAK,YAAY,CACjB,OAEF,GAAI,EAAW,EAAM,WAAW,CAAE,CAChC,KAAK,YAAc,GACnB,KAAK,YAAY,CACjB,OAEE,QAAK,QAAU,OAEnB,IAAI,IAAS,IAAY,CACvB,KAAK,UAAY,GACjB,KAAK,YAAY,CACjB,OAEF,GAAI,EAAW,EAAM,KAAK,EAAI,EAAW,EAAM,IAAI,CAAE,CACnD,KAAK,KAAK,GAAG,CACb,OAEF,GAAI,EAAW,EAAM,OAAO,EAAI,EAAW,EAAM,IAAI,CAAE,CACrD,KAAK,KAAK,EAAE,CACZ,OAGF,GAAI,IAAS,MAAa,IAAS;GAAe,EAAW,EAAM,QAAQ,CAAE,CAC3E,IAAM,EAAQ,KAAK,YAAY,EAAE,MAC7B,GAAO,KAAK,KAAK,CAAE,KAAM,SAAU,QAAO,CAAC,GAInD,kBAA0B,EAAoB,CAC5C,GAAI,EAAW,EAAM,SAAS,CAAE,CAC9B,KAAK,UAAY,GACjB,KAAK,MAAQ,GACb,KAAK,SAAW,EAChB,KAAK,YAAY,CACjB,OAEF,GAAI,IAAS,MAAa,IAAS;EAAa,CAC9C,KAAK,UAAY,GACjB,KAAK,YAAY,CACjB,OAEE,IAAS,IAAe,KAAK,MAAQ,KAAK,MAAM,MAAM,EAAG,GAAG,CAGvD,EAAK,SAAW,GAAK,GAAQ,MAAK,KAAK,OAAS,GACzD,KAAK,SAAW,EAChB,KAAK,YAAY,CAGnB,WAAyC,CACvC,GAAM,CAAE,aAAY,eAAc,cAAe,KAAK,QAAQ,QACxD,EAAU,KAAK,QAAQ,QAAQ,OAAO,OAAQ,GAAU,EAAM,OAAO,OAAS,EAAE,CAAC,OACjF,EAAQ,KAAK,UAAY,EAAe,KAAK,QAAU,OAAS,EAAa,EACnF,MAAO,CACL,MAAO,SACP,OAAQ,EACR,WAAY,mCAGZ,YAAa,GAAG,EAAW,YAAY,EAAQ,aAAa,EAAa,EAAa,CAAC,eAAe,EAAa,EAAW,CAAC,OAC/H,OAAQ,KAAK,SAAS,EAAM,CAC7B,CAGH,MAAc,EAAc,EAAqB,EAAqB,OAAgB,CACpF,OAAO,KAAK,MAAM,GAAG,EAAY,KAAK,MAAM,GAAG,EAAQ,IAAI,EAAK,GAAG,CAAC,CAItE,SAAiB,EAAuD,CACtE,OAAO,EACJ,KAAK,CAAC,EAAK,KAAW,GAAG,KAAK,MAAM,EAAK,aAAa,GAAG,KAAK,MAAM,GAAG,MAAO,IAAI,IAAQ,GAAG,CAC7F,KAAK,KAAK,MAAM,GAAG,MAAO,MAAM,CAAC,CAGtC,WAAqB,EAAe,EAA0B,CAC5D,GAAI,EAAQ,GAAW,OAAO,KAAK,cAAc,EAAO,EAAO,CAE/D,IAAM,EAAY,KAAK,IAAI,GAAgB,KAAK,OAAO,EAAQ,GAAK,kBAAgB,CAAC,CAC/E,EAAc,KAAK,IAAI,EAAG,EAAQ,EAAY,EAAE,CAChD,EAAc,KAAK,IAAI,EAAG,EAAY,EAAY,CAClD,EAAgB,KAAK,IAAI,EAAG,EAAc,EAAY,CAEtD,EAAO,KAAK,UAAU,EAAa,EAAO,CAC1C,EAAQ,KAAK,YAAY,EAAe,EAAO,CAC/C,EAAU,KAAK,MAAM,GAAG,cAAe,IAAI,CACjD,OAAO,MAAM,KAAK,CAAE,OAAQ,EAAQ,EAAG,EAAG,IAEjC,EAAgB,GADR,EAAO,EAAK,IAAU,GAAI,EAAY,CAAC,GAAG,EAAQ,GAAG,EAAO,EAAM,IAAU,GAAI,EAAc,GACjF,EAAO,IAAS,CAC5C,CAGJ,cAAsB,EAAe,EAA0B,CAC7D,IAAM,EAAY,KAAK,IAAI,EAAG,KAAK,OAAO,EAAS,GAAK,EAAE,CAAC,CACrD,EAAe,KAAK,IAAI,EAAG,EAAS,EAAY,EAAE,CAClD,EAAM,KAAK,UAAU,EAAO,EAAU,CACtC,EAAS,KAAK,YAAY,EAAO,EAAa,CACpD,MAAO,CACL,GAAG,EAAI,MAAM,EAAG,EAAU,CAC1B,GAAG,MAAM,KAAK,CAAE,OAAQ,KAAK,IAAI,EAAG,EAAY,EAAI,OAAO,CAAE,KAAQ,GAAG,CACxE,KAAK,MAAM,GAAG,cAAe,IAAK,OAAO,EAAM,CAAC,CAChD,GAAG,EAAO,MAAM,EAAG,EAAa,CACjC,CAGH,UAAkB,EAAe,EAA0B,CACzD,IAAM,EAAU,KAAK,WAAa,KAAK,MAAQ,GAAG,EAAQ,KAAiB,KAAK,QAAU,EACpF,EAAQ,CAAC,KAAK,MAAM,KAAK,KAAK,MAAM,GAAG,SAAU,EAAQ,CAAC,CAAE,GAAG,CAC/D,EAAO,KAAK,MAAM,CACxB,GAAI,EAAK,SAAW,EAElB,OADA,EAAM,KAAK,KAAK,MAAM,GAAG,MAAO,wBAAW,CAAC,CACrC,EAGT,IAAM,EAAS,KAAK,IAAI,EAAG,EAAS,EAAM,OAAO,CAC3C,EAAU,KAAK,WAAW,EAAK,CAE/B,EAAW,EAAQ,KAAK,IAAI,KAAK,SAAU,EAAQ,OAAS,EAAE,GAAK,GACnE,EAAQ,KAAK,IAAI,EAAG,KAAK,IAAI,EAAW,EAAS,EAAG,KAAK,IAAI,EAAG,EAAK,OAAS,EAAO,CAAC,CAAC,CAC7F,IAAK,GAAM,CAAC,EAAQ,KAAQ,EAAK,MAAM,EAAO,EAAQ,EAAO,CAAC,SAAS,CAAE,CACvE,IAAM,EAAa,EAAQ,IAAW,EAChC,EAAS,EAAa,KAAK,MAAM,GAAG,SAAU,IAAiB,CAAG,IAClE,EAAS,IAAI,OAAO,EAAI,MAAQ,EAAO,CAIvC,EAAqB,EAAI,OAAS,QAAU,SAAW,EAAI,OAAS,QAAU,MAAQ,OACxF,EAAQ,KAAK,MAAM,GAAG,EAAQ,GAAG,IAAS,EAAI,QAAQ,EACtD,EAAI,OAAS,SAAW,KAAY,EAAQ,KAAK,MAAM,KAAK,EAAM,EAItE,IAAM,EAAM,EAAI,IAAM,EAAgB,EAAI,IAAK,KAAK,MAAM,EAAQ,GAAU,CAAE,IAAS,CAAG,GACpF,EAAW,EAAM,EAAa,EAAI,CAAG,EAAI,EACzC,EAAO,EAAO,GAAG,EAAO,GAAG,IAAS,KAAK,IAAI,EAAG,EAAQ,EAAS,CAAC,CAClE,EAAO,EAAM,GAAG,EAAK,GAAG,KAAK,MAAM,GAAG,MAAO,EAAI,GAAK,EAC5D,EAAM,KAAK,EAAa,KAAK,MAAM,GAAG,aAAc,EAAO,EAAM,EAAM,CAAC,CAAG,EAAK,CAElF,OAAO,EAGT,MAAc,EAAe,EAAe,EAAoB,EAAyB,CAEvF,OAAO,EAAK,EADO,KAAK,IAAI,EAAG,EAAQ,GACV,CAAC,CAAC,KAC5B,EAAM,IACL,GAAG,KAAK,MAAM,GAAG,MAAO,EAAO,IAAU,EAAI,EAAQ,GAAI,GAAa,CAAC,GAAG,KAAK,MAAM,GAAG,EAAQ,EAAK,GACxG,CAGH,YAAoB,EAAe,EAA0B,CAC3D,IAAM,EAAQ,KAAK,YAAY,EAAE,MACjC,GAAI,CAAC,EAAO,MAAO,CAAC,GAAI,GAAI,KAAK,MAAM,GAAG,MAAO,6BAAa,CAAC,CAE/D,IAAM,EAAW,EAAK,SAAS,KAAK,QAAQ,SAAU,EAAM,SAAS,EAAI,EAAM,SACzE,EAAQ,CACZ,KAAK,MAAM,KAAK,KAAK,MAAM,GAAG,SAAU,EAAM,KAAK,CAAC,CACpD,GACA,GAAG,KAAK,MAAM,cAAe,EAAM,YAAa,OAAQ,EAAM,CAC9D,GAAG,KAAK,MAAM,SAAU,GAAG,EAAM,MAAM,KAAK,EAAM,QAAS,UAAW,EAAM,CAC5E,GAAG,KAAK,MAAM,OAAQ,EAAU,OAAQ,EAAM,CAC9C,GAAG,KAAK,MACN,YACA,EAAM,eAAiB,oBAAoB,EAAM,OAAS,UAAU,EAAM,KAAK,OAC/E,UACA,EACD,CACF,CAEK,EAAO,KAAK,UAAU,EAAO,EAAM,CACnC,EAAY,KAAK,IAAI,EAAG,EAAS,EAAM,OAAS,EAAE,CAGxD,KAAK,WAAa,KAAK,IAAI,KAAK,WAAY,KAAK,IAAI,EAAG,EAAK,OAAS,EAAU,CAAC,CACjF,IAAM,EAAQ,EAAK,MAAM,KAAK,WAAY,KAAK,WAAa,EAAU,CAChE,EACJ,EAAK,OAAS,EACV,SAAS,KAAK,WAAa,EAAE,GAAG,KAAK,WAAa,EAAM,OAAO,KAAK,EAAK,SACzE,WACA,EAAQ,KAAK,QAAU,OAAS,KAAK,MAAM,GAAG,SAAU,aAAa,CAAG,GAE9E,EAAM,KAAK,GAAI,GAAG,KAAK,MAAM,GAAG,MAAO,EAAS,GAAG,IAAQ,CAC3D,IAAK,IAAM,KAAQ,EAAO,EAAM,KAAK,KAAK,MAAM,GAAG,MAAO,EAAK,CAAC,CAChE,OAAO,IAIX,eAAsB,EACpB,EACA,EAC8B,CAC9B,OAAO,EAAI,GAAG,QACX,EAAK,EAAO,EAAc,IAAS,IAAI,EAAuB,EAAK,EAAO,EAAS,EAAK,CACzF,EACD"}
@@ -1,2 +1,2 @@
1
- require(`../../_virtual/_rolldown/runtime.cjs`);let e=require(`@agimon-ai/doompi-config/piContext`);const t=`info`,n=`(none)`;function r(e){return e.split(`,`).map(e=>e.trim()).filter(Boolean)}function i(e){let{domains:t}=e;return t.length>0?t:[`default`]}function a(e,t){return`Active domains: ${i(e).join(`, `)||n}\nAvailable domains: ${t.join(`, `)}\nUse /domains <name[,name...]> to switch.`}function o(e){return e.map(e=>({value:e,label:e}))}function s(s){let c,l,u,d,f=()=>c??=import(`@agimon-ai/doompi-config/domains`),p=()=>l??=Promise.resolve().then(()=>require(`../../adapters/config/harnessState.cjs`)),m=()=>u??=Promise.resolve().then(()=>require(`../../adapters/matrixSwitcher.cjs`)),h=()=>d??=Promise.resolve().then(()=>require(`./components/matrixPicker.cjs`));s.on(`session_start`,(t,n)=>{let r=(0,e.requireDoomConfigContext)(n).harness;n.ui.addAutocompleteProvider(e=>({triggerCharacters:[`,`],async getSuggestions(t,n,i,a){let o=t[n]??``,{domainCompletionItems:s,domainCompletionPrefix:c,listDomainNames:l}=await f(),u=c(o.slice(0,i));if(u===void 0)return e.getSuggestions(t,n,i,a);let d=r.root;if(!d)return e.getSuggestions(t,n,i,a);let p=s(l(d),u);return p.length===0?e.getSuggestions(t,n,i,a):{prefix:u,items:p}},applyCompletion(t,n,r,i,a){return e.applyCompletion(t,n,r,i,a)},shouldTriggerFileCompletion(t,n,r){return e.shouldTriggerFileCompletion?.(t,n,r)??!0}}))}),s.registerCommand(`domains`,{description:`Show the selected domains and pick a different set`,handler:async(c,l)=>{let u=(0,e.requireDoomConfigContext)(l).harness,{requireHarnessRoot:d}=await p(),g=d(u),_=r(c);if(_.length===0&&l.mode===`tui`){let e=i(u),[{listDomainNames:t},{MatrixPickerComponent:r}]=await Promise.all([f(),h()]),a=await l.ui.custom((i,a,s,c)=>new r({title:`Domains (active: ${e.join(`, `)||n})`,items:o(t(g)),selected:[...e],multi:!0},a,s,c));if(!a||a.length===0)return;_=a}if(_.length===0){let{listDomainNames:e}=await f();l.ui.notify(a(u,e(g)),t);return}await l.waitForIdle();let[{applyDomains:v},{persistHarnessSelection:y}]=await Promise.all([m(),p()]);y(s,await v(_,u)),l.ui.notify(`Switched domains to: ${_.join(`, `)}`,t),await l.reload()}})}exports.default=s,exports.domainItems=o;
1
+ require(`../../_virtual/_rolldown/runtime.cjs`);let e=require(`@agimon-ai/doompi-config/piContext`);const t=`info`,n=`(none)`;function r(e){return e.split(`,`).map(e=>e.trim()).filter(Boolean)}function i(e){let{domains:t}=e;return t.length>0?t:[`default`]}function a(e,t){return`Active domains: ${i(e).join(`, `)||n}\nAvailable domains: ${t.join(`, `)}\nUse /domains <name[,name...]> to switch.`}function o(e){return e.map(e=>({value:e,label:e}))}function s(s){let c,l,u,d,f=()=>c??=import(`@agimon-ai/doompi-config/domains`),p=()=>l??=Promise.resolve().then(()=>require(`../../adapters/config/harnessState.cjs`)),m=()=>u??=Promise.resolve().then(()=>require(`../../adapters/matrixSwitcher.cjs`)),h=()=>d??=Promise.resolve().then(()=>require(`./components/matrixPicker.cjs`));s.on(`resources_discover`,(t,n)=>({skillPaths:[...(0,e.requireDoomConfigContext)(n).harness.skillDirectories]})),s.on(`session_start`,(t,n)=>{let r=(0,e.requireDoomConfigContext)(n).harness;n.ui.addAutocompleteProvider(e=>({triggerCharacters:[`,`],async getSuggestions(t,n,i,a){let o=t[n]??``,{domainCompletionItems:s,domainCompletionPrefix:c,listDomainNames:l}=await f(),u=c(o.slice(0,i));if(u===void 0)return e.getSuggestions(t,n,i,a);let d=r.root;if(!d)return e.getSuggestions(t,n,i,a);let p=s(l(d),u);return p.length===0?e.getSuggestions(t,n,i,a):{prefix:u,items:p}},applyCompletion(t,n,r,i,a){return e.applyCompletion(t,n,r,i,a)},shouldTriggerFileCompletion(t,n,r){return e.shouldTriggerFileCompletion?.(t,n,r)??!0}}))}),s.registerCommand(`domains`,{description:`Show the selected domains and pick a different set`,handler:async(c,l)=>{let u=(0,e.requireDoomConfigContext)(l).harness,{requireHarnessRoot:d}=await p(),g=d(u),_=r(c);if(_.length===0&&l.mode===`tui`){let e=i(u),[{listDomainNames:t},{MatrixPickerComponent:r}]=await Promise.all([f(),h()]),a=await l.ui.custom((i,a,s,c)=>new r({title:`Domains (active: ${e.join(`, `)||n})`,items:o(t(g)),selected:[...e],multi:!0},a,s,c));if(!a||a.length===0)return;_=a}if(_.length===0){let{listDomainNames:e}=await f();l.ui.notify(a(u,e(g)),t);return}await l.waitForIdle();let[{applyDomains:v},{persistHarnessSelection:y}]=await Promise.all([m(),p()]);y(s,await v(_,u)),l.ui.notify(`Switched domains to: ${_.join(`, `)}`,t),await l.reload()}})}exports.default=s,exports.domainItems=o;
2
2
  //# sourceMappingURL=domains.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"domains.cjs","names":[],"sources":["../../../src/extensions/entries/domains.ts"],"sourcesContent":["import { requireDoomConfigContext } from '@agimon-ai/doompi-config/piContext';\nimport type { ExtensionAPI } from '@earendil-works/pi-coding-agent';\nimport type { SelectItem } from '@earendil-works/pi-tui';\nimport type { DoomHarnessContext } from '../../adapters/config/harnessState.ts';\n\nconst DOMAIN_SEPARATOR = ',';\nconst INFO = 'info';\nconst NONE = '(none)';\n\nfunction splitDomains(value: string): string[] {\n return value\n .split(DOMAIN_SEPARATOR)\n .map((item) => item.trim())\n .filter(Boolean);\n}\n\nfunction currentDomains(state: DoomHarnessContext): readonly string[] {\n const { domains } = state;\n return domains.length > 0 ? domains : ['default'];\n}\n\nfunction domainSummary(state: DoomHarnessContext, availableDomains: readonly string[]): string {\n const active = currentDomains(state).join(', ') || NONE;\n return `Active domains: ${active}\\nAvailable domains: ${availableDomains.join(', ')}\\nUse /domains <name[,name...]> to switch.`;\n}\n\n/** Picker rows for the domain multi-select. */\nexport function domainItems(names: string[]): SelectItem[] {\n return names.map((name) => ({ value: name, label: name }));\n}\n\nexport function domainsExtension(pi: ExtensionAPI): void {\n let domainConfigPromise: Promise<typeof import('@agimon-ai/doompi-config/domains')> | undefined;\n let harnessStatePromise: Promise<typeof import('../../adapters/config/harnessState.ts')> | undefined;\n let matrixSwitcherPromise: Promise<typeof import('../../adapters/matrixSwitcher.ts')> | undefined;\n let pickerPromise: Promise<typeof import('./components/matrixPicker.ts')> | undefined;\n\n const loadDomainConfig = () => (domainConfigPromise ??= import('@agimon-ai/doompi-config/domains'));\n const loadHarnessState = () => (harnessStatePromise ??= import('../../adapters/config/harnessState.ts'));\n const loadMatrixSwitcher = () => (matrixSwitcherPromise ??= import('../../adapters/matrixSwitcher.ts'));\n const loadPicker = () => (pickerPromise ??= import('./components/matrixPicker.ts'));\n\n pi.on('session_start', (_event, ctx) => {\n const state = requireDoomConfigContext(ctx).harness;\n ctx.ui.addAutocompleteProvider((current) => ({\n triggerCharacters: [','],\n async getSuggestions(lines, cursorLine, cursorCol, options) {\n const line = lines[cursorLine] ?? '';\n const { domainCompletionItems, domainCompletionPrefix, listDomainNames } = await loadDomainConfig();\n const prefix = domainCompletionPrefix(line.slice(0, cursorCol));\n if (prefix === undefined) return current.getSuggestions(lines, cursorLine, cursorCol, options);\n\n const repoRoot = state.root;\n if (!repoRoot) return current.getSuggestions(lines, cursorLine, cursorCol, options);\n\n const items = domainCompletionItems(listDomainNames(repoRoot), prefix);\n if (items.length === 0) return current.getSuggestions(lines, cursorLine, cursorCol, options);\n return { prefix, items };\n },\n applyCompletion(lines, cursorLine, cursorCol, item, prefix) {\n return current.applyCompletion(lines, cursorLine, cursorCol, item, prefix);\n },\n shouldTriggerFileCompletion(lines, cursorLine, cursorCol) {\n return current.shouldTriggerFileCompletion?.(lines, cursorLine, cursorCol) ?? true;\n },\n }));\n });\n\n pi.registerCommand('domains', {\n description: 'Show the selected domains and pick a different set',\n handler: async (args, ctx) => {\n const state = requireDoomConfigContext(ctx).harness;\n const { requireHarnessRoot } = await loadHarnessState();\n const repoRoot = requireHarnessRoot(state);\n\n let requested = splitDomains(args);\n // No argument opens the multi-select. Domains compose, unlike a layer\n // base, so this picker toggles with space and confirms the whole set.\n if (requested.length === 0 && ctx.mode === 'tui') {\n const active = currentDomains(state);\n const [{ listDomainNames }, { MatrixPickerComponent }] = await Promise.all([loadDomainConfig(), loadPicker()]);\n const chosen = await ctx.ui.custom<string[] | undefined>(\n (_tui, theme, keybindings, done) =>\n new MatrixPickerComponent(\n {\n title: `Domains (active: ${active.join(', ') || NONE})`,\n items: domainItems(listDomainNames(repoRoot)),\n selected: [...active],\n multi: true,\n },\n theme,\n keybindings,\n done,\n ),\n );\n if (!chosen || chosen.length === 0) return;\n requested = chosen;\n }\n\n if (requested.length === 0) {\n const { listDomainNames } = await loadDomainConfig();\n ctx.ui.notify(domainSummary(state, listDomainNames(repoRoot)), INFO);\n return;\n }\n\n await ctx.waitForIdle();\n const [{ applyDomains }, { persistHarnessSelection }] = await Promise.all([\n loadMatrixSwitcher(),\n loadHarnessState(),\n ]);\n const updated = await applyDomains(requested, state);\n persistHarnessSelection(pi, updated);\n ctx.ui.notify(`Switched domains to: ${requested.join(', ')}`, INFO);\n await ctx.reload();\n },\n });\n}\n\nexport default domainsExtension;\n"],"mappings":"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"}
1
+ {"version":3,"file":"domains.cjs","names":[],"sources":["../../../src/extensions/entries/domains.ts"],"sourcesContent":["import { requireDoomConfigContext } from '@agimon-ai/doompi-config/piContext';\nimport type { ExtensionAPI } from '@earendil-works/pi-coding-agent';\nimport type { SelectItem } from '@earendil-works/pi-tui';\nimport type { DoomHarnessContext } from '../../adapters/config/harnessState.ts';\n\nconst DOMAIN_SEPARATOR = ',';\nconst INFO = 'info';\nconst NONE = '(none)';\n\nfunction splitDomains(value: string): string[] {\n return value\n .split(DOMAIN_SEPARATOR)\n .map((item) => item.trim())\n .filter(Boolean);\n}\n\nfunction currentDomains(state: DoomHarnessContext): readonly string[] {\n const { domains } = state;\n return domains.length > 0 ? domains : ['default'];\n}\n\nfunction domainSummary(state: DoomHarnessContext, availableDomains: readonly string[]): string {\n const active = currentDomains(state).join(', ') || NONE;\n return `Active domains: ${active}\\nAvailable domains: ${availableDomains.join(', ')}\\nUse /domains <name[,name...]> to switch.`;\n}\n\n/** Picker rows for the domain multi-select. */\nexport function domainItems(names: string[]): SelectItem[] {\n return names.map((name) => ({ value: name, label: name }));\n}\n\nexport function domainsExtension(pi: ExtensionAPI): void {\n let domainConfigPromise: Promise<typeof import('@agimon-ai/doompi-config/domains')> | undefined;\n let harnessStatePromise: Promise<typeof import('../../adapters/config/harnessState.ts')> | undefined;\n let matrixSwitcherPromise: Promise<typeof import('../../adapters/matrixSwitcher.ts')> | undefined;\n let pickerPromise: Promise<typeof import('./components/matrixPicker.ts')> | undefined;\n\n const loadDomainConfig = () => (domainConfigPromise ??= import('@agimon-ai/doompi-config/domains'));\n const loadHarnessState = () => (harnessStatePromise ??= import('../../adapters/config/harnessState.ts'));\n const loadMatrixSwitcher = () => (matrixSwitcherPromise ??= import('../../adapters/matrixSwitcher.ts'));\n const loadPicker = () => (pickerPromise ??= import('./components/matrixPicker.ts'));\n\n // Pi reloads base resources, then asks extensions to extend them again.\n // Read the rebound context so a domain switch discovers only the new selection.\n pi.on('resources_discover', (_event, ctx) => ({\n skillPaths: [...requireDoomConfigContext(ctx).harness.skillDirectories],\n }));\n\n pi.on('session_start', (_event, ctx) => {\n const state = requireDoomConfigContext(ctx).harness;\n ctx.ui.addAutocompleteProvider((current) => ({\n triggerCharacters: [','],\n async getSuggestions(lines, cursorLine, cursorCol, options) {\n const line = lines[cursorLine] ?? '';\n const { domainCompletionItems, domainCompletionPrefix, listDomainNames } = await loadDomainConfig();\n const prefix = domainCompletionPrefix(line.slice(0, cursorCol));\n if (prefix === undefined) return current.getSuggestions(lines, cursorLine, cursorCol, options);\n\n const repoRoot = state.root;\n if (!repoRoot) return current.getSuggestions(lines, cursorLine, cursorCol, options);\n\n const items = domainCompletionItems(listDomainNames(repoRoot), prefix);\n if (items.length === 0) return current.getSuggestions(lines, cursorLine, cursorCol, options);\n return { prefix, items };\n },\n applyCompletion(lines, cursorLine, cursorCol, item, prefix) {\n return current.applyCompletion(lines, cursorLine, cursorCol, item, prefix);\n },\n shouldTriggerFileCompletion(lines, cursorLine, cursorCol) {\n return current.shouldTriggerFileCompletion?.(lines, cursorLine, cursorCol) ?? true;\n },\n }));\n });\n\n pi.registerCommand('domains', {\n description: 'Show the selected domains and pick a different set',\n handler: async (args, ctx) => {\n const state = requireDoomConfigContext(ctx).harness;\n const { requireHarnessRoot } = await loadHarnessState();\n const repoRoot = requireHarnessRoot(state);\n\n let requested = splitDomains(args);\n // No argument opens the multi-select. Domains compose, unlike a layer\n // base, so this picker toggles with space and confirms the whole set.\n if (requested.length === 0 && ctx.mode === 'tui') {\n const active = currentDomains(state);\n const [{ listDomainNames }, { MatrixPickerComponent }] = await Promise.all([loadDomainConfig(), loadPicker()]);\n const chosen = await ctx.ui.custom<string[] | undefined>(\n (_tui, theme, keybindings, done) =>\n new MatrixPickerComponent(\n {\n title: `Domains (active: ${active.join(', ') || NONE})`,\n items: domainItems(listDomainNames(repoRoot)),\n selected: [...active],\n multi: true,\n },\n theme,\n keybindings,\n done,\n ),\n );\n if (!chosen || chosen.length === 0) return;\n requested = chosen;\n }\n\n if (requested.length === 0) {\n const { listDomainNames } = await loadDomainConfig();\n ctx.ui.notify(domainSummary(state, listDomainNames(repoRoot)), INFO);\n return;\n }\n\n await ctx.waitForIdle();\n const [{ applyDomains }, { persistHarnessSelection }] = await Promise.all([\n loadMatrixSwitcher(),\n loadHarnessState(),\n ]);\n const updated = await applyDomains(requested, state);\n persistHarnessSelection(pi, updated);\n ctx.ui.notify(`Switched domains to: ${requested.join(', ')}`, INFO);\n await ctx.reload();\n },\n });\n}\n\nexport default domainsExtension;\n"],"mappings":"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"}
@@ -1,2 +1,2 @@
1
- import{requireDoomConfigContext as e}from"@agimon-ai/doompi-config/piContext";const t=`info`,n=`(none)`;function r(e){return e.split(`,`).map(e=>e.trim()).filter(Boolean)}function i(e){let{domains:t}=e;return t.length>0?t:[`default`]}function a(e,t){return`Active domains: ${i(e).join(`, `)||n}\nAvailable domains: ${t.join(`, `)}\nUse /domains <name[,name...]> to switch.`}function o(e){return e.map(e=>({value:e,label:e}))}function s(s){let c,l,u,d,f=()=>c??=import(`@agimon-ai/doompi-config/domains`),p=()=>l??=import(`../../adapters/config/harnessState.mjs`),m=()=>u??=import(`../../adapters/matrixSwitcher.mjs`),h=()=>d??=import(`./components/matrixPicker.mjs`);s.on(`session_start`,(t,n)=>{let r=e(n).harness;n.ui.addAutocompleteProvider(e=>({triggerCharacters:[`,`],async getSuggestions(t,n,i,a){let o=t[n]??``,{domainCompletionItems:s,domainCompletionPrefix:c,listDomainNames:l}=await f(),u=c(o.slice(0,i));if(u===void 0)return e.getSuggestions(t,n,i,a);let d=r.root;if(!d)return e.getSuggestions(t,n,i,a);let p=s(l(d),u);return p.length===0?e.getSuggestions(t,n,i,a):{prefix:u,items:p}},applyCompletion(t,n,r,i,a){return e.applyCompletion(t,n,r,i,a)},shouldTriggerFileCompletion(t,n,r){return e.shouldTriggerFileCompletion?.(t,n,r)??!0}}))}),s.registerCommand(`domains`,{description:`Show the selected domains and pick a different set`,handler:async(c,l)=>{let u=e(l).harness,{requireHarnessRoot:d}=await p(),g=d(u),_=r(c);if(_.length===0&&l.mode===`tui`){let e=i(u),[{listDomainNames:t},{MatrixPickerComponent:r}]=await Promise.all([f(),h()]),a=await l.ui.custom((i,a,s,c)=>new r({title:`Domains (active: ${e.join(`, `)||n})`,items:o(t(g)),selected:[...e],multi:!0},a,s,c));if(!a||a.length===0)return;_=a}if(_.length===0){let{listDomainNames:e}=await f();l.ui.notify(a(u,e(g)),t);return}await l.waitForIdle();let[{applyDomains:v},{persistHarnessSelection:y}]=await Promise.all([m(),p()]);y(s,await v(_,u)),l.ui.notify(`Switched domains to: ${_.join(`, `)}`,t),await l.reload()}})}export{s as default,o as domainItems};
1
+ import{requireDoomConfigContext as e}from"@agimon-ai/doompi-config/piContext";const t=`info`,n=`(none)`;function r(e){return e.split(`,`).map(e=>e.trim()).filter(Boolean)}function i(e){let{domains:t}=e;return t.length>0?t:[`default`]}function a(e,t){return`Active domains: ${i(e).join(`, `)||n}\nAvailable domains: ${t.join(`, `)}\nUse /domains <name[,name...]> to switch.`}function o(e){return e.map(e=>({value:e,label:e}))}function s(s){let c,l,u,d,f=()=>c??=import(`@agimon-ai/doompi-config/domains`),p=()=>l??=import(`../../adapters/config/harnessState.mjs`),m=()=>u??=import(`../../adapters/matrixSwitcher.mjs`),h=()=>d??=import(`./components/matrixPicker.mjs`);s.on(`resources_discover`,(t,n)=>({skillPaths:[...e(n).harness.skillDirectories]})),s.on(`session_start`,(t,n)=>{let r=e(n).harness;n.ui.addAutocompleteProvider(e=>({triggerCharacters:[`,`],async getSuggestions(t,n,i,a){let o=t[n]??``,{domainCompletionItems:s,domainCompletionPrefix:c,listDomainNames:l}=await f(),u=c(o.slice(0,i));if(u===void 0)return e.getSuggestions(t,n,i,a);let d=r.root;if(!d)return e.getSuggestions(t,n,i,a);let p=s(l(d),u);return p.length===0?e.getSuggestions(t,n,i,a):{prefix:u,items:p}},applyCompletion(t,n,r,i,a){return e.applyCompletion(t,n,r,i,a)},shouldTriggerFileCompletion(t,n,r){return e.shouldTriggerFileCompletion?.(t,n,r)??!0}}))}),s.registerCommand(`domains`,{description:`Show the selected domains and pick a different set`,handler:async(c,l)=>{let u=e(l).harness,{requireHarnessRoot:d}=await p(),g=d(u),_=r(c);if(_.length===0&&l.mode===`tui`){let e=i(u),[{listDomainNames:t},{MatrixPickerComponent:r}]=await Promise.all([f(),h()]),a=await l.ui.custom((i,a,s,c)=>new r({title:`Domains (active: ${e.join(`, `)||n})`,items:o(t(g)),selected:[...e],multi:!0},a,s,c));if(!a||a.length===0)return;_=a}if(_.length===0){let{listDomainNames:e}=await f();l.ui.notify(a(u,e(g)),t);return}await l.waitForIdle();let[{applyDomains:v},{persistHarnessSelection:y}]=await Promise.all([m(),p()]);y(s,await v(_,u)),l.ui.notify(`Switched domains to: ${_.join(`, `)}`,t),await l.reload()}})}export{s as default,o as domainItems};
2
2
  //# sourceMappingURL=domains.mjs.map