@compilr-dev/cli 0.4.0 → 0.5.0

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 (315) hide show
  1. package/README.md +30 -12
  2. package/dist/agent.d.ts +74 -1
  3. package/dist/agent.js +259 -76
  4. package/dist/anchors/index.d.ts +9 -0
  5. package/dist/anchors/index.js +9 -0
  6. package/dist/anchors/project-anchors.d.ts +79 -0
  7. package/dist/anchors/project-anchors.js +202 -0
  8. package/dist/commands/handler-types.d.ts +68 -0
  9. package/dist/commands/handler-types.js +8 -0
  10. package/dist/commands/handlers/agent-commands.d.ts +13 -0
  11. package/dist/commands/handlers/agent-commands.js +305 -0
  12. package/dist/commands/handlers/design-commands.d.ts +15 -0
  13. package/dist/commands/handlers/design-commands.js +334 -0
  14. package/dist/commands/handlers/index.d.ts +20 -0
  15. package/dist/commands/handlers/index.js +43 -0
  16. package/dist/commands/handlers/overlay-commands.d.ts +21 -0
  17. package/dist/commands/handlers/overlay-commands.js +287 -0
  18. package/dist/commands/handlers/project-commands.d.ts +11 -0
  19. package/dist/commands/handlers/project-commands.js +167 -0
  20. package/dist/commands/handlers/simple-commands.d.ts +19 -0
  21. package/dist/commands/handlers/simple-commands.js +144 -0
  22. package/dist/commands/index.d.ts +2 -1
  23. package/dist/commands/registry.d.ts +50 -0
  24. package/dist/commands/registry.js +75 -0
  25. package/dist/commands-v2/handlers/context.d.ts +13 -0
  26. package/dist/commands-v2/handlers/context.js +348 -0
  27. package/dist/commands-v2/handlers/core.d.ts +13 -0
  28. package/dist/commands-v2/handlers/core.js +165 -0
  29. package/dist/commands-v2/handlers/debug.d.ts +11 -0
  30. package/dist/commands-v2/handlers/debug.js +159 -0
  31. package/dist/commands-v2/handlers/index.d.ts +12 -0
  32. package/dist/commands-v2/handlers/index.js +24 -0
  33. package/dist/commands-v2/handlers/project.d.ts +22 -0
  34. package/dist/commands-v2/handlers/project.js +814 -0
  35. package/dist/commands-v2/handlers/settings.d.ts +15 -0
  36. package/dist/commands-v2/handlers/settings.js +235 -0
  37. package/dist/commands-v2/index.d.ts +13 -0
  38. package/dist/commands-v2/index.js +15 -0
  39. package/dist/commands-v2/registry.d.ts +37 -0
  40. package/dist/commands-v2/registry.js +80 -0
  41. package/dist/commands-v2/types.d.ts +75 -0
  42. package/dist/commands-v2/types.js +7 -0
  43. package/dist/commands.js +110 -7
  44. package/dist/index.js +288 -29
  45. package/dist/input-handlers/index.d.ts +7 -0
  46. package/dist/input-handlers/index.js +7 -0
  47. package/dist/input-handlers/memory-handler.d.ts +26 -0
  48. package/dist/input-handlers/memory-handler.js +68 -0
  49. package/dist/repl-helpers.d.ts +63 -0
  50. package/dist/repl-helpers.js +318 -0
  51. package/dist/repl-v2.d.ts +155 -0
  52. package/dist/repl-v2.js +774 -0
  53. package/dist/repl.d.ts +32 -4
  54. package/dist/repl.js +250 -977
  55. package/dist/settings/index.d.ts +23 -0
  56. package/dist/settings/index.js +48 -0
  57. package/dist/settings/paths.d.ts +110 -0
  58. package/dist/settings/paths.js +264 -0
  59. package/dist/templates/compilr-md.js +7 -4
  60. package/dist/templates/index.js +3 -4
  61. package/dist/themes/colors.js +3 -1
  62. package/dist/themes/registry.d.ts +5 -36
  63. package/dist/themes/registry.js +11 -95
  64. package/dist/themes/types.d.ts +3 -38
  65. package/dist/themes/types.js +2 -2
  66. package/dist/tools/anchor-tools.d.ts +31 -0
  67. package/dist/tools/anchor-tools.js +255 -0
  68. package/dist/tools/backlog-wrappers.d.ts +54 -0
  69. package/dist/tools/backlog-wrappers.js +338 -0
  70. package/dist/tools/backlog.js +1 -1
  71. package/dist/tools/db-tools.d.ts +65 -0
  72. package/dist/tools/db-tools.js +19 -0
  73. package/dist/tools/document-db.d.ts +43 -0
  74. package/dist/tools/document-db.js +220 -0
  75. package/dist/tools/project-db.d.ts +102 -0
  76. package/dist/tools/project-db.js +370 -0
  77. package/dist/tools/workitem-db.d.ts +103 -0
  78. package/dist/tools/workitem-db.js +549 -0
  79. package/dist/tools.js +13 -3
  80. package/dist/ui/agents-overlay-v2.d.ts +43 -0
  81. package/dist/ui/agents-overlay-v2.js +809 -0
  82. package/dist/ui/agents-overlay.d.ts +5 -5
  83. package/dist/ui/agents-overlay.js +782 -420
  84. package/dist/ui/anchors-overlay.d.ts +12 -0
  85. package/dist/ui/anchors-overlay.js +775 -0
  86. package/dist/ui/arch-type-overlay.d.ts +1 -6
  87. package/dist/ui/arch-type-overlay.js +175 -203
  88. package/dist/ui/ask-user-overlay-v2.d.ts +26 -0
  89. package/dist/ui/ask-user-overlay-v2.js +555 -0
  90. package/dist/ui/ask-user-overlay.d.ts +2 -2
  91. package/dist/ui/ask-user-overlay.js +443 -535
  92. package/dist/ui/ask-user-simple-overlay-v2.d.ts +25 -0
  93. package/dist/ui/ask-user-simple-overlay-v2.js +215 -0
  94. package/dist/ui/ask-user-simple-overlay.d.ts +2 -2
  95. package/dist/ui/ask-user-simple-overlay.js +182 -209
  96. package/dist/ui/backlog-overlay.d.ts +16 -1
  97. package/dist/ui/backlog-overlay.js +525 -659
  98. package/dist/ui/base/index.d.ts +26 -0
  99. package/dist/ui/base/index.js +33 -0
  100. package/dist/ui/base/inline-overlay-utils.d.ts +217 -0
  101. package/dist/ui/base/inline-overlay-utils.js +320 -0
  102. package/dist/ui/base/inline-overlay.d.ts +159 -0
  103. package/dist/ui/base/inline-overlay.js +257 -0
  104. package/dist/ui/base/key-utils.d.ts +15 -0
  105. package/dist/ui/base/key-utils.js +30 -0
  106. package/dist/ui/base/overlay-base-v2.d.ts +193 -0
  107. package/dist/ui/base/overlay-base-v2.js +246 -0
  108. package/dist/ui/base/overlay-base.d.ts +156 -0
  109. package/dist/ui/base/overlay-base.js +238 -0
  110. package/dist/ui/base/overlay-lifecycle.d.ts +65 -0
  111. package/dist/ui/base/overlay-lifecycle.js +159 -0
  112. package/dist/ui/base/overlay-types.d.ts +185 -0
  113. package/dist/ui/base/overlay-types.js +7 -0
  114. package/dist/ui/base/render-utils.d.ts +8 -0
  115. package/dist/ui/base/render-utils.js +11 -0
  116. package/dist/ui/base/screen-stack.d.ts +148 -0
  117. package/dist/ui/base/screen-stack.js +184 -0
  118. package/dist/ui/base/tabbed-list-overlay-v2.d.ts +103 -0
  119. package/dist/ui/base/tabbed-list-overlay-v2.js +317 -0
  120. package/dist/ui/base/tabbed-list-overlay.d.ts +153 -0
  121. package/dist/ui/base/tabbed-list-overlay.js +369 -0
  122. package/dist/ui/commands-overlay-v2.d.ts +33 -0
  123. package/dist/ui/commands-overlay-v2.js +441 -0
  124. package/dist/ui/commands-overlay.d.ts +7 -2
  125. package/dist/ui/commands-overlay.js +384 -355
  126. package/dist/ui/config-overlay.d.ts +5 -4
  127. package/dist/ui/config-overlay.js +243 -513
  128. package/dist/ui/conversation.d.ts +75 -4
  129. package/dist/ui/conversation.js +374 -161
  130. package/dist/ui/docs-overlay.d.ts +17 -0
  131. package/dist/ui/docs-overlay.js +303 -0
  132. package/dist/ui/ephemeral.d.ts +1 -1
  133. package/dist/ui/ephemeral.js +1 -1
  134. package/dist/ui/features/index.d.ts +34 -0
  135. package/dist/ui/features/index.js +34 -0
  136. package/dist/ui/features/input-feature.d.ts +85 -0
  137. package/dist/ui/features/input-feature.js +238 -0
  138. package/dist/ui/features/list-feature.d.ts +155 -0
  139. package/dist/ui/features/list-feature.js +244 -0
  140. package/dist/ui/features/pagination-feature.d.ts +154 -0
  141. package/dist/ui/features/pagination-feature.js +238 -0
  142. package/dist/ui/features/search-feature.d.ts +148 -0
  143. package/dist/ui/features/search-feature.js +185 -0
  144. package/dist/ui/features/tab-feature.d.ts +194 -0
  145. package/dist/ui/features/tab-feature.js +307 -0
  146. package/dist/ui/footer-v2.d.ts +222 -0
  147. package/dist/ui/footer-v2.js +1349 -0
  148. package/dist/ui/footer.d.ts +107 -0
  149. package/dist/ui/footer.js +359 -67
  150. package/dist/ui/guardrail-overlay.d.ts +29 -0
  151. package/dist/ui/guardrail-overlay.js +145 -0
  152. package/dist/ui/help-overlay-v2.d.ts +34 -0
  153. package/dist/ui/help-overlay-v2.js +309 -0
  154. package/dist/ui/help-overlay.d.ts +16 -0
  155. package/dist/ui/help-overlay.js +316 -0
  156. package/dist/ui/index.d.ts +1 -1
  157. package/dist/ui/index.js +1 -3
  158. package/dist/ui/init-overlay-v2.d.ts +34 -0
  159. package/dist/ui/init-overlay-v2.js +600 -0
  160. package/dist/ui/init-overlay.d.ts +12 -2
  161. package/dist/ui/init-overlay.js +349 -270
  162. package/dist/ui/input-prompt-v2.d.ts +1 -0
  163. package/dist/ui/input-prompt-v2.js +14 -6
  164. package/dist/ui/input-prompt.d.ts +116 -33
  165. package/dist/ui/input-prompt.js +536 -337
  166. package/dist/ui/iteration-limit-overlay-v2.d.ts +21 -0
  167. package/dist/ui/iteration-limit-overlay-v2.js +114 -0
  168. package/dist/ui/iteration-limit-overlay.d.ts +2 -2
  169. package/dist/ui/iteration-limit-overlay.js +92 -128
  170. package/dist/ui/keys-overlay-v2.d.ts +41 -0
  171. package/dist/ui/keys-overlay-v2.js +248 -0
  172. package/dist/ui/keys-overlay.d.ts +1 -0
  173. package/dist/ui/keys-overlay.js +203 -141
  174. package/dist/ui/line-utils.d.ts +88 -0
  175. package/dist/ui/line-utils.js +150 -0
  176. package/dist/ui/live-region.d.ts +161 -0
  177. package/dist/ui/live-region.js +387 -0
  178. package/dist/ui/mascot/expressions.d.ts +32 -0
  179. package/dist/ui/mascot/expressions.js +213 -0
  180. package/dist/ui/mascot/index.d.ts +8 -0
  181. package/dist/ui/mascot/index.js +8 -0
  182. package/dist/ui/mascot/renderer.d.ts +19 -0
  183. package/dist/ui/mascot/renderer.js +97 -0
  184. package/dist/ui/mascot-overlay-v2.d.ts +41 -0
  185. package/dist/ui/mascot-overlay-v2.js +138 -0
  186. package/dist/ui/mascot-overlay.d.ts +21 -0
  187. package/dist/ui/mascot-overlay.js +146 -0
  188. package/dist/ui/model-overlay-v2.d.ts +49 -0
  189. package/dist/ui/model-overlay-v2.js +118 -0
  190. package/dist/ui/model-overlay.d.ts +27 -0
  191. package/dist/ui/model-overlay.js +221 -0
  192. package/dist/ui/model-warning-overlay.js +3 -5
  193. package/dist/ui/new-overlay.d.ts +34 -0
  194. package/dist/ui/new-overlay.js +604 -0
  195. package/dist/ui/overlay/impl/agents-overlay-v2.d.ts +45 -0
  196. package/dist/ui/overlay/impl/agents-overlay-v2.js +825 -0
  197. package/dist/ui/overlay/impl/anchors-overlay-v2.d.ts +47 -0
  198. package/dist/ui/overlay/impl/anchors-overlay-v2.js +783 -0
  199. package/dist/ui/overlay/impl/arch-type-overlay-v2.d.ts +37 -0
  200. package/dist/ui/overlay/impl/arch-type-overlay-v2.js +240 -0
  201. package/dist/ui/overlay/impl/ask-user-overlay-v2.d.ts +72 -0
  202. package/dist/ui/overlay/impl/ask-user-overlay-v2.js +584 -0
  203. package/dist/ui/overlay/impl/ask-user-simple-overlay-v2.d.ts +46 -0
  204. package/dist/ui/overlay/impl/ask-user-simple-overlay-v2.js +204 -0
  205. package/dist/ui/overlay/impl/backlog-overlay-v2.d.ts +49 -0
  206. package/dist/ui/overlay/impl/backlog-overlay-v2.js +642 -0
  207. package/dist/ui/overlay/impl/commands-overlay-v2.d.ts +33 -0
  208. package/dist/ui/overlay/impl/commands-overlay-v2.js +441 -0
  209. package/dist/ui/overlay/impl/config-overlay-v2.d.ts +100 -0
  210. package/dist/ui/overlay/impl/config-overlay-v2.js +654 -0
  211. package/dist/ui/overlay/impl/dashboard-overlay-v2.d.ts +55 -0
  212. package/dist/ui/overlay/impl/dashboard-overlay-v2.js +359 -0
  213. package/dist/ui/overlay/impl/docs-overlay-v2.d.ts +45 -0
  214. package/dist/ui/overlay/impl/docs-overlay-v2.js +114 -0
  215. package/dist/ui/overlay/impl/document-detail-overlay-v2.d.ts +77 -0
  216. package/dist/ui/overlay/impl/document-detail-overlay-v2.js +1071 -0
  217. package/dist/ui/overlay/impl/guardrail-overlay-v2.d.ts +43 -0
  218. package/dist/ui/overlay/impl/guardrail-overlay-v2.js +114 -0
  219. package/dist/ui/overlay/impl/help-overlay-v2.d.ts +34 -0
  220. package/dist/ui/overlay/impl/help-overlay-v2.js +309 -0
  221. package/dist/ui/overlay/impl/init-overlay-v2.d.ts +77 -0
  222. package/dist/ui/overlay/impl/init-overlay-v2.js +593 -0
  223. package/dist/ui/overlay/impl/init-setup-overlay-v2.d.ts +25 -0
  224. package/dist/ui/overlay/impl/init-setup-overlay-v2.js +97 -0
  225. package/dist/ui/overlay/impl/iteration-limit-overlay-v2.d.ts +35 -0
  226. package/dist/ui/overlay/impl/iteration-limit-overlay-v2.js +105 -0
  227. package/dist/ui/overlay/impl/keys-overlay-v2.d.ts +41 -0
  228. package/dist/ui/overlay/impl/keys-overlay-v2.js +248 -0
  229. package/dist/ui/overlay/impl/mascot-overlay-v2.d.ts +41 -0
  230. package/dist/ui/overlay/impl/mascot-overlay-v2.js +138 -0
  231. package/dist/ui/overlay/impl/model-overlay-v2.d.ts +49 -0
  232. package/dist/ui/overlay/impl/model-overlay-v2.js +118 -0
  233. package/dist/ui/overlay/impl/model-warning-overlay-v2.d.ts +46 -0
  234. package/dist/ui/overlay/impl/model-warning-overlay-v2.js +132 -0
  235. package/dist/ui/overlay/impl/new-overlay-v2.d.ts +77 -0
  236. package/dist/ui/overlay/impl/new-overlay-v2.js +593 -0
  237. package/dist/ui/overlay/impl/permission-overlay-v2.d.ts +36 -0
  238. package/dist/ui/overlay/impl/permission-overlay-v2.js +380 -0
  239. package/dist/ui/overlay/impl/projects-overlay-v2.d.ts +36 -0
  240. package/dist/ui/overlay/impl/projects-overlay-v2.js +499 -0
  241. package/dist/ui/overlay/impl/theme-overlay-v2.d.ts +42 -0
  242. package/dist/ui/overlay/impl/theme-overlay-v2.js +135 -0
  243. package/dist/ui/overlay/impl/tools-overlay-v2.d.ts +47 -0
  244. package/dist/ui/overlay/impl/tools-overlay-v2.js +218 -0
  245. package/dist/ui/overlay/impl/tutorial-overlay-v2.d.ts +31 -0
  246. package/dist/ui/overlay/impl/tutorial-overlay-v2.js +1035 -0
  247. package/dist/ui/overlay/impl/workflow-overlay-v2.d.ts +80 -0
  248. package/dist/ui/overlay/impl/workflow-overlay-v2.js +637 -0
  249. package/dist/ui/overlay/index.d.ts +33 -0
  250. package/dist/ui/overlay/index.js +35 -0
  251. package/dist/ui/overlay/key-utils.d.ts +6 -0
  252. package/dist/ui/overlay/key-utils.js +6 -0
  253. package/dist/ui/overlay/overlay-types.d.ts +128 -0
  254. package/dist/ui/overlay/overlay-types.js +22 -0
  255. package/dist/ui/overlay/types.d.ts +135 -0
  256. package/dist/ui/overlay/types.js +22 -0
  257. package/dist/ui/overlays/help-overlay-v2.d.ts +28 -0
  258. package/dist/ui/overlays/help-overlay-v2.js +198 -0
  259. package/dist/ui/overlays/index.d.ts +11 -0
  260. package/dist/ui/overlays/index.js +11 -0
  261. package/dist/ui/overlays.d.ts +0 -4
  262. package/dist/ui/overlays.js +0 -444
  263. package/dist/ui/permission-overlay-v2.d.ts +36 -0
  264. package/dist/ui/permission-overlay-v2.js +380 -0
  265. package/dist/ui/permission-overlay.d.ts +1 -1
  266. package/dist/ui/permission-overlay.js +186 -298
  267. package/dist/ui/projects-overlay.d.ts +19 -0
  268. package/dist/ui/projects-overlay.js +484 -0
  269. package/dist/ui/providers/types.d.ts +178 -0
  270. package/dist/ui/providers/types.js +9 -0
  271. package/dist/ui/render-modes.d.ts +36 -0
  272. package/dist/ui/render-modes.js +44 -0
  273. package/dist/ui/startup-menu.d.ts +36 -0
  274. package/dist/ui/startup-menu.js +236 -0
  275. package/dist/ui/subagent-renderer.d.ts +117 -0
  276. package/dist/ui/subagent-renderer.js +334 -0
  277. package/dist/ui/terminal-codes.d.ts +94 -0
  278. package/dist/ui/terminal-codes.js +124 -0
  279. package/dist/ui/terminal-renderer.d.ts +221 -0
  280. package/dist/ui/terminal-renderer.js +751 -0
  281. package/dist/ui/terminal-ui.d.ts +463 -0
  282. package/dist/ui/terminal-ui.js +2296 -0
  283. package/dist/ui/terminal.d.ts +20 -0
  284. package/dist/ui/terminal.js +72 -0
  285. package/dist/ui/theme-overlay-v2.d.ts +42 -0
  286. package/dist/ui/theme-overlay-v2.js +135 -0
  287. package/dist/ui/theme-overlay.d.ts +24 -0
  288. package/dist/ui/theme-overlay.js +127 -0
  289. package/dist/ui/todo-zone.js +53 -25
  290. package/dist/ui/tool-formatters.d.ts +16 -0
  291. package/dist/ui/tool-formatters.js +516 -0
  292. package/dist/ui/tools-overlay-v2.d.ts +47 -0
  293. package/dist/ui/tools-overlay-v2.js +218 -0
  294. package/dist/ui/tools-overlay.d.ts +10 -2
  295. package/dist/ui/tools-overlay.js +172 -220
  296. package/dist/ui/tutorial-overlay-v2.d.ts +31 -0
  297. package/dist/ui/tutorial-overlay-v2.js +1035 -0
  298. package/dist/ui/tutorial-overlay.d.ts +1 -0
  299. package/dist/ui/tutorial-overlay.js +400 -302
  300. package/dist/ui/workflow-overlay.d.ts +22 -0
  301. package/dist/ui/workflow-overlay.js +636 -0
  302. package/dist/utils/debug-log.d.ts +28 -0
  303. package/dist/utils/debug-log.js +57 -0
  304. package/dist/utils/model-tiers.js +1 -1
  305. package/dist/utils/path-safety.d.ts +56 -0
  306. package/dist/utils/path-safety.js +239 -0
  307. package/dist/workflow/guided-mode-injector.d.ts +42 -0
  308. package/dist/workflow/guided-mode-injector.js +191 -0
  309. package/dist/workflow/index.d.ts +8 -0
  310. package/dist/workflow/index.js +8 -0
  311. package/dist/workflow/step-criteria.d.ts +62 -0
  312. package/dist/workflow/step-criteria.js +150 -0
  313. package/dist/workflow/step-tracker.d.ts +92 -0
  314. package/dist/workflow/step-tracker.js +141 -0
  315. package/package.json +12 -5
@@ -0,0 +1,185 @@
1
+ /**
2
+ * Search Feature
3
+ *
4
+ * Composable feature for search/filter text input in overlays.
5
+ * Handles typing, backspace, and escape to clear.
6
+ *
7
+ * Usage:
8
+ * ```typescript
9
+ * interface MyState extends OverlayState, SearchState {
10
+ * items: string[];
11
+ * }
12
+ *
13
+ * class MyScreen extends BaseScreen {
14
+ * private search = new SearchFeature({
15
+ * onSearch: (query) => this.filterItems(query),
16
+ * });
17
+ *
18
+ * handleKey(data: Buffer): ScreenResult {
19
+ * const result = this.search.handleKey(data, this.state);
20
+ * if (result?.handled) {
21
+ * return result.action === 'render' ? stay() : stay(false);
22
+ * }
23
+ * // ... other key handling
24
+ * }
25
+ *
26
+ * render(): string[] {
27
+ * return [
28
+ * this.search.renderSearchBox(this.state, styles, { label: 'Search' }),
29
+ * ...filteredItemLines,
30
+ * ];
31
+ * }
32
+ * }
33
+ * ```
34
+ */
35
+ import { isBackspace, isEscape, isPrintable, getPrintableChar, } from '../base/key-utils.js';
36
+ // =============================================================================
37
+ // SearchFeature Class
38
+ // =============================================================================
39
+ /**
40
+ * Feature for search/filter text input.
41
+ *
42
+ * Handles:
43
+ * - Printable character input
44
+ * - Backspace to delete
45
+ * - Escape to clear (optional)
46
+ */
47
+ export class SearchFeature {
48
+ config;
49
+ constructor(config = {}) {
50
+ this.config = {
51
+ escapeClearsSearch: true,
52
+ placeholder: '',
53
+ ...config,
54
+ };
55
+ }
56
+ // ===========================================================================
57
+ // Key Handling
58
+ // ===========================================================================
59
+ /**
60
+ * Handle a key press.
61
+ *
62
+ * @param data - Raw key buffer
63
+ * @param state - State containing searchQuery
64
+ * @returns Result if handled, null otherwise
65
+ */
66
+ handleKey(data, state) {
67
+ // Escape clears search
68
+ if (this.config.escapeClearsSearch && isEscape(data)) {
69
+ if (state.searchQuery.length > 0) {
70
+ state.searchQuery = '';
71
+ this.config.onSearch?.(state.searchQuery);
72
+ return { handled: true, action: 'render', cleared: true };
73
+ }
74
+ // Don't handle Escape if search is already empty
75
+ return null;
76
+ }
77
+ // Backspace deletes last character
78
+ if (isBackspace(data)) {
79
+ if (state.searchQuery.length > 0) {
80
+ state.searchQuery = state.searchQuery.slice(0, -1);
81
+ this.config.onSearch?.(state.searchQuery);
82
+ return { handled: true, action: 'render' };
83
+ }
84
+ return { handled: true, action: 'none' };
85
+ }
86
+ // Printable characters add to search
87
+ if (isPrintable(data)) {
88
+ const char = getPrintableChar(data);
89
+ if (char) {
90
+ state.searchQuery += char;
91
+ this.config.onSearch?.(state.searchQuery);
92
+ return { handled: true, action: 'render' };
93
+ }
94
+ }
95
+ return null;
96
+ }
97
+ // ===========================================================================
98
+ // Rendering
99
+ // ===========================================================================
100
+ /**
101
+ * Render a search box with optional cursor.
102
+ *
103
+ * @param state - State containing searchQuery
104
+ * @param styles - Theme styles
105
+ * @param options - Rendering options
106
+ * @returns Formatted search box string
107
+ */
108
+ renderSearchBox(state, styles, options = {}) {
109
+ const label = options.label ?? 'Search';
110
+ const showCursor = options.showCursor ?? true;
111
+ const prefix = options.prefix ?? ' ';
112
+ const cursor = showCursor ? '█' : '';
113
+ const query = state.searchQuery;
114
+ const placeholder = !query && this.config.placeholder
115
+ ? styles.muted(this.config.placeholder)
116
+ : '';
117
+ if (query) {
118
+ const styledQuery = styles.primary
119
+ ? styles.primary(query)
120
+ : query;
121
+ return `${prefix}${label}: ${styledQuery}${cursor}`;
122
+ }
123
+ return `${prefix}${label}: ${placeholder}${cursor}`;
124
+ }
125
+ /**
126
+ * Render search hints for the footer.
127
+ *
128
+ * @param hasQuery - Whether there's an active search query
129
+ * @returns Hint string
130
+ */
131
+ renderHints(hasQuery = false) {
132
+ if (hasQuery && this.config.escapeClearsSearch) {
133
+ return 'Type to search · Esc Clear';
134
+ }
135
+ return 'Type to search';
136
+ }
137
+ // ===========================================================================
138
+ // Utilities
139
+ // ===========================================================================
140
+ /**
141
+ * Check if there's an active search query.
142
+ */
143
+ hasQuery(state) {
144
+ return state.searchQuery.length > 0;
145
+ }
146
+ /**
147
+ * Get the current search query.
148
+ */
149
+ getQuery(state) {
150
+ return state.searchQuery;
151
+ }
152
+ /**
153
+ * Clear the search query.
154
+ */
155
+ clear(state) {
156
+ state.searchQuery = '';
157
+ this.config.onSearch?.(state.searchQuery);
158
+ }
159
+ /**
160
+ * Set the search query programmatically.
161
+ */
162
+ setQuery(state, query) {
163
+ state.searchQuery = query;
164
+ this.config.onSearch?.(state.searchQuery);
165
+ }
166
+ /**
167
+ * Filter items based on search query.
168
+ * Generic helper that works with any item type.
169
+ *
170
+ * @param items - Items to filter
171
+ * @param state - State containing searchQuery
172
+ * @param getSearchableText - Function to extract searchable text from item
173
+ * @returns Filtered items
174
+ */
175
+ filterItems(items, state, getSearchableText) {
176
+ const query = state.searchQuery.toLowerCase().trim();
177
+ if (!query) {
178
+ return items;
179
+ }
180
+ return items.filter((item) => {
181
+ const text = getSearchableText(item).toLowerCase();
182
+ return text.includes(query);
183
+ });
184
+ }
185
+ }
@@ -0,0 +1,194 @@
1
+ /**
2
+ * Tab Feature
3
+ *
4
+ * Composable feature for tab/filter navigation.
5
+ * Handles Tab/Shift+Tab and number keys for tab switching.
6
+ *
7
+ * Note: Arrow keys (←→/hl) are NOT used for tabs - they are reserved
8
+ * for page navigation in paginated overlays. This ensures consistent
9
+ * keyboard behavior across all overlays.
10
+ *
11
+ * Usage:
12
+ * ```typescript
13
+ * const TABS = [
14
+ * { id: 'all', label: 'All' },
15
+ * { id: 'active', label: 'Active' },
16
+ * { id: 'completed', label: 'Completed' },
17
+ * ];
18
+ *
19
+ * interface MyState extends OverlayState, TabState {
20
+ * // ...
21
+ * }
22
+ *
23
+ * class MyOverlay extends BaseOverlay<MyState, void> {
24
+ * private tabFeature = new TabFeature(TABS, (tabId) => {
25
+ * this.onTabChange(tabId);
26
+ * });
27
+ *
28
+ * handleKey(data: Buffer): void {
29
+ * const result = this.tabFeature.handleKey(data, this.state);
30
+ * if (result?.handled) {
31
+ * if (result.action === 'render') this.update();
32
+ * return;
33
+ * }
34
+ * // ... other key handling
35
+ * }
36
+ *
37
+ * render(): string[] {
38
+ * return [
39
+ * ...this.renderHeader('My Overlay'),
40
+ * ' ' + this.tabFeature.renderTabs(this.state, this.styles),
41
+ * '',
42
+ * // ... content
43
+ * ...this.renderFooter(this.tabFeature.renderHints() + ' · ↑↓/jk Navigate')
44
+ * ];
45
+ * }
46
+ * }
47
+ * ```
48
+ */
49
+ import type { KeyResult, TabState, TabDefinition } from '../base/overlay-types.js';
50
+ /**
51
+ * Configuration for TabFeature.
52
+ */
53
+ export interface TabFeatureConfig {
54
+ /**
55
+ * Tab definitions.
56
+ */
57
+ tabs: TabDefinition[];
58
+ /**
59
+ * Callback when tab changes.
60
+ * @param tabId - ID of the new tab
61
+ * @param index - Index of the new tab
62
+ */
63
+ onChange?: (tabId: string, index: number) => void;
64
+ /**
65
+ * Whether to use number keys (1-9) for direct tab selection.
66
+ * Default: true (if tabs.length <= 9)
67
+ */
68
+ useNumberKeys?: boolean;
69
+ /**
70
+ * Whether navigation wraps at boundaries.
71
+ * Default: true
72
+ */
73
+ wrapNavigation?: boolean;
74
+ }
75
+ /**
76
+ * Feature for tab/filter navigation.
77
+ *
78
+ * Handles:
79
+ * - Tab/Shift+Tab for cycling tabs
80
+ * - Number keys 1-9 for direct selection (optional)
81
+ *
82
+ * Note: Arrow keys are NOT handled - they are reserved for pagination.
83
+ */
84
+ export declare class TabFeature {
85
+ private readonly config;
86
+ /**
87
+ * Create a new TabFeature.
88
+ *
89
+ * @param tabs - Tab definitions
90
+ * @param onChange - Optional callback when tab changes
91
+ */
92
+ constructor(tabs: TabDefinition[], onChange?: (tabId: string, index: number) => void);
93
+ /**
94
+ * Create from config object (alternative constructor).
95
+ */
96
+ static fromConfig(config: TabFeatureConfig): TabFeature;
97
+ /**
98
+ * Handle a key press.
99
+ *
100
+ * @param data - Raw key buffer
101
+ * @param state - State containing currentTab
102
+ * @returns KeyResult if handled, null otherwise
103
+ */
104
+ handleKey(data: Buffer, state: TabState): KeyResult | null;
105
+ /**
106
+ * Render tabs in a row.
107
+ *
108
+ * @param state - State containing currentTab
109
+ * @param styles - Theme styles
110
+ * @returns Rendered tab bar string
111
+ */
112
+ renderTabs(state: TabState, styles: {
113
+ selected: (s: string) => string;
114
+ muted: (s: string) => string;
115
+ }): string;
116
+ /**
117
+ * Render tabs with number key hints.
118
+ *
119
+ * @param state - State containing currentTab
120
+ * @param styles - Theme styles
121
+ * @returns Rendered tab bar string with numbers
122
+ */
123
+ renderTabsWithNumbers(state: TabState, styles: {
124
+ selected: (s: string) => string;
125
+ muted: (s: string) => string;
126
+ }): string;
127
+ /**
128
+ * Render tabs with custom separator.
129
+ *
130
+ * @param state - State containing currentTab
131
+ * @param styles - Theme styles
132
+ * @param separator - Separator between tabs (default: '')
133
+ * @returns Rendered tab bar string
134
+ */
135
+ renderTabsWithSeparator(state: TabState, styles: {
136
+ selected: (s: string) => string;
137
+ muted: (s: string) => string;
138
+ }, separator?: string): string;
139
+ /**
140
+ * Render keyboard hints for tabs.
141
+ *
142
+ * @returns Hint string like "Tab Switch · 1-5 Jump"
143
+ */
144
+ renderHints(): string;
145
+ /**
146
+ * Get the current tab definition.
147
+ *
148
+ * @param state - State containing currentTab
149
+ * @returns Current tab or undefined
150
+ */
151
+ getCurrentTab(state: TabState): TabDefinition | undefined;
152
+ /**
153
+ * Get the current tab ID.
154
+ *
155
+ * @param state - State containing currentTab
156
+ * @returns Current tab ID or empty string
157
+ */
158
+ getCurrentTabId(state: TabState): string;
159
+ /**
160
+ * Get the number of tabs.
161
+ */
162
+ getTabCount(): number;
163
+ /**
164
+ * Find tab index by ID.
165
+ *
166
+ * @param tabId - Tab ID to find
167
+ * @returns Index or -1 if not found
168
+ */
169
+ findTabIndex(tabId: string): number;
170
+ /**
171
+ * Set current tab by ID.
172
+ *
173
+ * @param state - State to modify
174
+ * @param tabId - Tab ID to select
175
+ * @returns true if tab was found and selected
176
+ */
177
+ setTabById(state: TabState, tabId: string): boolean;
178
+ private notifyChange;
179
+ }
180
+ /**
181
+ * Create tab definitions from an array of strings.
182
+ * IDs are lowercase versions of labels.
183
+ *
184
+ * @param labels - Tab labels
185
+ * @returns Tab definitions
186
+ */
187
+ export declare function createTabsFromLabels(labels: string[]): TabDefinition[];
188
+ /**
189
+ * Create filter tabs (common pattern).
190
+ *
191
+ * @param filters - Array of filter names
192
+ * @returns Tab definitions with 'all' as first tab
193
+ */
194
+ export declare function createFilterTabs(filters: string[]): TabDefinition[];
@@ -0,0 +1,307 @@
1
+ /**
2
+ * Tab Feature
3
+ *
4
+ * Composable feature for tab/filter navigation.
5
+ * Handles Tab/Shift+Tab and number keys for tab switching.
6
+ *
7
+ * Note: Arrow keys (←→/hl) are NOT used for tabs - they are reserved
8
+ * for page navigation in paginated overlays. This ensures consistent
9
+ * keyboard behavior across all overlays.
10
+ *
11
+ * Usage:
12
+ * ```typescript
13
+ * const TABS = [
14
+ * { id: 'all', label: 'All' },
15
+ * { id: 'active', label: 'Active' },
16
+ * { id: 'completed', label: 'Completed' },
17
+ * ];
18
+ *
19
+ * interface MyState extends OverlayState, TabState {
20
+ * // ...
21
+ * }
22
+ *
23
+ * class MyOverlay extends BaseOverlay<MyState, void> {
24
+ * private tabFeature = new TabFeature(TABS, (tabId) => {
25
+ * this.onTabChange(tabId);
26
+ * });
27
+ *
28
+ * handleKey(data: Buffer): void {
29
+ * const result = this.tabFeature.handleKey(data, this.state);
30
+ * if (result?.handled) {
31
+ * if (result.action === 'render') this.update();
32
+ * return;
33
+ * }
34
+ * // ... other key handling
35
+ * }
36
+ *
37
+ * render(): string[] {
38
+ * return [
39
+ * ...this.renderHeader('My Overlay'),
40
+ * ' ' + this.tabFeature.renderTabs(this.state, this.styles),
41
+ * '',
42
+ * // ... content
43
+ * ...this.renderFooter(this.tabFeature.renderHints() + ' · ↑↓/jk Navigate')
44
+ * ];
45
+ * }
46
+ * }
47
+ * ```
48
+ */
49
+ import { isTab, isShiftTab, getNumberKey, } from '../base/key-utils.js';
50
+ // =============================================================================
51
+ // TabFeature Class
52
+ // =============================================================================
53
+ /**
54
+ * Feature for tab/filter navigation.
55
+ *
56
+ * Handles:
57
+ * - Tab/Shift+Tab for cycling tabs
58
+ * - Number keys 1-9 for direct selection (optional)
59
+ *
60
+ * Note: Arrow keys are NOT handled - they are reserved for pagination.
61
+ */
62
+ export class TabFeature {
63
+ config;
64
+ /**
65
+ * Create a new TabFeature.
66
+ *
67
+ * @param tabs - Tab definitions
68
+ * @param onChange - Optional callback when tab changes
69
+ */
70
+ constructor(tabs, onChange) {
71
+ this.config = {
72
+ tabs,
73
+ onChange,
74
+ useNumberKeys: tabs.length <= 9,
75
+ wrapNavigation: true,
76
+ };
77
+ }
78
+ /**
79
+ * Create from config object (alternative constructor).
80
+ */
81
+ static fromConfig(config) {
82
+ const feature = new TabFeature(config.tabs, config.onChange);
83
+ if (config.useNumberKeys !== undefined) {
84
+ feature.config.useNumberKeys = config.useNumberKeys;
85
+ }
86
+ if (config.wrapNavigation !== undefined) {
87
+ feature.config.wrapNavigation = config.wrapNavigation;
88
+ }
89
+ return feature;
90
+ }
91
+ // ===========================================================================
92
+ // Key Handling
93
+ // ===========================================================================
94
+ /**
95
+ * Handle a key press.
96
+ *
97
+ * @param data - Raw key buffer
98
+ * @param state - State containing currentTab
99
+ * @returns KeyResult if handled, null otherwise
100
+ */
101
+ handleKey(data, state) {
102
+ const tabCount = this.config.tabs.length;
103
+ if (tabCount === 0) {
104
+ return null; // No tabs
105
+ }
106
+ // Tab - next tab
107
+ if (isTab(data)) {
108
+ const newIndex = this.config.wrapNavigation
109
+ ? (state.currentTab + 1) % tabCount
110
+ : Math.min(state.currentTab + 1, tabCount - 1);
111
+ if (newIndex !== state.currentTab) {
112
+ state.currentTab = newIndex;
113
+ this.notifyChange(state);
114
+ return { handled: true, action: 'render' };
115
+ }
116
+ return { handled: true }; // At end, consume but no change
117
+ }
118
+ // Shift+Tab - previous tab
119
+ if (isShiftTab(data)) {
120
+ const newIndex = this.config.wrapNavigation
121
+ ? (state.currentTab - 1 + tabCount) % tabCount
122
+ : Math.max(state.currentTab - 1, 0);
123
+ if (newIndex !== state.currentTab) {
124
+ state.currentTab = newIndex;
125
+ this.notifyChange(state);
126
+ return { handled: true, action: 'render' };
127
+ }
128
+ return { handled: true }; // At start, consume but no change
129
+ }
130
+ // Number keys 1-9 for direct selection
131
+ if (this.config.useNumberKeys) {
132
+ const num = getNumberKey(data);
133
+ if (num !== null && num >= 1 && num <= tabCount) {
134
+ const newIndex = num - 1;
135
+ if (newIndex !== state.currentTab) {
136
+ state.currentTab = newIndex;
137
+ this.notifyChange(state);
138
+ return { handled: true, action: 'render' };
139
+ }
140
+ return { handled: true }; // Already on this tab
141
+ }
142
+ }
143
+ return null; // Not handled
144
+ }
145
+ // ===========================================================================
146
+ // Rendering
147
+ // ===========================================================================
148
+ /**
149
+ * Render tabs in a row.
150
+ *
151
+ * @param state - State containing currentTab
152
+ * @param styles - Theme styles
153
+ * @returns Rendered tab bar string
154
+ */
155
+ renderTabs(state, styles) {
156
+ return this.config.tabs
157
+ .map((tab, index) => {
158
+ const label = ` ${tab.label} `;
159
+ return index === state.currentTab
160
+ ? styles.selected(label)
161
+ : styles.muted(label);
162
+ })
163
+ .join('');
164
+ }
165
+ /**
166
+ * Render tabs with number key hints.
167
+ *
168
+ * @param state - State containing currentTab
169
+ * @param styles - Theme styles
170
+ * @returns Rendered tab bar string with numbers
171
+ */
172
+ renderTabsWithNumbers(state, styles) {
173
+ return this.config.tabs
174
+ .map((tab, index) => {
175
+ const num = String(index + 1);
176
+ const label = ` ${num}:${tab.label} `;
177
+ return index === state.currentTab
178
+ ? styles.selected(label)
179
+ : styles.muted(label);
180
+ })
181
+ .join('');
182
+ }
183
+ /**
184
+ * Render tabs with custom separator.
185
+ *
186
+ * @param state - State containing currentTab
187
+ * @param styles - Theme styles
188
+ * @param separator - Separator between tabs (default: '')
189
+ * @returns Rendered tab bar string
190
+ */
191
+ renderTabsWithSeparator(state, styles, separator = '') {
192
+ return this.config.tabs
193
+ .map((tab, index) => {
194
+ const label = ` ${tab.label} `;
195
+ return index === state.currentTab
196
+ ? styles.selected(label)
197
+ : styles.muted(label);
198
+ })
199
+ .join(separator);
200
+ }
201
+ /**
202
+ * Render keyboard hints for tabs.
203
+ *
204
+ * @returns Hint string like "Tab Switch · 1-5 Jump"
205
+ */
206
+ renderHints() {
207
+ const tabCount = this.config.tabs.length;
208
+ if (tabCount === 0) {
209
+ return '';
210
+ }
211
+ if (this.config.useNumberKeys && tabCount <= 9) {
212
+ return `Tab Switch · 1-${String(tabCount)} Jump`;
213
+ }
214
+ return 'Tab Switch';
215
+ }
216
+ // ===========================================================================
217
+ // Utilities
218
+ // ===========================================================================
219
+ /**
220
+ * Get the current tab definition.
221
+ *
222
+ * @param state - State containing currentTab
223
+ * @returns Current tab or undefined
224
+ */
225
+ getCurrentTab(state) {
226
+ return this.config.tabs[state.currentTab];
227
+ }
228
+ /**
229
+ * Get the current tab ID.
230
+ *
231
+ * @param state - State containing currentTab
232
+ * @returns Current tab ID or empty string
233
+ */
234
+ getCurrentTabId(state) {
235
+ return this.config.tabs[state.currentTab]?.id ?? '';
236
+ }
237
+ /**
238
+ * Get the number of tabs.
239
+ */
240
+ getTabCount() {
241
+ return this.config.tabs.length;
242
+ }
243
+ /**
244
+ * Find tab index by ID.
245
+ *
246
+ * @param tabId - Tab ID to find
247
+ * @returns Index or -1 if not found
248
+ */
249
+ findTabIndex(tabId) {
250
+ return this.config.tabs.findIndex((t) => t.id === tabId);
251
+ }
252
+ /**
253
+ * Set current tab by ID.
254
+ *
255
+ * @param state - State to modify
256
+ * @param tabId - Tab ID to select
257
+ * @returns true if tab was found and selected
258
+ */
259
+ setTabById(state, tabId) {
260
+ const index = this.findTabIndex(tabId);
261
+ if (index >= 0) {
262
+ state.currentTab = index;
263
+ return true;
264
+ }
265
+ return false;
266
+ }
267
+ // ===========================================================================
268
+ // Private Methods
269
+ // ===========================================================================
270
+ notifyChange(state) {
271
+ if (this.config.onChange) {
272
+ const tab = this.config.tabs[state.currentTab];
273
+ this.config.onChange(tab.id, state.currentTab);
274
+ }
275
+ }
276
+ }
277
+ // =============================================================================
278
+ // Helper Functions
279
+ // =============================================================================
280
+ /**
281
+ * Create tab definitions from an array of strings.
282
+ * IDs are lowercase versions of labels.
283
+ *
284
+ * @param labels - Tab labels
285
+ * @returns Tab definitions
286
+ */
287
+ export function createTabsFromLabels(labels) {
288
+ return labels.map((label) => ({
289
+ id: label.toLowerCase().replace(/\s+/g, '-'),
290
+ label,
291
+ }));
292
+ }
293
+ /**
294
+ * Create filter tabs (common pattern).
295
+ *
296
+ * @param filters - Array of filter names
297
+ * @returns Tab definitions with 'all' as first tab
298
+ */
299
+ export function createFilterTabs(filters) {
300
+ return [
301
+ { id: 'all', label: 'All' },
302
+ ...filters.map((f) => ({
303
+ id: f.toLowerCase().replace(/\s+/g, '-'),
304
+ label: f,
305
+ })),
306
+ ];
307
+ }