@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
@@ -1,83 +1,141 @@
1
1
  /**
2
- * Input Prompt
2
+ * Input Prompt v2 (Event-Driven)
3
3
  *
4
- * @deprecated This module is superseded by input-prompt-v2.ts which uses
5
- * event-driven architecture for non-blocking input with queue mode support.
6
- * This file is kept for reference and may be removed in a future cleanup.
4
+ * Refactored input handling with event-driven architecture.
5
+ * - Emits events instead of blocking with getInput()
6
+ * - Supports queue mode for capturing input during agent execution
7
+ * - Always captures keystrokes (no blocking)
7
8
  *
8
- * Self-contained input handling with multiline support, autocomplete,
9
- * history navigation, and proper visual line navigation.
9
+ * Events:
10
+ * - 'submit' - User pressed Enter with input
11
+ * - 'command' - User submitted a slash command
12
+ * - 'cancel' - User pressed Ctrl+C
13
+ * - 'escape' - User pressed Esc (for aborting agent)
14
+ * - 'change' - Input text changed
10
15
  */
11
- import pc from 'picocolors';
16
+ import { EventEmitter } from 'events';
17
+ import chalk from 'chalk';
18
+ import { getStyles } from '../themes/index.js';
12
19
  import * as terminal from './terminal.js';
20
+ import { getAutocompleteCommands } from '../commands.js';
21
+ import { getFileMatches, extractAtMention, replaceAtMention, } from './file-autocomplete.js';
22
+ import { debugRender } from '../utils/debug-log.js';
23
+ import { stripAnsi as unifiedStripAnsi, getPhysicalLineCountWithOffset, } from './line-utils.js';
13
24
  // =============================================================================
14
25
  // Constants
15
26
  // =============================================================================
16
27
  const MAX_VISIBLE_COMMANDS = 10;
17
28
  // =============================================================================
18
- // Default Commands
29
+ // Default Commands (from central registry)
19
30
  // =============================================================================
20
- export const DEFAULT_COMMANDS = [
21
- { command: '/help', description: 'Show available commands' },
22
- { command: '/exit', description: 'Quit the demo' },
23
- { command: '/clear', description: 'Clear conversation history' },
24
- { command: '/compact', description: 'Summarize old messages' },
25
- { command: '/tools', description: 'List available tools' },
26
- { command: '/tokens', description: 'Show session token usage' },
27
- { command: '/context', description: 'Show context window usage' },
28
- ];
31
+ export const DEFAULT_COMMANDS = getAutocompleteCommands();
29
32
  // =============================================================================
30
33
  // Helper Functions
31
34
  // =============================================================================
32
35
  /**
33
36
  * Strip ANSI codes from string
37
+ * Uses unified line-utils for consistent behavior across components.
34
38
  */
35
39
  export function stripAnsi(str) {
36
- // eslint-disable-next-line no-control-regex
37
- return str.replace(/\x1B\[[0-9;]*[a-zA-Z]/g, '');
40
+ return unifiedStripAnsi(str);
38
41
  }
39
42
  /**
40
43
  * Calculate physical lines for wrapped text
44
+ * Uses unified line-utils for consistent behavior across components.
41
45
  */
42
46
  function calcPhysicalLines(text, startCol, termWidth) {
43
- if (text.length === 0)
44
- return 1;
45
- const totalLen = startCol + text.length;
46
- return Math.ceil(totalLen / termWidth) || 1;
47
+ return getPhysicalLineCountWithOffset(text, startCol, termWidth);
47
48
  }
48
49
  /**
49
- * Filter commands matching input
50
+ * Calculate fuzzy match score for a query against a target string.
51
+ * Higher score = better match.
52
+ * Returns -1 if no match.
53
+ */
54
+ function fuzzyMatchScore(query, target) {
55
+ const queryLower = query.toLowerCase();
56
+ const targetLower = target.toLowerCase();
57
+ // Exact prefix match - highest priority (score 1000+)
58
+ if (targetLower.startsWith(queryLower)) {
59
+ return 1000 + (100 - target.length); // Shorter commands rank higher
60
+ }
61
+ // Contiguous substring match - high priority (score 500+)
62
+ if (targetLower.includes(queryLower)) {
63
+ const index = targetLower.indexOf(queryLower);
64
+ return 500 + (100 - index); // Earlier matches rank higher
65
+ }
66
+ // Fuzzy match - characters appear in order (score 100+)
67
+ let queryIdx = 0;
68
+ let consecutiveBonus = 0;
69
+ let lastMatchIdx = -1;
70
+ for (let i = 0; i < targetLower.length && queryIdx < queryLower.length; i++) {
71
+ if (targetLower[i] === queryLower[queryIdx]) {
72
+ // Bonus for consecutive matches
73
+ if (lastMatchIdx === i - 1) {
74
+ consecutiveBonus += 10;
75
+ }
76
+ lastMatchIdx = i;
77
+ queryIdx++;
78
+ }
79
+ }
80
+ // All query characters found in order
81
+ if (queryIdx === queryLower.length) {
82
+ return 100 + consecutiveBonus + (100 - target.length);
83
+ }
84
+ // No match
85
+ return -1;
86
+ }
87
+ /**
88
+ * Filter and rank commands matching input using fuzzy matching
50
89
  */
51
90
  function filterCommands(input, commands) {
52
- const lower = input.toLowerCase();
53
- return commands.filter((cmd) => cmd.command.toLowerCase().startsWith(lower));
91
+ // Score all commands
92
+ const scored = commands
93
+ .map((cmd) => ({
94
+ cmd,
95
+ score: fuzzyMatchScore(input, cmd.command),
96
+ }))
97
+ .filter((item) => item.score >= 0);
98
+ // Sort by score (highest first)
99
+ scored.sort((a, b) => b.score - a.score);
100
+ return scored.map((item) => item.cmd);
54
101
  }
55
102
  // =============================================================================
56
103
  // Input Prompt Class
57
104
  // =============================================================================
58
- export class InputPrompt {
105
+ export class InputPrompt extends EventEmitter {
59
106
  // Configuration
60
107
  prompt;
61
108
  promptLen;
62
109
  showSeparators;
63
110
  commands;
64
- getTodos;
65
111
  // Input state
66
112
  state = {
67
113
  lines: [''],
68
114
  currentLine: 0,
69
115
  cursorPos: 0,
70
116
  };
71
- // Autocomplete
117
+ // Command autocomplete (for /commands)
72
118
  autocomplete = {
73
119
  active: false,
74
120
  matches: [],
75
121
  selectedIndex: 0,
122
+ scrollOffset: 0,
123
+ };
124
+ // File autocomplete (for @paths)
125
+ fileAutocomplete = {
126
+ active: false,
127
+ matches: [],
128
+ selectedIndex: 0,
129
+ scrollOffset: 0,
130
+ partial: '',
76
131
  };
77
132
  // History
78
133
  history = [];
79
134
  historyIndex = -1;
80
135
  savedInput = '';
136
+ // Queue mode
137
+ queueMode = false;
138
+ queuedInputs = [];
81
139
  // Rendering tracking
82
140
  renderedLines = 0;
83
141
  dropdownLines = 0;
@@ -85,29 +143,48 @@ export class InputPrompt {
85
143
  hasSeparators = false;
86
144
  // Control
87
145
  isRunning = false;
88
- resolveInput = null;
146
+ // Double Esc detection
147
+ lastEscTime = 0;
148
+ DOUBLE_ESC_THRESHOLD_MS = 500; // 500ms window for double Esc
149
+ // Suggestion (ghost text for next action)
150
+ suggestion = null;
89
151
  constructor(options = {}) {
90
- this.prompt = options.prompt ?? pc.cyan('❯ ');
152
+ super();
153
+ this.prompt = options.prompt ?? getStyles().primary('❯ ');
91
154
  this.promptLen = stripAnsi(this.prompt).length;
92
155
  this.showSeparators = options.showSeparators ?? true;
93
156
  this.commands = options.commands ?? DEFAULT_COMMANDS;
94
- this.getTodos = options.getTodos;
95
157
  }
96
158
  // ===========================================================================
97
159
  // Public API
98
160
  // ===========================================================================
99
161
  /**
100
- * Get input from user (async)
101
- * Returns when user submits or cancels
162
+ * Update the prompt string (for dynamic theme changes)
102
163
  */
103
- async getInput() {
104
- return new Promise((resolve) => {
105
- this.resolveInput = resolve;
106
- this.start();
107
- });
164
+ setPrompt(prompt) {
165
+ this.prompt = prompt;
166
+ this.promptLen = stripAnsi(prompt).length;
167
+ }
168
+ /**
169
+ * Set a suggestion for the next action (ghost text)
170
+ */
171
+ setSuggestion(action) {
172
+ this.suggestion = action;
173
+ }
174
+ /**
175
+ * Get the current suggestion
176
+ */
177
+ getSuggestion() {
178
+ return this.suggestion;
179
+ }
180
+ /**
181
+ * Clear the current suggestion
182
+ */
183
+ clearSuggestion() {
184
+ this.suggestion = null;
108
185
  }
109
186
  /**
110
- * Start input mode
187
+ * Start input capture (non-blocking, event-driven)
111
188
  */
112
189
  start() {
113
190
  if (this.isRunning)
@@ -115,17 +192,67 @@ export class InputPrompt {
115
192
  this.isRunning = true;
116
193
  terminal.enableRawMode();
117
194
  this.resetState();
118
- this.render();
119
195
  process.stdin.on('data', this.handleData);
120
196
  }
121
197
  /**
122
- * Stop input mode
198
+ * Stop input capture
123
199
  */
124
200
  stop() {
201
+ if (!this.isRunning)
202
+ return;
125
203
  this.isRunning = false;
126
204
  terminal.disableRawMode();
127
205
  process.stdin.removeListener('data', this.handleData);
128
206
  }
207
+ /**
208
+ * Check if running
209
+ */
210
+ isActive() {
211
+ return this.isRunning;
212
+ }
213
+ /**
214
+ * Enable/disable queue mode
215
+ * In queue mode, Enter adds to queue instead of emitting 'submit'
216
+ */
217
+ setQueueMode(enabled) {
218
+ this.queueMode = enabled;
219
+ }
220
+ /**
221
+ * Check if in queue mode
222
+ */
223
+ isQueueMode() {
224
+ return this.queueMode;
225
+ }
226
+ /**
227
+ * Get all queued inputs (FIFO order)
228
+ */
229
+ getQueuedInputs() {
230
+ return [...this.queuedInputs];
231
+ }
232
+ /**
233
+ * Pop the first queued input
234
+ */
235
+ popQueuedInput() {
236
+ return this.queuedInputs.shift() ?? null;
237
+ }
238
+ /**
239
+ * Check if there are queued inputs
240
+ */
241
+ hasQueuedInput() {
242
+ return this.queuedInputs.length > 0;
243
+ }
244
+ /**
245
+ * Get number of queued inputs
246
+ */
247
+ getQueueLength() {
248
+ return this.queuedInputs.length;
249
+ }
250
+ /**
251
+ * Clear the queue
252
+ */
253
+ clearQueue() {
254
+ this.queuedInputs = [];
255
+ }
129
256
  /**
130
257
  * Get current buffer value
131
258
  */
@@ -139,37 +266,196 @@ export class InputPrompt {
139
266
  this.state.lines = value.split('\n');
140
267
  this.state.currentLine = this.state.lines.length - 1;
141
268
  this.state.cursorPos = this.state.lines[this.state.currentLine].length;
142
- if (this.isRunning) {
143
- this.updateAutocomplete();
144
- this.render();
269
+ this.updateAutocomplete();
270
+ }
271
+ /**
272
+ * Clear input buffer
273
+ */
274
+ clearInput() {
275
+ this.state = {
276
+ lines: [''],
277
+ currentLine: 0,
278
+ cursorPos: 0,
279
+ };
280
+ this.autocomplete = {
281
+ active: false,
282
+ matches: [],
283
+ selectedIndex: 0,
284
+ scrollOffset: 0,
285
+ };
286
+ this.fileAutocomplete = {
287
+ active: false,
288
+ matches: [],
289
+ selectedIndex: 0,
290
+ scrollOffset: 0,
291
+ partial: '',
292
+ };
293
+ }
294
+ /**
295
+ * Add command to history
296
+ */
297
+ addToHistory(input) {
298
+ if (input.trim() && (this.history.length === 0 || this.history[this.history.length - 1] !== input)) {
299
+ this.history.push(input);
145
300
  }
146
301
  }
147
302
  /**
148
- * Clear the rendered area
303
+ * Render the input prompt - returns array of lines
304
+ * Does NOT write to terminal (Footer handles that)
149
305
  */
150
- clear() {
151
- this.clearDropdown();
152
- if (this.linesAboveCursor > 0) {
153
- terminal.moveCursorToLineStart();
154
- terminal.moveCursorUp(this.linesAboveCursor);
306
+ render() {
307
+ const lines = [];
308
+ // Top separator
309
+ if (this.showSeparators) {
310
+ lines.push(this.getSeparatorLine());
155
311
  }
156
- terminal.clearToEndOfScreen();
157
- this.renderedLines = 0;
158
- this.linesAboveCursor = 0;
312
+ // Input lines
313
+ const s = getStyles();
314
+ for (let i = 0; i < this.state.lines.length; i++) {
315
+ const linePrompt = i === 0 ? this.prompt : s.muted(' \\ ');
316
+ const lineContent = this.state.lines[i];
317
+ // Show suggestion as ghost text on first line when input is empty
318
+ if (i === 0 && lineContent === '' && this.suggestion && !this.autocomplete.active && !this.fileAutocomplete.active) {
319
+ const hint = s.muted(' (tab to accept)');
320
+ lines.push(linePrompt + s.muted(this.suggestion) + hint);
321
+ }
322
+ else {
323
+ lines.push(linePrompt + lineContent);
324
+ }
325
+ }
326
+ // Bottom separator
327
+ if (this.showSeparators) {
328
+ lines.push(this.getSeparatorLine());
329
+ }
330
+ return lines;
159
331
  }
160
332
  /**
161
- * Get number of rendered lines
333
+ * Get the cursor position info for rendering
162
334
  */
163
- getRenderedLines() {
164
- return this.renderedLines + this.dropdownLines + (this.showSeparators ? 2 : 0);
335
+ getCursorInfo() {
336
+ const termWidth = terminal.getTerminalWidth();
337
+ const currentLinePromptLen = this.state.currentLine === 0 ? this.promptLen : 5;
338
+ const cursorAbsPos = currentLinePromptLen + this.state.cursorPos;
339
+ // Calculate which row within input (accounting for separators)
340
+ let row = this.showSeparators ? 1 : 0; // Start after top separator
341
+ for (let i = 0; i < this.state.currentLine; i++) {
342
+ const lp = i === 0 ? this.promptLen : 5;
343
+ row += calcPhysicalLines(this.state.lines[i], lp, termWidth);
344
+ }
345
+ row += Math.floor(cursorAbsPos / termWidth);
346
+ const col = cursorAbsPos % termWidth;
347
+ debugRender('InputPrompt:getCursorInfo', `termWidth=${String(termWidth)} cursorPos=${String(this.state.cursorPos)} currentLine=${String(this.state.currentLine)} promptLen=${String(this.promptLen)}`);
348
+ debugRender('InputPrompt:getCursorInfo', `cursorAbsPos=${String(cursorAbsPos)} row=${String(row)} col=${String(col)}`);
349
+ return { row, col };
165
350
  }
166
351
  /**
167
- * Add command to history
352
+ * Get autocomplete dropdown lines (if active)
168
353
  */
169
- addToHistory(input) {
170
- if (input.trim() && (this.history.length === 0 || this.history[this.history.length - 1] !== input)) {
171
- this.history.push(input);
354
+ getAutocompleteLines() {
355
+ const s = getStyles();
356
+ // File autocomplete takes priority
357
+ if (this.fileAutocomplete.active && this.fileAutocomplete.matches.length > 0) {
358
+ const lines = [];
359
+ const offset = this.fileAutocomplete.scrollOffset;
360
+ const visible = this.fileAutocomplete.matches.slice(offset, offset + MAX_VISIBLE_COMMANDS);
361
+ const total = this.fileAutocomplete.matches.length;
362
+ // Show scroll indicator if there are more items above
363
+ if (offset > 0) {
364
+ lines.push(s.muted(` ↑ ${String(offset)} more above`));
365
+ }
366
+ for (let i = 0; i < visible.length; i++) {
367
+ const file = visible[i];
368
+ const actualIndex = offset + i;
369
+ const isSelected = actualIndex === this.fileAutocomplete.selectedIndex;
370
+ const prefix = isSelected ? s.primary('❯ ') : ' ';
371
+ const icon = file.isDirectory ? s.warning('📁 ') : s.info('📄 ');
372
+ const name = isSelected ? s.primary(chalk.bold(file.path)) : file.path;
373
+ lines.push(`${prefix}${icon}${name}`);
374
+ }
375
+ // Show scroll indicator if there are more items below
376
+ const remaining = total - offset - visible.length;
377
+ if (remaining > 0) {
378
+ lines.push(s.muted(` ↓ ${String(remaining)} more below`));
379
+ }
380
+ return lines;
381
+ }
382
+ // Command autocomplete
383
+ if (!this.autocomplete.active || this.autocomplete.matches.length === 0) {
384
+ return [];
172
385
  }
386
+ const lines = [];
387
+ const offset = this.autocomplete.scrollOffset;
388
+ const visible = this.autocomplete.matches.slice(offset, offset + MAX_VISIBLE_COMMANDS);
389
+ const total = this.autocomplete.matches.length;
390
+ // Show scroll indicator if there are more items above
391
+ if (offset > 0) {
392
+ lines.push(s.muted(` ↑ ${String(offset)} more above`));
393
+ }
394
+ for (let i = 0; i < visible.length; i++) {
395
+ const cmd = visible[i];
396
+ const actualIndex = offset + i;
397
+ const isSelected = actualIndex === this.autocomplete.selectedIndex;
398
+ const prefix = isSelected ? s.primary('❯ ') : ' ';
399
+ const name = isSelected ? s.primary(chalk.bold(cmd.command)) : cmd.command;
400
+ const desc = s.muted(` - ${cmd.description}`);
401
+ lines.push(`${prefix}${name}${desc}`);
402
+ }
403
+ // Show scroll indicator if there are more items below
404
+ const remaining = total - offset - visible.length;
405
+ if (remaining > 0) {
406
+ lines.push(s.muted(` ↓ ${String(remaining)} more below`));
407
+ }
408
+ return lines;
409
+ }
410
+ /**
411
+ * Check if autocomplete is showing
412
+ */
413
+ isAutocompleteActive() {
414
+ return ((this.autocomplete.active && this.autocomplete.matches.length > 0) ||
415
+ (this.fileAutocomplete.active && this.fileAutocomplete.matches.length > 0));
416
+ }
417
+ /**
418
+ * Get height of rendered content
419
+ */
420
+ getHeight() {
421
+ let height = this.state.lines.length;
422
+ if (this.showSeparators)
423
+ height += 2;
424
+ return height;
425
+ }
426
+ // ===========================================================================
427
+ // Legacy API (for backwards compatibility during migration)
428
+ // ===========================================================================
429
+ /**
430
+ * @deprecated Use event-driven start() instead
431
+ * Kept for backwards compatibility during migration
432
+ */
433
+ async getInput() {
434
+ return new Promise((resolve) => {
435
+ const onSubmit = (input) => {
436
+ cleanup();
437
+ resolve({ action: 'submit', value: input });
438
+ };
439
+ const onCommand = (command, args) => {
440
+ cleanup();
441
+ resolve({ action: 'command', command, args });
442
+ };
443
+ const onCancel = () => {
444
+ cleanup();
445
+ resolve({ action: 'cancel' });
446
+ };
447
+ const cleanup = () => {
448
+ this.removeListener('submit', onSubmit);
449
+ this.removeListener('command', onCommand);
450
+ this.removeListener('cancel', onCancel);
451
+ };
452
+ this.on('submit', onSubmit);
453
+ this.on('command', onCommand);
454
+ this.on('cancel', onCancel);
455
+ if (!this.isRunning) {
456
+ this.start();
457
+ }
458
+ });
173
459
  }
174
460
  // ===========================================================================
175
461
  // Private: State Management
@@ -184,6 +470,14 @@ export class InputPrompt {
184
470
  active: false,
185
471
  matches: [],
186
472
  selectedIndex: 0,
473
+ scrollOffset: 0,
474
+ };
475
+ this.fileAutocomplete = {
476
+ active: false,
477
+ matches: [],
478
+ selectedIndex: 0,
479
+ scrollOffset: 0,
480
+ partial: '',
187
481
  };
188
482
  this.historyIndex = -1;
189
483
  this.savedInput = '';
@@ -194,229 +488,52 @@ export class InputPrompt {
194
488
  }
195
489
  updateAutocomplete() {
196
490
  const fullInput = this.getValue();
491
+ const currentLine = this.state.lines[this.state.currentLine];
492
+ const cursorPos = this.state.cursorPos;
493
+ // Check for @ file path autocomplete first
494
+ const atMention = extractAtMention(currentLine, cursorPos);
495
+ if (atMention !== null) {
496
+ // File autocomplete mode
497
+ this.autocomplete.active = false;
498
+ this.autocomplete.matches = [];
499
+ this.autocomplete.selectedIndex = 0;
500
+ this.autocomplete.scrollOffset = 0;
501
+ this.fileAutocomplete.active = true;
502
+ this.fileAutocomplete.partial = atMention;
503
+ this.fileAutocomplete.matches = getFileMatches(atMention);
504
+ if (this.fileAutocomplete.selectedIndex >= this.fileAutocomplete.matches.length) {
505
+ this.fileAutocomplete.selectedIndex = Math.max(0, this.fileAutocomplete.matches.length - 1);
506
+ this.fileAutocomplete.scrollOffset = 0;
507
+ }
508
+ return;
509
+ }
510
+ // Reset file autocomplete
511
+ this.fileAutocomplete.active = false;
512
+ this.fileAutocomplete.matches = [];
513
+ this.fileAutocomplete.selectedIndex = 0;
514
+ this.fileAutocomplete.scrollOffset = 0;
515
+ this.fileAutocomplete.partial = '';
516
+ // Check for / command autocomplete
197
517
  if (fullInput.startsWith('/') && this.state.lines.length === 1) {
198
518
  this.autocomplete.active = true;
199
- this.autocomplete.matches = filterCommands(fullInput, this.commands);
519
+ // Refresh commands dynamically to include newly created custom commands
520
+ const freshCommands = getAutocompleteCommands();
521
+ this.autocomplete.matches = filterCommands(fullInput, freshCommands);
200
522
  if (this.autocomplete.selectedIndex >= this.autocomplete.matches.length) {
201
523
  this.autocomplete.selectedIndex = Math.max(0, this.autocomplete.matches.length - 1);
524
+ this.autocomplete.scrollOffset = 0;
202
525
  }
203
526
  }
204
527
  else {
205
528
  this.autocomplete.active = false;
206
529
  this.autocomplete.matches = [];
207
530
  this.autocomplete.selectedIndex = 0;
531
+ this.autocomplete.scrollOffset = 0;
208
532
  }
209
533
  }
210
- // ===========================================================================
211
- // Private: Layout Calculation
212
- // ===========================================================================
213
- /**
214
- * Get physical layout of current buffer
215
- */
216
- getPhysicalLayout() {
217
- const termWidth = terminal.getTerminalWidth();
218
- const lines = [];
219
- let cursorRow = 0;
220
- let cursorCol = 0;
221
- let totalRows = 0;
222
- for (let i = 0; i < this.state.lines.length; i++) {
223
- const linePromptLen = i === 0 ? this.promptLen : 5; // " \ "
224
- const lineText = this.state.lines[i];
225
- const physicalLines = calcPhysicalLines(lineText, linePromptLen, termWidth);
226
- for (let p = 0; p < physicalLines; p++) {
227
- const start = p * termWidth - (p === 0 ? 0 : linePromptLen);
228
- const end = start + termWidth;
229
- lines.push(lineText.slice(Math.max(0, start), end));
230
- }
231
- if (i === this.state.currentLine) {
232
- const cursorAbsPos = linePromptLen + this.state.cursorPos;
233
- cursorRow = totalRows + Math.floor(cursorAbsPos / termWidth);
234
- cursorCol = cursorAbsPos % termWidth;
235
- }
236
- totalRows += physicalLines;
237
- }
238
- return { lines, cursorRow, cursorCol, totalRows };
239
- }
240
- // ===========================================================================
241
- // Private: Rendering
242
- // ===========================================================================
243
534
  getSeparatorLine() {
244
- return pc.dim('─'.repeat(terminal.getTerminalWidth()));
245
- }
246
- renderTodos() {
247
- if (!this.getTodos)
248
- return 0;
249
- const todos = this.getTodos();
250
- if (todos.length === 0)
251
- return 0;
252
- let linesRendered = 0;
253
- // Group todos by status for display
254
- const inProgress = todos.filter(t => t.status === 'in_progress');
255
- const pending = todos.filter(t => t.status === 'pending');
256
- const completed = todos.filter(t => t.status === 'completed');
257
- // Show in-progress task prominently
258
- for (const todo of inProgress) {
259
- const label = todo.activeForm || todo.content;
260
- terminal.write(pc.cyan('⟳ ') + pc.bold(label) + '\n');
261
- linesRendered++;
262
- }
263
- // Show pending tasks
264
- for (const todo of pending) {
265
- terminal.write(pc.dim('○ ') + pc.dim(todo.content) + '\n');
266
- linesRendered++;
267
- }
268
- // Show completed tasks (dimmed)
269
- for (const todo of completed) {
270
- terminal.write(pc.green('✓ ') + pc.dim(pc.strikethrough(todo.content)) + '\n');
271
- linesRendered++;
272
- }
273
- if (linesRendered > 0) {
274
- terminal.write('\n');
275
- linesRendered++;
276
- }
277
- return linesRendered;
278
- }
279
- render() {
280
- const termWidth = terminal.getTerminalWidth();
281
- // Clear previous render
282
- this.clearDropdown();
283
- if (this.linesAboveCursor > 0) {
284
- terminal.moveCursorToLineStart();
285
- terminal.moveCursorUp(this.linesAboveCursor);
286
- }
287
- else {
288
- terminal.moveCursorToLineStart();
289
- }
290
- terminal.clearToEndOfScreen();
291
- // Render todos (above input area)
292
- let physicalLinesRendered = 0;
293
- physicalLinesRendered += this.renderTodos();
294
- // Render top separator
295
- if (this.showSeparators) {
296
- terminal.write(this.getSeparatorLine() + '\n');
297
- physicalLinesRendered += 1;
298
- }
299
- // Render input lines
300
- let physicalLinesBeforeCursor = 0;
301
- for (let i = 0; i < this.state.lines.length; i++) {
302
- const linePrompt = i === 0 ? this.prompt : pc.dim(' \\ ');
303
- const linePromptLen = i === 0 ? this.promptLen : 5;
304
- if (i < this.state.currentLine) {
305
- physicalLinesBeforeCursor += calcPhysicalLines(this.state.lines[i], linePromptLen, termWidth);
306
- }
307
- terminal.write(linePrompt + this.state.lines[i]);
308
- if (i < this.state.lines.length - 1) {
309
- terminal.write('\n');
310
- }
311
- }
312
- // Render bottom separator
313
- if (this.showSeparators) {
314
- terminal.write('\n' + this.getSeparatorLine());
315
- }
316
- // Track state
317
- this.renderedLines = this.state.lines.length;
318
- this.hasSeparators = this.showSeparators;
319
- // Position cursor
320
- const currentLinePromptLen = this.state.currentLine === 0 ? this.promptLen : 5;
321
- const cursorAbsPos = currentLinePromptLen + this.state.cursorPos;
322
- const currentLinePhysical = calcPhysicalLines(this.state.lines[this.state.currentLine], currentLinePromptLen, termWidth);
323
- const cursorPhysicalRow = Math.floor(cursorAbsPos / termWidth);
324
- // Move up from bottom to cursor position
325
- let linesToMoveUp = this.showSeparators ? 1 : 0;
326
- for (let i = this.state.currentLine + 1; i < this.state.lines.length; i++) {
327
- const lp = i === 0 ? this.promptLen : 5;
328
- linesToMoveUp += calcPhysicalLines(this.state.lines[i], lp, termWidth);
329
- }
330
- linesToMoveUp += currentLinePhysical - 1 - cursorPhysicalRow;
331
- if (linesToMoveUp > 0) {
332
- terminal.moveCursorUp(linesToMoveUp);
333
- }
334
- const cursorCol = (cursorAbsPos % termWidth) + 1;
335
- terminal.moveCursorToColumn(cursorCol);
336
- // Track cursor position for next render
337
- this.linesAboveCursor = physicalLinesRendered + physicalLinesBeforeCursor + cursorPhysicalRow;
338
- // Render dropdown
339
- this.renderDropdown();
340
- }
341
- renderDropdown() {
342
- if (!this.autocomplete.active || this.autocomplete.matches.length === 0) {
343
- this.dropdownLines = 0;
344
- return;
345
- }
346
- const termWidth = terminal.getTerminalWidth();
347
- const currentLinePromptLen = this.state.currentLine === 0 ? this.promptLen : 5;
348
- // Move down past remaining input lines and bottom separator
349
- let linesToMoveDown = this.state.lines.length - 1 - this.state.currentLine;
350
- if (this.hasSeparators)
351
- linesToMoveDown += 1;
352
- // Account for wrapped lines
353
- for (let i = this.state.currentLine; i < this.state.lines.length; i++) {
354
- const lp = i === 0 ? this.promptLen : 5;
355
- const physical = calcPhysicalLines(this.state.lines[i], lp, termWidth);
356
- if (i === this.state.currentLine) {
357
- const cursorAbsPos = currentLinePromptLen + this.state.cursorPos;
358
- const cursorRow = Math.floor(cursorAbsPos / termWidth);
359
- linesToMoveDown += physical - 1 - cursorRow;
360
- }
361
- else {
362
- linesToMoveDown += physical - 1;
363
- }
364
- }
365
- if (linesToMoveDown > 0) {
366
- terminal.moveCursorDown(linesToMoveDown);
367
- }
368
- terminal.write('\n');
369
- const visible = this.autocomplete.matches.slice(0, MAX_VISIBLE_COMMANDS);
370
- for (let i = 0; i < visible.length; i++) {
371
- const cmd = visible[i];
372
- const isSelected = i === this.autocomplete.selectedIndex;
373
- const prefix = isSelected ? pc.cyan('❯ ') : ' ';
374
- const name = isSelected ? pc.cyan(pc.bold(cmd.command)) : cmd.command;
375
- const desc = pc.dim(` - ${cmd.description}`);
376
- terminal.write(`${prefix}${name}${desc}\n`);
377
- }
378
- // Move back up to cursor position
379
- this.dropdownLines = visible.length;
380
- const linesToMoveUp = this.dropdownLines + linesToMoveDown + 1;
381
- terminal.moveCursorUp(linesToMoveUp);
382
- const cursorAbsPos = currentLinePromptLen + this.state.cursorPos;
383
- const cursorCol = (cursorAbsPos % termWidth) + 1;
384
- terminal.moveCursorToColumn(cursorCol);
385
- }
386
- clearDropdown() {
387
- if (this.dropdownLines === 0)
388
- return;
389
- const termWidth = terminal.getTerminalWidth();
390
- const currentLinePromptLen = this.state.currentLine === 0 ? this.promptLen : 5;
391
- // Move down past remaining lines and separator
392
- let linesToMoveDown = this.state.lines.length - 1 - this.state.currentLine;
393
- if (this.hasSeparators)
394
- linesToMoveDown += 1;
395
- for (let i = this.state.currentLine; i < this.state.lines.length; i++) {
396
- const lp = i === 0 ? this.promptLen : 5;
397
- const physical = calcPhysicalLines(this.state.lines[i], lp, termWidth);
398
- if (i === this.state.currentLine) {
399
- const cursorAbsPos = currentLinePromptLen + this.state.cursorPos;
400
- const cursorRow = Math.floor(cursorAbsPos / termWidth);
401
- linesToMoveDown += physical - 1 - cursorRow;
402
- }
403
- else {
404
- linesToMoveDown += physical - 1;
405
- }
406
- }
407
- if (linesToMoveDown > 0) {
408
- terminal.moveCursorDown(linesToMoveDown);
409
- }
410
- terminal.write('\n');
411
- // Clear dropdown lines
412
- for (let i = 0; i < this.dropdownLines; i++) {
413
- terminal.clearLine();
414
- terminal.write('\n');
415
- }
416
- // Move back up
417
- const linesToMoveUp = this.dropdownLines + linesToMoveDown + 1;
418
- terminal.moveCursorUp(linesToMoveUp);
419
- this.dropdownLines = 0;
535
+ const s = getStyles();
536
+ return s.muted('─'.repeat(terminal.getTerminalWidth()));
420
537
  }
421
538
  // ===========================================================================
422
539
  // Private: Input Handling
@@ -433,7 +550,9 @@ export class InputPrompt {
433
550
  const isEnter = key === '\r' || key === '\n';
434
551
  const isBackspace = key === '\x7f' || key === '\b';
435
552
  const isTab = key === '\t';
553
+ const isShiftTab = data.length === 3 && data[0] === 0x1b && data[1] === 0x5b && data[2] === 0x5a;
436
554
  const isCtrlC = key === '\x03';
555
+ const isCtrlO = key === '\x0f'; // Ctrl+O for toggle subagent expand
437
556
  const isUpArrow = data.length === 3 && data[0] === 0x1b && data[1] === 0x5b && data[2] === 0x41;
438
557
  const isDownArrow = data.length === 3 && data[0] === 0x1b && data[1] === 0x5b && data[2] === 0x42;
439
558
  const isLeftArrow = data.length === 3 && data[0] === 0x1b && data[1] === 0x5b && data[2] === 0x44;
@@ -460,16 +579,39 @@ export class InputPrompt {
460
579
  const isEnd = key === '\x05' || (data.length === 3 && data[0] === 0x1b && data[1] === 0x5b && data[2] === 0x46);
461
580
  // Ctrl+C - cancel
462
581
  if (isCtrlC) {
463
- this.finishInput({ action: 'cancel' });
582
+ this.emit('cancel');
464
583
  return;
465
584
  }
466
- // Escape - close autocomplete
585
+ // Ctrl+O - toggle subagent expand (only when agent is running)
586
+ if (isCtrlO) {
587
+ this.emit('toggleSubagentExpand');
588
+ return;
589
+ }
590
+ // Escape - close autocomplete, double Esc clears input, or emit escape for agent abort
467
591
  if (isEscape) {
468
- if (this.autocomplete.active) {
469
- this.clearDropdown();
592
+ const now = Date.now();
593
+ const isDoubleEsc = now - this.lastEscTime < this.DOUBLE_ESC_THRESHOLD_MS;
594
+ this.lastEscTime = now;
595
+ if (this.fileAutocomplete.active) {
596
+ // First priority: close file autocomplete
597
+ this.fileAutocomplete.active = false;
598
+ this.fileAutocomplete.matches = [];
599
+ this.emit('change', this.getValue());
600
+ }
601
+ else if (this.autocomplete.active) {
602
+ // Second priority: close command autocomplete
470
603
  this.autocomplete.active = false;
471
604
  this.autocomplete.matches = [];
472
- this.render();
605
+ this.emit('change', this.getValue());
606
+ }
607
+ else if (isDoubleEsc && this.getValue().length > 0) {
608
+ // Third priority: double Esc clears input (if there's content)
609
+ this.clearInput();
610
+ this.emit('change', this.getValue());
611
+ }
612
+ else {
613
+ // Fourth priority: emit escape for agent abort
614
+ this.emit('escape');
473
615
  }
474
616
  return;
475
617
  }
@@ -478,6 +620,11 @@ export class InputPrompt {
478
620
  this.handleEnter();
479
621
  return;
480
622
  }
623
+ // Shift+Tab - cycle modes
624
+ if (isShiftTab) {
625
+ this.emit('modeChange');
626
+ return;
627
+ }
481
628
  // Tab - accept autocomplete or insert spaces
482
629
  if (isTab) {
483
630
  this.handleTab();
@@ -512,12 +659,12 @@ export class InputPrompt {
512
659
  // Home/End
513
660
  if (isHome) {
514
661
  this.state.cursorPos = 0;
515
- this.render();
662
+ this.emit('change', this.getValue());
516
663
  return;
517
664
  }
518
665
  if (isEnd) {
519
666
  this.state.cursorPos = this.state.lines[this.state.currentLine].length;
520
- this.render();
667
+ this.emit('change', this.getValue());
521
668
  return;
522
669
  }
523
670
  // Backspace
@@ -532,13 +679,17 @@ export class InputPrompt {
532
679
  .filter((c) => c.charCodeAt(0) >= 32)
533
680
  .join('');
534
681
  if (printable.length > 0) {
682
+ // Clear suggestion when user starts typing
683
+ if (this.suggestion) {
684
+ this.suggestion = null;
685
+ }
535
686
  const line = this.state.lines[this.state.currentLine];
536
687
  this.state.lines[this.state.currentLine] =
537
688
  line.slice(0, this.state.cursorPos) + printable + line.slice(this.state.cursorPos);
538
689
  this.state.cursorPos += printable.length;
539
690
  this.historyIndex = -1;
540
691
  this.updateAutocomplete();
541
- this.render();
692
+ this.emit('change', this.getValue());
542
693
  }
543
694
  }
544
695
  }
@@ -551,35 +702,82 @@ export class InputPrompt {
551
702
  this.state.currentLine++;
552
703
  this.state.cursorPos = 0;
553
704
  this.autocomplete.active = false;
554
- this.clearDropdown();
555
- this.render();
705
+ this.emit('change', this.getValue());
556
706
  return;
557
707
  }
558
- // Autocomplete selection
708
+ // File autocomplete selection - accept the path and continue
709
+ if (this.fileAutocomplete.active && this.fileAutocomplete.matches.length > 0) {
710
+ const selectedFile = this.fileAutocomplete.matches[this.fileAutocomplete.selectedIndex];
711
+ const result = replaceAtMention(currentLine, this.state.cursorPos, selectedFile.path);
712
+ this.state.lines[this.state.currentLine] = result.input;
713
+ this.state.cursorPos = result.cursorPos;
714
+ this.fileAutocomplete.active = false;
715
+ this.fileAutocomplete.matches = [];
716
+ // Don't return - fall through to submit
717
+ }
718
+ // Command autocomplete selection - accept the command and continue to execute it
559
719
  if (this.autocomplete.active && this.autocomplete.matches.length > 0) {
560
720
  this.state.lines[0] = this.autocomplete.matches[this.autocomplete.selectedIndex].command;
561
721
  this.state.cursorPos = this.state.lines[0].length;
722
+ this.autocomplete.active = false;
723
+ // Don't return - fall through to execute the command
562
724
  }
563
725
  const input = this.getValue();
726
+ // Empty input - ignore
727
+ if (!input.trim()) {
728
+ return;
729
+ }
730
+ // Add to history
731
+ this.addToHistory(input);
732
+ // Queue mode - add to queue instead of emitting
733
+ if (this.queueMode) {
734
+ this.queuedInputs.push(input);
735
+ this.clearInput();
736
+ this.emit('change', this.getValue());
737
+ return;
738
+ }
564
739
  // Check if it's a command
565
740
  if (input.startsWith('/')) {
566
741
  const parts = input.slice(1).split(/\s+/);
567
742
  const command = parts[0];
568
743
  const args = parts.slice(1).join(' ');
569
- this.finishInput({ action: 'command', command, args });
744
+ this.clearInput();
745
+ this.emit('command', command, args);
570
746
  }
571
747
  else {
572
- this.finishInput({ action: 'submit', value: input });
748
+ this.clearInput();
749
+ this.emit('submit', input);
573
750
  }
574
751
  }
575
752
  handleTab() {
753
+ // Accept suggestion if present and input is empty
754
+ if (this.suggestion && this.getValue() === '') {
755
+ this.state.lines[0] = this.suggestion;
756
+ this.state.cursorPos = this.suggestion.length;
757
+ this.suggestion = null;
758
+ this.emit('change', this.getValue());
759
+ return;
760
+ }
761
+ // File autocomplete completion
762
+ if (this.fileAutocomplete.active && this.fileAutocomplete.matches.length > 0) {
763
+ const selectedFile = this.fileAutocomplete.matches[this.fileAutocomplete.selectedIndex];
764
+ const currentLine = this.state.lines[this.state.currentLine];
765
+ const result = replaceAtMention(currentLine, this.state.cursorPos, selectedFile.path);
766
+ this.state.lines[this.state.currentLine] = result.input;
767
+ this.state.cursorPos = result.cursorPos;
768
+ this.fileAutocomplete.active = false;
769
+ this.fileAutocomplete.matches = [];
770
+ this.updateAutocomplete(); // Check if still in @ context (e.g., directory selected)
771
+ this.emit('change', this.getValue());
772
+ return;
773
+ }
774
+ // Command autocomplete completion
576
775
  if (this.autocomplete.active && this.autocomplete.matches.length > 0) {
577
776
  this.state.lines[this.state.currentLine] =
578
777
  this.autocomplete.matches[this.autocomplete.selectedIndex].command;
579
778
  this.state.cursorPos = this.state.lines[this.state.currentLine].length;
580
779
  this.autocomplete.active = false;
581
780
  this.autocomplete.matches = [];
582
- this.clearDropdown();
583
781
  }
584
782
  else {
585
783
  // Insert 2 spaces
@@ -590,13 +788,27 @@ export class InputPrompt {
590
788
  this.historyIndex = -1;
591
789
  this.updateAutocomplete();
592
790
  }
593
- this.render();
791
+ this.emit('change', this.getValue());
594
792
  }
595
793
  handleArrowUp() {
596
- // Autocomplete navigation
794
+ // File autocomplete navigation
795
+ if (this.fileAutocomplete.active && this.fileAutocomplete.selectedIndex > 0) {
796
+ this.fileAutocomplete.selectedIndex--;
797
+ // Adjust scroll offset if selection goes above visible area
798
+ if (this.fileAutocomplete.selectedIndex < this.fileAutocomplete.scrollOffset) {
799
+ this.fileAutocomplete.scrollOffset = this.fileAutocomplete.selectedIndex;
800
+ }
801
+ this.emit('change', this.getValue());
802
+ return;
803
+ }
804
+ // Command autocomplete navigation
597
805
  if (this.autocomplete.active && this.autocomplete.selectedIndex > 0) {
598
806
  this.autocomplete.selectedIndex--;
599
- this.render();
807
+ // Adjust scroll offset if selection goes above visible area
808
+ if (this.autocomplete.selectedIndex < this.autocomplete.scrollOffset) {
809
+ this.autocomplete.scrollOffset = this.autocomplete.selectedIndex;
810
+ }
811
+ this.emit('change', this.getValue());
600
812
  return;
601
813
  }
602
814
  const termWidth = terminal.getTerminalWidth();
@@ -609,7 +821,7 @@ export class InputPrompt {
609
821
  const newAbsPos = (currentPhysicalRow - 1) * termWidth + cursorColInRow;
610
822
  this.state.cursorPos = Math.max(0, newAbsPos - currentLinePromptLen);
611
823
  this.state.cursorPos = Math.min(this.state.cursorPos, this.state.lines[this.state.currentLine].length);
612
- this.render();
824
+ this.emit('change', this.getValue());
613
825
  return;
614
826
  }
615
827
  // Navigate to previous logical line
@@ -622,7 +834,7 @@ export class InputPrompt {
622
834
  const targetAbsPos = lastRowStart + cursorColInRow;
623
835
  this.state.cursorPos = Math.max(0, targetAbsPos - prevLinePromptLen);
624
836
  this.state.cursorPos = Math.min(this.state.cursorPos, prevLineLen);
625
- this.render();
837
+ this.emit('change', this.getValue());
626
838
  return;
627
839
  }
628
840
  // History navigation (at top of input)
@@ -636,17 +848,33 @@ export class InputPrompt {
636
848
  this.state.lines = [historyEntry];
637
849
  this.state.currentLine = 0;
638
850
  this.state.cursorPos = historyEntry.length;
639
- this.render();
851
+ this.emit('change', this.getValue());
640
852
  }
641
853
  }
642
854
  }
643
855
  handleArrowDown() {
644
- // Autocomplete navigation
856
+ // File autocomplete navigation
857
+ if (this.fileAutocomplete.active &&
858
+ this.fileAutocomplete.selectedIndex < this.fileAutocomplete.matches.length - 1) {
859
+ this.fileAutocomplete.selectedIndex++;
860
+ // Adjust scroll offset if selection goes below visible area
861
+ const maxVisibleIndex = this.fileAutocomplete.scrollOffset + MAX_VISIBLE_COMMANDS - 1;
862
+ if (this.fileAutocomplete.selectedIndex > maxVisibleIndex) {
863
+ this.fileAutocomplete.scrollOffset = this.fileAutocomplete.selectedIndex - MAX_VISIBLE_COMMANDS + 1;
864
+ }
865
+ this.emit('change', this.getValue());
866
+ return;
867
+ }
868
+ // Command autocomplete navigation
645
869
  if (this.autocomplete.active &&
646
- this.autocomplete.selectedIndex < this.autocomplete.matches.length - 1 &&
647
- this.autocomplete.selectedIndex < MAX_VISIBLE_COMMANDS - 1) {
870
+ this.autocomplete.selectedIndex < this.autocomplete.matches.length - 1) {
648
871
  this.autocomplete.selectedIndex++;
649
- this.render();
872
+ // Adjust scroll offset if selection goes below visible area
873
+ const maxVisibleIndex = this.autocomplete.scrollOffset + MAX_VISIBLE_COMMANDS - 1;
874
+ if (this.autocomplete.selectedIndex > maxVisibleIndex) {
875
+ this.autocomplete.scrollOffset = this.autocomplete.selectedIndex - MAX_VISIBLE_COMMANDS + 1;
876
+ }
877
+ this.emit('change', this.getValue());
650
878
  return;
651
879
  }
652
880
  const termWidth = terminal.getTerminalWidth();
@@ -660,7 +888,7 @@ export class InputPrompt {
660
888
  const newAbsPos = (currentPhysicalRow + 1) * termWidth + cursorColInRow;
661
889
  this.state.cursorPos = Math.max(0, newAbsPos - currentLinePromptLen);
662
890
  this.state.cursorPos = Math.min(this.state.cursorPos, this.state.lines[this.state.currentLine].length);
663
- this.render();
891
+ this.emit('change', this.getValue());
664
892
  return;
665
893
  }
666
894
  // Navigate to next logical line
@@ -670,7 +898,7 @@ export class InputPrompt {
670
898
  const nextLineLen = this.state.lines[this.state.currentLine].length;
671
899
  this.state.cursorPos = Math.max(0, cursorColInRow - nextLinePromptLen);
672
900
  this.state.cursorPos = Math.min(this.state.cursorPos, nextLineLen);
673
- this.render();
901
+ this.emit('change', this.getValue());
674
902
  return;
675
903
  }
676
904
  // History forward
@@ -688,29 +916,29 @@ export class InputPrompt {
688
916
  this.state.cursorPos = historyEntry.length;
689
917
  }
690
918
  this.updateAutocomplete();
691
- this.render();
919
+ this.emit('change', this.getValue());
692
920
  }
693
921
  }
694
922
  handleArrowLeft() {
695
923
  if (this.state.cursorPos > 0) {
696
924
  this.state.cursorPos--;
697
- terminal.write('\x1B[D');
925
+ this.emit('change', this.getValue());
698
926
  }
699
927
  else if (this.state.currentLine > 0) {
700
928
  this.state.currentLine--;
701
929
  this.state.cursorPos = this.state.lines[this.state.currentLine].length;
702
- this.render();
930
+ this.emit('change', this.getValue());
703
931
  }
704
932
  }
705
933
  handleArrowRight() {
706
934
  if (this.state.cursorPos < this.state.lines[this.state.currentLine].length) {
707
935
  this.state.cursorPos++;
708
- terminal.write('\x1B[C');
936
+ this.emit('change', this.getValue());
709
937
  }
710
938
  else if (this.state.currentLine < this.state.lines.length - 1) {
711
939
  this.state.currentLine++;
712
940
  this.state.cursorPos = 0;
713
- this.render();
941
+ this.emit('change', this.getValue());
714
942
  }
715
943
  }
716
944
  handleWordLeft() {
@@ -722,12 +950,12 @@ export class InputPrompt {
722
950
  while (pos > 0 && line[pos - 1] !== ' ')
723
951
  pos--;
724
952
  this.state.cursorPos = pos;
725
- this.render();
953
+ this.emit('change', this.getValue());
726
954
  }
727
955
  else if (this.state.currentLine > 0) {
728
956
  this.state.currentLine--;
729
957
  this.state.cursorPos = this.state.lines[this.state.currentLine].length;
730
- this.render();
958
+ this.emit('change', this.getValue());
731
959
  }
732
960
  }
733
961
  handleWordRight() {
@@ -739,12 +967,12 @@ export class InputPrompt {
739
967
  while (pos < line.length && line[pos] === ' ')
740
968
  pos++;
741
969
  this.state.cursorPos = pos;
742
- this.render();
970
+ this.emit('change', this.getValue());
743
971
  }
744
972
  else if (this.state.currentLine < this.state.lines.length - 1) {
745
973
  this.state.currentLine++;
746
974
  this.state.cursorPos = 0;
747
- this.render();
975
+ this.emit('change', this.getValue());
748
976
  }
749
977
  }
750
978
  handleBackspace() {
@@ -755,7 +983,7 @@ export class InputPrompt {
755
983
  line.slice(0, this.state.cursorPos - 1) + line.slice(this.state.cursorPos);
756
984
  this.state.cursorPos--;
757
985
  this.updateAutocomplete();
758
- this.render();
986
+ this.emit('change', this.getValue());
759
987
  }
760
988
  else if (this.state.currentLine > 0) {
761
989
  const currentLine = this.state.lines[this.state.currentLine];
@@ -765,36 +993,7 @@ export class InputPrompt {
765
993
  this.state.currentLine--;
766
994
  this.state.cursorPos = prevLine.length;
767
995
  this.updateAutocomplete();
768
- this.render();
769
- }
770
- }
771
- finishInput(result) {
772
- // Clear display
773
- this.clearDropdown();
774
- if (this.linesAboveCursor > 0) {
775
- terminal.moveCursorToLineStart();
776
- terminal.moveCursorUp(this.linesAboveCursor);
777
- }
778
- terminal.clearToEndOfScreen();
779
- // Print clean input (without separators)
780
- for (let i = 0; i < this.state.lines.length; i++) {
781
- const linePrompt = i === 0 ? this.prompt : pc.dim(' \\ ');
782
- terminal.write(linePrompt + this.state.lines[i]);
783
- if (i < this.state.lines.length - 1) {
784
- terminal.write('\n');
785
- }
786
- }
787
- terminal.writeLine('');
788
- // Save to history if submitting
789
- if (result.action === 'submit' || result.action === 'command') {
790
- const input = this.getValue();
791
- this.addToHistory(input);
792
- }
793
- // Stop and resolve
794
- this.stop();
795
- if (this.resolveInput) {
796
- this.resolveInput(result);
797
- this.resolveInput = null;
996
+ this.emit('change', this.getValue());
798
997
  }
799
998
  }
800
999
  }