@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,1349 @@
1
+ /**
2
+ * Footer V2 - UI Orchestrator
3
+ *
4
+ * Single point of control for all terminal UI:
5
+ * - Owns all rendering decisions (when to clear, when to render)
6
+ * - Owns all UI state (agentRunning, todos, currentTool, etc.)
7
+ * - Provides output methods that handle clear/render automatically
8
+ * - REPL just emits events, doesn't manage rendering timing
9
+ *
10
+ * Key principles:
11
+ * - NO scroll regions (preserves terminal scrollback)
12
+ * - Deterministic render cycle
13
+ * - All output goes through footer (no external console.log)
14
+ */
15
+ import { EventEmitter } from 'events';
16
+ import * as readline from 'readline';
17
+ import * as terminal from './terminal.js';
18
+ import { getPhysicalLineCount, getVisibleLength } from './line-utils.js';
19
+ import { getStyles } from '../themes/index.js';
20
+ import { MODE_INFO } from './types.js';
21
+ import { getAutocompleteCommands } from '../commands.js';
22
+ import { getFileMatches, extractAtMention, replaceAtMention, } from './file-autocomplete.js';
23
+ // =============================================================================
24
+ // Constants
25
+ // =============================================================================
26
+ const MAX_VISIBLE_COMMANDS = 10;
27
+ // =============================================================================
28
+ // Mascot Expressions
29
+ // =============================================================================
30
+ /**
31
+ * Inline mascot expressions for agent identity and state feedback.
32
+ * Used to prefix agent messages and in spinner animations.
33
+ */
34
+ export const MASCOT = {
35
+ // Core expressions
36
+ neutral: '[•_•]', // Default state - regular messages
37
+ thinking: '[°_°]', // Processing/thinking
38
+ searching: '[◐_◐]', // Searching/scanning files
39
+ success: '[^_^]', // Task completed successfully
40
+ error: '[×_×]', // Error occurred
41
+ confused: '[?_?]', // Needs clarification
42
+ working: '[•̀_•́]', // Actively working on task
43
+ // CRT monitor animation frames (subtle scanner effect)
44
+ crt: ['[░░░]', '[▒░░]', '[░▒░]', '[░░▒]', '[░▒░]', '[▒░░]'],
45
+ };
46
+ // =============================================================================
47
+ // Fuzzy Matching
48
+ // =============================================================================
49
+ /**
50
+ * Calculate fuzzy match score for a query against a target string.
51
+ * Higher score = better match. Returns -1 if no match.
52
+ */
53
+ function fuzzyMatchScore(query, target) {
54
+ const queryLower = query.toLowerCase();
55
+ const targetLower = target.toLowerCase();
56
+ // Exact prefix match - highest priority (score 1000+)
57
+ if (targetLower.startsWith(queryLower)) {
58
+ return 1000 + (100 - target.length); // Shorter commands rank higher
59
+ }
60
+ // Contiguous substring match - high priority (score 500+)
61
+ if (targetLower.includes(queryLower)) {
62
+ const index = targetLower.indexOf(queryLower);
63
+ return 500 + (100 - index); // Earlier matches rank higher
64
+ }
65
+ // Fuzzy match - characters appear in order (score 100+)
66
+ let queryIdx = 0;
67
+ let consecutiveBonus = 0;
68
+ let lastMatchIdx = -1;
69
+ for (let i = 0; i < targetLower.length && queryIdx < queryLower.length; i++) {
70
+ if (targetLower[i] === queryLower[queryIdx]) {
71
+ if (lastMatchIdx === i - 1) {
72
+ consecutiveBonus += 10;
73
+ }
74
+ lastMatchIdx = i;
75
+ queryIdx++;
76
+ }
77
+ }
78
+ if (queryIdx === queryLower.length) {
79
+ return 100 + consecutiveBonus + (100 - target.length);
80
+ }
81
+ return -1;
82
+ }
83
+ /**
84
+ * Filter and rank commands matching input using fuzzy matching
85
+ */
86
+ function filterCommands(input, commands) {
87
+ const scored = commands
88
+ .map((cmd) => ({
89
+ cmd,
90
+ score: fuzzyMatchScore(input, cmd.command),
91
+ }))
92
+ .filter((item) => item.score >= 0);
93
+ scored.sort((a, b) => b.score - a.score);
94
+ return scored.map((item) => item.cmd);
95
+ }
96
+ // =============================================================================
97
+ // Footer V2 Class
98
+ // =============================================================================
99
+ export class FooterV2 extends EventEmitter {
100
+ // Configuration
101
+ promptPrefix;
102
+ promptPrefixLen;
103
+ // Input state (multiline)
104
+ lines = [''];
105
+ currentLine = 0;
106
+ cursorPos = 0;
107
+ mode;
108
+ projectName = null;
109
+ todos = [];
110
+ spinnerText = null;
111
+ spinnerFrame = 0;
112
+ agentRunning = false;
113
+ queuedInputs = [];
114
+ currentTool = null;
115
+ // Command autocomplete state (for /commands)
116
+ autocomplete = {
117
+ active: false,
118
+ matches: [],
119
+ selectedIndex: 0,
120
+ scrollOffset: 0,
121
+ };
122
+ // File autocomplete state (for @paths)
123
+ fileAutocomplete = {
124
+ active: false,
125
+ matches: [],
126
+ selectedIndex: 0,
127
+ scrollOffset: 0,
128
+ partial: '',
129
+ };
130
+ // History state
131
+ history = [];
132
+ historyIndex = -1;
133
+ savedInput = '';
134
+ // Render tracking
135
+ lastRenderHeight = 0;
136
+ cursorLineFromBottom = 0;
137
+ isRunning = false;
138
+ isPaused = false;
139
+ renderTimer = null;
140
+ needsRender = false;
141
+ // Double Esc detection
142
+ lastEscapeTime = 0;
143
+ // Ghost text suggestion
144
+ suggestion = null;
145
+ // Spinner animation - CRT monitor fill effect
146
+ spinnerFrames = MASCOT.crt;
147
+ spinnerTimer = null;
148
+ constructor(options = {}) {
149
+ super();
150
+ const s = getStyles();
151
+ this.promptPrefix = options.prompt ?? s.primaryBold('compilr>') + ' ';
152
+ this.promptPrefixLen = getVisibleLength(this.promptPrefix);
153
+ this.mode = options.initialMode ?? 'normal';
154
+ }
155
+ // ===========================================================================
156
+ // Input value helpers
157
+ // ===========================================================================
158
+ /** Get the full input value (all lines joined with newlines) */
159
+ getInputValue() {
160
+ return this.lines.join('\n');
161
+ }
162
+ /** Get the current line content */
163
+ getCurrentLineContent() {
164
+ return this.lines[this.currentLine];
165
+ }
166
+ /** Clear input and reset to single empty line */
167
+ clearInput() {
168
+ this.lines = [''];
169
+ this.currentLine = 0;
170
+ this.cursorPos = 0;
171
+ }
172
+ // ===========================================================================
173
+ // Lifecycle
174
+ // ===========================================================================
175
+ start() {
176
+ if (this.isRunning)
177
+ return;
178
+ this.isRunning = true;
179
+ this.isPaused = false;
180
+ // Start keyboard input handling
181
+ this.startKeyboardCapture();
182
+ // Initial render
183
+ this.render();
184
+ // Start render loop (60ms = ~16fps)
185
+ this.renderTimer = setInterval(() => {
186
+ if (this.needsRender && !this.isPaused) {
187
+ this.render();
188
+ this.needsRender = false;
189
+ }
190
+ }, 60);
191
+ }
192
+ stop() {
193
+ if (!this.isRunning)
194
+ return;
195
+ this.isRunning = false;
196
+ // Stop render loop
197
+ if (this.renderTimer) {
198
+ clearInterval(this.renderTimer);
199
+ this.renderTimer = null;
200
+ }
201
+ // Stop spinner
202
+ this.stopSpinnerAnimation();
203
+ // Stop keyboard capture
204
+ this.stopKeyboardCapture();
205
+ // Clear footer
206
+ this.clear();
207
+ }
208
+ // ===========================================================================
209
+ // Public API - Output (THE KEY METHODS)
210
+ // ===========================================================================
211
+ /**
212
+ * Print a semantic item to the scrolling zone.
213
+ * FooterV2 handles all formatting based on the item type.
214
+ */
215
+ print(item) {
216
+ const s = getStyles();
217
+ this.clear();
218
+ switch (item.type) {
219
+ case 'user-message':
220
+ console.log('');
221
+ console.log(s.primaryBold('> ') + item.text);
222
+ console.log(''); // Trailing blank for separation
223
+ break;
224
+ case 'agent-text': {
225
+ // Prefix agent messages with mascot expression (accent colored) and separator
226
+ const expr = item.expression ? MASCOT[item.expression] : MASCOT.neutral;
227
+ console.log(s.primary(expr) + s.muted(' > ') + item.text);
228
+ console.log(''); // Trailing blank for separation
229
+ break;
230
+ }
231
+ case 'tool-start':
232
+ console.log(s.info(`● ${item.name}`) + s.muted(`(${item.params})`));
233
+ break;
234
+ case 'tool-result':
235
+ console.log(s.info(`● ${item.name}`) + s.muted(`(${item.params})`));
236
+ if (item.success === false) {
237
+ console.log(s.error(` ⎿ ${item.result}`));
238
+ }
239
+ else {
240
+ console.log(s.muted(` ⎿ ${item.result}`));
241
+ }
242
+ console.log('');
243
+ break;
244
+ case 'tool-error':
245
+ console.log(s.info(`● ${item.name}`) + s.muted(`(${item.params})`));
246
+ console.log(s.error(` ⎿ Error: ${item.error}`));
247
+ console.log('');
248
+ break;
249
+ case 'interrupted': {
250
+ // Show what was ongoing (if provided)
251
+ if (item.action) {
252
+ console.log(s.info(`● ${item.action}`));
253
+ }
254
+ else {
255
+ // No action means interrupted right after user message
256
+ // Move cursor up to consume the trailing blank from user-message
257
+ process.stdout.write('\x1b[1A'); // Move up one line
258
+ }
259
+ // Show interrupted line with mascot
260
+ const suggestion = item.suggestion ?? 'What should I do instead?';
261
+ console.log(s.warning(` ⎿ ${MASCOT.confused} Interrupted - ${suggestion}`));
262
+ console.log('');
263
+ // Also set as ghost text suggestion
264
+ this.setSuggestion(suggestion);
265
+ break;
266
+ }
267
+ case 'error':
268
+ console.log(s.error(`${MASCOT.error} ${item.message}`));
269
+ console.log('');
270
+ break;
271
+ case 'success':
272
+ console.log(s.success(`${MASCOT.success} ${item.message}`));
273
+ console.log('');
274
+ break;
275
+ case 'info':
276
+ console.log(s.info(item.message));
277
+ console.log('');
278
+ break;
279
+ case 'warning':
280
+ console.log(s.warning(item.message));
281
+ console.log('');
282
+ break;
283
+ case 'raw':
284
+ console.log(item.text);
285
+ break;
286
+ case 'raw-lines':
287
+ for (const line of item.lines) {
288
+ console.log(line);
289
+ }
290
+ break;
291
+ }
292
+ this.render();
293
+ }
294
+ /**
295
+ * Execute a callback that outputs to terminal.
296
+ * Handles clear → callback → render automatically.
297
+ * Use this for complex output that needs multiple console.log calls.
298
+ * @deprecated Prefer using print() with PrintableItem
299
+ */
300
+ output(callback) {
301
+ this.clear();
302
+ callback();
303
+ this.render();
304
+ }
305
+ // ===========================================================================
306
+ // Public API - State setters
307
+ // ===========================================================================
308
+ setAgentRunning(running) {
309
+ const wasRunning = this.agentRunning;
310
+ this.agentRunning = running;
311
+ if (running && !wasRunning) {
312
+ this.startSpinnerAnimation();
313
+ }
314
+ else if (!running && wasRunning) {
315
+ this.stopSpinnerAnimation();
316
+ this.currentTool = null;
317
+ }
318
+ this.needsRender = true;
319
+ }
320
+ isAgentRunning() {
321
+ return this.agentRunning;
322
+ }
323
+ setTodos(todos) {
324
+ this.todos = todos;
325
+ this.needsRender = true;
326
+ }
327
+ getTodos() {
328
+ return [...this.todos];
329
+ }
330
+ setSpinnerText(text) {
331
+ this.spinnerText = text;
332
+ this.needsRender = true;
333
+ }
334
+ setCurrentTool(tool) {
335
+ this.currentTool = tool;
336
+ if (tool) {
337
+ this.spinnerText = tool;
338
+ }
339
+ this.needsRender = true;
340
+ }
341
+ setMode(mode) {
342
+ this.mode = mode;
343
+ this.needsRender = true;
344
+ }
345
+ getMode() {
346
+ return this.mode;
347
+ }
348
+ setProjectName(name) {
349
+ this.projectName = name;
350
+ this.needsRender = true;
351
+ }
352
+ getProjectName() {
353
+ return this.projectName;
354
+ }
355
+ /**
356
+ * Set ghost text suggestion (shown when input is empty)
357
+ */
358
+ setSuggestion(text) {
359
+ this.suggestion = text;
360
+ this.needsRender = true;
361
+ }
362
+ getSuggestion() {
363
+ return this.suggestion;
364
+ }
365
+ clearSuggestion() {
366
+ this.suggestion = null;
367
+ this.needsRender = true;
368
+ }
369
+ // ===========================================================================
370
+ // Public API - Input queue
371
+ // ===========================================================================
372
+ getQueuedInputs() {
373
+ return [...this.queuedInputs];
374
+ }
375
+ popQueuedInput() {
376
+ if (this.queuedInputs.length === 0)
377
+ return null;
378
+ const input = this.queuedInputs.shift();
379
+ this.needsRender = true;
380
+ return input ?? null;
381
+ }
382
+ hasQueuedInput() {
383
+ return this.queuedInputs.length > 0;
384
+ }
385
+ clearQueue() {
386
+ this.queuedInputs = [];
387
+ this.needsRender = true;
388
+ }
389
+ // ===========================================================================
390
+ // Public API - Animation control (for overlays)
391
+ // ===========================================================================
392
+ /**
393
+ * Pause footer completely (for overlays)
394
+ */
395
+ pauseAnimation() {
396
+ this.isPaused = true;
397
+ // Stop render loop
398
+ if (this.renderTimer) {
399
+ clearInterval(this.renderTimer);
400
+ this.renderTimer = null;
401
+ }
402
+ // Stop spinner
403
+ this.stopSpinnerAnimation();
404
+ // Stop keyboard capture
405
+ this.stopKeyboardCapture();
406
+ // Clear footer from screen
407
+ this.clear();
408
+ }
409
+ /**
410
+ * Resume footer after pause
411
+ */
412
+ resumeAnimation() {
413
+ this.isPaused = false;
414
+ // Resume spinner if agent running
415
+ if (this.agentRunning) {
416
+ this.startSpinnerAnimation();
417
+ }
418
+ // Restart keyboard capture
419
+ this.startKeyboardCapture();
420
+ // Restart render loop
421
+ this.render();
422
+ this.renderTimer = setInterval(() => {
423
+ if (this.needsRender && !this.isPaused) {
424
+ this.render();
425
+ this.needsRender = false;
426
+ }
427
+ }, 60);
428
+ }
429
+ /**
430
+ * Restart input after command
431
+ */
432
+ restartInput() {
433
+ this.render();
434
+ }
435
+ /**
436
+ * Refresh prompt with theme colors
437
+ */
438
+ refreshPrompt() {
439
+ const s = getStyles();
440
+ this.promptPrefix = s.primaryBold('compilr>') + ' ';
441
+ this.promptPrefixLen = getVisibleLength(this.promptPrefix);
442
+ this.needsRender = true;
443
+ }
444
+ // ===========================================================================
445
+ // Public API - Legacy compatibility (for gradual migration)
446
+ // ===========================================================================
447
+ /**
448
+ * @deprecated Use print() or output() instead
449
+ */
450
+ clearForOutput() {
451
+ this.clear();
452
+ }
453
+ /**
454
+ * @deprecated Use print() or output() instead
455
+ */
456
+ forceRender() {
457
+ this.render();
458
+ this.needsRender = false;
459
+ }
460
+ // ===========================================================================
461
+ // Private - Rendering
462
+ // ===========================================================================
463
+ clear() {
464
+ if (this.lastRenderHeight === 0)
465
+ return;
466
+ terminal.moveCursorToLineStart();
467
+ const cursorRowFromTop = this.lastRenderHeight - this.cursorLineFromBottom;
468
+ const rowsToMoveUp = cursorRowFromTop - 1;
469
+ if (rowsToMoveUp > 0) {
470
+ terminal.moveCursorUp(rowsToMoveUp);
471
+ }
472
+ terminal.clearToEndOfScreen();
473
+ this.lastRenderHeight = 0;
474
+ this.cursorLineFromBottom = 0;
475
+ }
476
+ render() {
477
+ if (!this.isRunning || this.isPaused)
478
+ return;
479
+ const termWidth = terminal.getTerminalWidth();
480
+ const s = getStyles();
481
+ // Build all lines in order
482
+ const lines = [];
483
+ // 1. Header: Spinner (when running) or "Todos" (when idle with todos)
484
+ if (this.agentRunning) {
485
+ const spinnerLine = this.buildSpinnerLine();
486
+ lines.push({
487
+ content: spinnerLine,
488
+ physicalLines: getPhysicalLineCount(spinnerLine, termWidth),
489
+ });
490
+ }
491
+ else if (this.todos.length > 0) {
492
+ const headerLine = s.muted('Todos');
493
+ lines.push({
494
+ content: headerLine,
495
+ physicalLines: 1,
496
+ });
497
+ }
498
+ // 2. Todo list (always show if there are todos)
499
+ if (this.todos.length > 0) {
500
+ for (const todo of this.todos) {
501
+ const todoLine = this.buildTodoLine(todo);
502
+ lines.push({
503
+ content: todoLine,
504
+ physicalLines: getPhysicalLineCount(todoLine, termWidth),
505
+ });
506
+ }
507
+ // Blank line after todos for spacing
508
+ lines.push({ content: '', physicalLines: 1 });
509
+ }
510
+ // 3. Queued inputs (show multiline as single line with ↵ indicator)
511
+ for (const queued of this.queuedInputs) {
512
+ const displayText = queued.replace(/\n/g, ' ↵ ');
513
+ const queuedLine = s.muted(`queued: "${displayText}"`);
514
+ lines.push({
515
+ content: queuedLine,
516
+ physicalLines: getPhysicalLineCount(queuedLine, termWidth),
517
+ });
518
+ }
519
+ // 4. Top separator
520
+ const separator = '─'.repeat(termWidth);
521
+ lines.push({ content: separator, physicalLines: 1 });
522
+ // 5. Input prompt (multiline support)
523
+ const continuationPrompt = s.muted(' \\ ');
524
+ const continuationPromptLen = getVisibleLength(continuationPrompt);
525
+ const cursorLineIndex = lines.length + this.currentLine; // Index where cursor is
526
+ for (let i = 0; i < this.lines.length; i++) {
527
+ const linePrompt = i === 0 ? this.promptPrefix : continuationPrompt;
528
+ const linePromptLen = i === 0 ? this.promptPrefixLen : continuationPromptLen;
529
+ const lineContent = this.lines[i];
530
+ // Show ghost text on first line when input is empty and no autocomplete
531
+ let fullLine;
532
+ let totalLen;
533
+ if (i === 0 &&
534
+ lineContent === '' &&
535
+ this.lines.length === 1 &&
536
+ this.suggestion &&
537
+ !this.autocomplete.active &&
538
+ !this.fileAutocomplete.active) {
539
+ // Build: [prompt][suggestion]...[↵ send]
540
+ const rightHint = '↵ send';
541
+ const leftContent = linePrompt + s.muted(this.suggestion);
542
+ const leftLen = linePromptLen + this.suggestion.length;
543
+ const rightLen = rightHint.length;
544
+ const padding = Math.max(1, termWidth - leftLen - rightLen);
545
+ fullLine = leftContent + ' '.repeat(padding) + s.muted(rightHint);
546
+ totalLen = termWidth; // Full width
547
+ }
548
+ else {
549
+ fullLine = linePrompt + lineContent;
550
+ totalLen = linePromptLen + getVisibleLength(lineContent);
551
+ }
552
+ const physicalLines = totalLen === 0 ? 1 : Math.ceil(totalLen / termWidth) || 1;
553
+ lines.push({
554
+ content: fullLine,
555
+ physicalLines,
556
+ });
557
+ }
558
+ // 6. Bottom separator
559
+ lines.push({ content: separator, physicalLines: 1 });
560
+ // 7. Mode indicator
561
+ const modeLine = this.buildModeLine(termWidth);
562
+ lines.push({
563
+ content: modeLine,
564
+ physicalLines: getPhysicalLineCount(modeLine, termWidth),
565
+ });
566
+ // 8. Autocomplete dropdown (if active) - file autocomplete takes priority
567
+ const fileAutocompleteLines = this.buildFileAutocompleteLines(termWidth);
568
+ const autocompleteLines = fileAutocompleteLines.length > 0
569
+ ? fileAutocompleteLines
570
+ : this.buildAutocompleteLines(termWidth);
571
+ for (const line of autocompleteLines) {
572
+ lines.push({
573
+ content: line,
574
+ physicalLines: getPhysicalLineCount(line, termWidth),
575
+ });
576
+ }
577
+ // Calculate total physical height
578
+ let totalPhysicalLines = 0;
579
+ for (const line of lines) {
580
+ totalPhysicalLines += line.physicalLines;
581
+ }
582
+ // Clear existing footer
583
+ this.clear();
584
+ // Write all lines
585
+ for (let i = 0; i < lines.length; i++) {
586
+ terminal.write(lines[i].content);
587
+ if (i < lines.length - 1) {
588
+ terminal.write('\n');
589
+ }
590
+ }
591
+ // Calculate cursor position (for multiline, position on the line where cursor is)
592
+ let rowsAfterCursor = 0;
593
+ for (let i = cursorLineIndex + 1; i < lines.length; i++) {
594
+ rowsAfterCursor += lines[i].physicalLines;
595
+ }
596
+ if (rowsAfterCursor > 0) {
597
+ terminal.moveCursorUp(rowsAfterCursor);
598
+ }
599
+ // Calculate column within current line
600
+ const currentLinePromptLen = this.currentLine === 0 ? this.promptPrefixLen : continuationPromptLen;
601
+ const totalPos = currentLinePromptLen + this.cursorPos;
602
+ const cursorCol = (totalPos % termWidth) + 1;
603
+ terminal.moveCursorToColumn(cursorCol);
604
+ terminal.showCursor();
605
+ this.lastRenderHeight = totalPhysicalLines;
606
+ this.cursorLineFromBottom = rowsAfterCursor;
607
+ }
608
+ // ===========================================================================
609
+ // Private - Line builders
610
+ // ===========================================================================
611
+ buildSpinnerLine() {
612
+ const s = getStyles();
613
+ const frame = this.spinnerFrames[this.spinnerFrame % this.spinnerFrames.length];
614
+ const text = this.currentTool ?? this.spinnerText ?? 'Thinking...';
615
+ // Format: [CRT scanner] text (single monitor animation, no double mascot)
616
+ return s.muted(`${frame} ${text}`);
617
+ }
618
+ buildTodoLine(todo) {
619
+ const s = getStyles();
620
+ const icon = todo.status === 'completed' ? '✓' :
621
+ todo.status === 'in_progress' ? '→' : '☐';
622
+ const style = todo.status === 'completed' ? s.muted :
623
+ todo.status === 'in_progress' ? s.info : s.muted;
624
+ return style(`${icon} ${todo.content}`);
625
+ }
626
+ buildModeLine(termWidth) {
627
+ const s = getStyles();
628
+ const modeInfo = MODE_INFO[this.mode];
629
+ let leftPart;
630
+ switch (this.mode) {
631
+ case 'normal':
632
+ leftPart = s.muted(`mode: ${modeInfo.label} (Shift+Tab to change)`);
633
+ break;
634
+ case 'auto-accept':
635
+ leftPart = s.warning(`mode: ${modeInfo.label}`) + s.muted(' (Shift+Tab to change)');
636
+ break;
637
+ case 'plan':
638
+ leftPart = s.info(`mode: ${modeInfo.label}`) + s.muted(' (Shift+Tab to change)');
639
+ break;
640
+ default:
641
+ leftPart = s.muted(`mode: ${modeInfo.label} (Shift+Tab to change)`);
642
+ }
643
+ if (!this.projectName) {
644
+ return leftPart;
645
+ }
646
+ const projectText = `Project: ${this.projectName}`;
647
+ const rightPart = s.muted(projectText);
648
+ const leftVisible = getVisibleLength(leftPart);
649
+ const padding = Math.max(2, termWidth - leftVisible - projectText.length);
650
+ return leftPart + ' '.repeat(padding) + rightPart;
651
+ }
652
+ buildAutocompleteLines(termWidth) {
653
+ if (!this.autocomplete.active || this.autocomplete.matches.length === 0) {
654
+ return [];
655
+ }
656
+ const s = getStyles();
657
+ const lines = [];
658
+ const { matches, selectedIndex, scrollOffset } = this.autocomplete;
659
+ const visible = matches.slice(scrollOffset, scrollOffset + MAX_VISIBLE_COMMANDS);
660
+ const total = matches.length;
661
+ // Show scroll indicator if there are more items above
662
+ if (scrollOffset > 0) {
663
+ lines.push(s.muted(` ↑ ${String(scrollOffset)} more above`));
664
+ }
665
+ for (let i = 0; i < visible.length; i++) {
666
+ const cmd = visible[i];
667
+ const actualIndex = scrollOffset + i;
668
+ const isSelected = actualIndex === selectedIndex;
669
+ const prefix = isSelected ? s.primary('❯ ') : ' ';
670
+ const name = isSelected ? s.primaryBold(cmd.command) : cmd.command;
671
+ // Truncate if too long
672
+ const baseLen = getVisibleLength(prefix) + getVisibleLength(name) + 3; // 3 for ' - '
673
+ const maxDescLen = termWidth - baseLen - 2;
674
+ const truncatedDesc = cmd.description.length > maxDescLen
675
+ ? cmd.description.slice(0, maxDescLen - 1) + '…'
676
+ : cmd.description;
677
+ lines.push(`${prefix}${name} ${s.muted('- ' + truncatedDesc)}`);
678
+ }
679
+ // Show scroll indicator if there are more items below
680
+ const belowCount = total - scrollOffset - visible.length;
681
+ if (belowCount > 0) {
682
+ lines.push(s.muted(` ↓ ${String(belowCount)} more below`));
683
+ }
684
+ return lines;
685
+ }
686
+ buildFileAutocompleteLines(termWidth) {
687
+ if (!this.fileAutocomplete.active || this.fileAutocomplete.matches.length === 0) {
688
+ return [];
689
+ }
690
+ const s = getStyles();
691
+ const lines = [];
692
+ const { matches, selectedIndex, scrollOffset } = this.fileAutocomplete;
693
+ const visible = matches.slice(scrollOffset, scrollOffset + MAX_VISIBLE_COMMANDS);
694
+ const total = matches.length;
695
+ // Show scroll indicator if there are more items above
696
+ if (scrollOffset > 0) {
697
+ lines.push(s.muted(` ↑ ${String(scrollOffset)} more above`));
698
+ }
699
+ for (let i = 0; i < visible.length; i++) {
700
+ const file = visible[i];
701
+ const actualIndex = scrollOffset + i;
702
+ const isSelected = actualIndex === selectedIndex;
703
+ const prefix = isSelected ? s.primary('❯ ') : ' ';
704
+ const icon = file.isDirectory ? '📁 ' : '📄 ';
705
+ const name = isSelected ? s.primaryBold(file.path) : file.path;
706
+ // Truncate if too long
707
+ const baseLen = getVisibleLength(prefix) + 3 + getVisibleLength(file.path); // 3 for icon
708
+ if (baseLen > termWidth - 2) {
709
+ const maxLen = termWidth - getVisibleLength(prefix) - 5; // 3 for icon, 2 for padding
710
+ const truncatedPath = file.path.length > maxLen
711
+ ? '…' + file.path.slice(-(maxLen - 1))
712
+ : file.path;
713
+ const truncatedName = isSelected ? s.primaryBold(truncatedPath) : truncatedPath;
714
+ lines.push(`${prefix}${icon}${truncatedName}`);
715
+ }
716
+ else {
717
+ lines.push(`${prefix}${icon}${name}`);
718
+ }
719
+ }
720
+ // Show scroll indicator if there are more items below
721
+ const belowCount = total - scrollOffset - visible.length;
722
+ if (belowCount > 0) {
723
+ lines.push(s.muted(` ↓ ${String(belowCount)} more below`));
724
+ }
725
+ return lines;
726
+ }
727
+ // ===========================================================================
728
+ // Private - Cursor calculations
729
+ // ===========================================================================
730
+ // ===========================================================================
731
+ // Private - Autocomplete
732
+ // ===========================================================================
733
+ updateAutocomplete() {
734
+ const currentLine = this.getCurrentLineContent();
735
+ // Check for @ file path autocomplete first (works on any line)
736
+ const atMention = extractAtMention(currentLine, this.cursorPos);
737
+ if (atMention !== null) {
738
+ // File autocomplete mode - disable command autocomplete
739
+ this.autocomplete.active = false;
740
+ this.autocomplete.matches = [];
741
+ this.autocomplete.selectedIndex = 0;
742
+ this.autocomplete.scrollOffset = 0;
743
+ // Enable file autocomplete
744
+ this.fileAutocomplete.active = true;
745
+ this.fileAutocomplete.partial = atMention;
746
+ this.fileAutocomplete.matches = getFileMatches(atMention);
747
+ if (this.fileAutocomplete.selectedIndex >= this.fileAutocomplete.matches.length) {
748
+ this.fileAutocomplete.selectedIndex = Math.max(0, this.fileAutocomplete.matches.length - 1);
749
+ this.fileAutocomplete.scrollOffset = 0;
750
+ }
751
+ return;
752
+ }
753
+ // Reset file autocomplete
754
+ this.fileAutocomplete.active = false;
755
+ this.fileAutocomplete.matches = [];
756
+ this.fileAutocomplete.selectedIndex = 0;
757
+ this.fileAutocomplete.scrollOffset = 0;
758
+ this.fileAutocomplete.partial = '';
759
+ // Activate command autocomplete when first line starts with /
760
+ const firstLine = this.lines[0];
761
+ if (this.currentLine === 0 && firstLine.startsWith('/')) {
762
+ this.autocomplete.active = true;
763
+ const freshCommands = getAutocompleteCommands();
764
+ this.autocomplete.matches = filterCommands(firstLine, freshCommands);
765
+ // Reset selection if it's out of bounds
766
+ if (this.autocomplete.selectedIndex >= this.autocomplete.matches.length) {
767
+ this.autocomplete.selectedIndex = Math.max(0, this.autocomplete.matches.length - 1);
768
+ this.autocomplete.scrollOffset = 0;
769
+ }
770
+ }
771
+ else {
772
+ this.autocomplete.active = false;
773
+ this.autocomplete.matches = [];
774
+ this.autocomplete.selectedIndex = 0;
775
+ this.autocomplete.scrollOffset = 0;
776
+ }
777
+ }
778
+ acceptAutocomplete() {
779
+ // File autocomplete takes priority (works on current line)
780
+ if (this.fileAutocomplete.active && this.fileAutocomplete.matches.length > 0) {
781
+ const selectedFile = this.fileAutocomplete.matches[this.fileAutocomplete.selectedIndex];
782
+ const currentLine = this.getCurrentLineContent();
783
+ const result = replaceAtMention(currentLine, this.cursorPos, selectedFile.path);
784
+ this.lines[this.currentLine] = result.input;
785
+ this.cursorPos = result.cursorPos;
786
+ this.fileAutocomplete.active = false;
787
+ this.fileAutocomplete.matches = [];
788
+ this.fileAutocomplete.selectedIndex = 0;
789
+ this.fileAutocomplete.scrollOffset = 0;
790
+ this.fileAutocomplete.partial = '';
791
+ // Check if still in @ context (e.g., directory selected, might want to continue)
792
+ this.updateAutocomplete();
793
+ return;
794
+ }
795
+ // Command autocomplete (works on first line only)
796
+ if (this.autocomplete.active && this.autocomplete.matches.length > 0) {
797
+ const selected = this.autocomplete.matches[this.autocomplete.selectedIndex];
798
+ this.lines[0] = selected.command;
799
+ this.currentLine = 0;
800
+ this.cursorPos = this.lines[0].length;
801
+ this.autocomplete.active = false;
802
+ this.autocomplete.matches = [];
803
+ this.autocomplete.selectedIndex = 0;
804
+ this.autocomplete.scrollOffset = 0;
805
+ }
806
+ }
807
+ closeAutocomplete() {
808
+ // Close file autocomplete
809
+ this.fileAutocomplete.active = false;
810
+ this.fileAutocomplete.matches = [];
811
+ this.fileAutocomplete.selectedIndex = 0;
812
+ this.fileAutocomplete.scrollOffset = 0;
813
+ this.fileAutocomplete.partial = '';
814
+ // Close command autocomplete
815
+ this.autocomplete.active = false;
816
+ this.autocomplete.matches = [];
817
+ this.autocomplete.selectedIndex = 0;
818
+ this.autocomplete.scrollOffset = 0;
819
+ }
820
+ navigateAutocompleteUp() {
821
+ // File autocomplete navigation takes priority
822
+ if (this.fileAutocomplete.active && this.fileAutocomplete.matches.length > 0) {
823
+ if (this.fileAutocomplete.selectedIndex > 0) {
824
+ this.fileAutocomplete.selectedIndex--;
825
+ if (this.fileAutocomplete.selectedIndex < this.fileAutocomplete.scrollOffset) {
826
+ this.fileAutocomplete.scrollOffset = this.fileAutocomplete.selectedIndex;
827
+ }
828
+ return true;
829
+ }
830
+ return false;
831
+ }
832
+ // Command autocomplete navigation
833
+ if (!this.autocomplete.active || this.autocomplete.matches.length === 0) {
834
+ return false;
835
+ }
836
+ if (this.autocomplete.selectedIndex > 0) {
837
+ this.autocomplete.selectedIndex--;
838
+ // Adjust scroll if selection goes above visible area
839
+ if (this.autocomplete.selectedIndex < this.autocomplete.scrollOffset) {
840
+ this.autocomplete.scrollOffset = this.autocomplete.selectedIndex;
841
+ }
842
+ return true;
843
+ }
844
+ return false;
845
+ }
846
+ navigateAutocompleteDown() {
847
+ // File autocomplete navigation takes priority
848
+ if (this.fileAutocomplete.active && this.fileAutocomplete.matches.length > 0) {
849
+ if (this.fileAutocomplete.selectedIndex < this.fileAutocomplete.matches.length - 1) {
850
+ this.fileAutocomplete.selectedIndex++;
851
+ const maxVisibleIndex = this.fileAutocomplete.scrollOffset + MAX_VISIBLE_COMMANDS - 1;
852
+ if (this.fileAutocomplete.selectedIndex > maxVisibleIndex) {
853
+ this.fileAutocomplete.scrollOffset = this.fileAutocomplete.selectedIndex - MAX_VISIBLE_COMMANDS + 1;
854
+ }
855
+ return true;
856
+ }
857
+ return false;
858
+ }
859
+ // Command autocomplete navigation
860
+ if (!this.autocomplete.active || this.autocomplete.matches.length === 0) {
861
+ return false;
862
+ }
863
+ if (this.autocomplete.selectedIndex < this.autocomplete.matches.length - 1) {
864
+ this.autocomplete.selectedIndex++;
865
+ // Adjust scroll if selection goes below visible area
866
+ const maxVisibleIndex = this.autocomplete.scrollOffset + MAX_VISIBLE_COMMANDS - 1;
867
+ if (this.autocomplete.selectedIndex > maxVisibleIndex) {
868
+ this.autocomplete.scrollOffset = this.autocomplete.selectedIndex - MAX_VISIBLE_COMMANDS + 1;
869
+ }
870
+ return true;
871
+ }
872
+ return false;
873
+ }
874
+ // ===========================================================================
875
+ // Private - History
876
+ // ===========================================================================
877
+ addToHistory(input) {
878
+ const trimmed = input.trim();
879
+ // Don't add empty or duplicate consecutive entries
880
+ if (trimmed && (this.history.length === 0 || this.history[this.history.length - 1] !== trimmed)) {
881
+ this.history.push(trimmed);
882
+ }
883
+ }
884
+ navigateHistoryUp() {
885
+ if (this.autocomplete.active)
886
+ return false;
887
+ if (this.history.length === 0)
888
+ return false;
889
+ // Save current input when starting to navigate
890
+ if (this.historyIndex === -1) {
891
+ this.savedInput = this.getInputValue();
892
+ }
893
+ // Navigate to older entry
894
+ if (this.historyIndex < this.history.length - 1) {
895
+ this.historyIndex++;
896
+ const historyEntry = this.history[this.history.length - 1 - this.historyIndex];
897
+ // History entries are single-line (newlines stripped on save)
898
+ this.lines = [historyEntry];
899
+ this.currentLine = 0;
900
+ this.cursorPos = historyEntry.length;
901
+ return true;
902
+ }
903
+ return false;
904
+ }
905
+ navigateHistoryDown() {
906
+ if (this.autocomplete.active)
907
+ return false;
908
+ if (this.historyIndex < 0)
909
+ return false;
910
+ this.historyIndex--;
911
+ if (this.historyIndex === -1) {
912
+ // Restore saved input (may be multiline)
913
+ this.lines = this.savedInput.split('\n');
914
+ if (this.lines.length === 0)
915
+ this.lines = [''];
916
+ this.currentLine = this.lines.length - 1;
917
+ this.cursorPos = this.lines[this.currentLine].length;
918
+ }
919
+ else {
920
+ // Navigate to newer entry
921
+ const historyEntry = this.history[this.history.length - 1 - this.historyIndex];
922
+ this.lines = [historyEntry];
923
+ this.currentLine = 0;
924
+ this.cursorPos = historyEntry.length;
925
+ }
926
+ return true;
927
+ }
928
+ resetHistoryNavigation() {
929
+ this.historyIndex = -1;
930
+ this.savedInput = '';
931
+ }
932
+ // ===========================================================================
933
+ // Private - Spinner animation
934
+ // ===========================================================================
935
+ startSpinnerAnimation() {
936
+ if (this.spinnerTimer)
937
+ return;
938
+ this.spinnerTimer = setInterval(() => {
939
+ this.spinnerFrame++;
940
+ this.needsRender = true;
941
+ }, 200); // Slower, more relaxed animation
942
+ }
943
+ stopSpinnerAnimation() {
944
+ if (this.spinnerTimer) {
945
+ clearInterval(this.spinnerTimer);
946
+ this.spinnerTimer = null;
947
+ }
948
+ this.spinnerFrame = 0;
949
+ }
950
+ // ===========================================================================
951
+ // Private - Keyboard handling
952
+ // ===========================================================================
953
+ keyHandler = null;
954
+ dataHandler = null;
955
+ startKeyboardCapture() {
956
+ if (this.keyHandler)
957
+ return; // Already capturing
958
+ readline.emitKeypressEvents(process.stdin);
959
+ if (process.stdin.isTTY) {
960
+ process.stdin.setRawMode(true);
961
+ }
962
+ // Handle raw data for reliable Escape and Option+Arrow detection
963
+ this.dataHandler = (data) => {
964
+ if (!this.isRunning || this.isPaused)
965
+ return;
966
+ // Pure Escape key is a single byte 0x1B
967
+ if (data.length === 1 && data[0] === 0x1b) {
968
+ this.handleEscape();
969
+ return;
970
+ }
971
+ // Mac Option+Left (word left): ESC b or ESC [ 1 ; 9 D
972
+ const isOptionLeft = (data.length === 2 && data[0] === 0x1b && data[1] === 0x62) ||
973
+ (data.length === 6 &&
974
+ data[0] === 0x1b &&
975
+ data[1] === 0x5b &&
976
+ data[2] === 0x31 &&
977
+ data[3] === 0x3b &&
978
+ data[4] === 0x39 &&
979
+ data[5] === 0x44);
980
+ // Mac Option+Right (word right): ESC f or ESC [ 1 ; 9 C
981
+ const isOptionRight = (data.length === 2 && data[0] === 0x1b && data[1] === 0x66) ||
982
+ (data.length === 6 &&
983
+ data[0] === 0x1b &&
984
+ data[1] === 0x5b &&
985
+ data[2] === 0x31 &&
986
+ data[3] === 0x3b &&
987
+ data[4] === 0x39 &&
988
+ data[5] === 0x43);
989
+ if (isOptionLeft) {
990
+ this.handleWordLeft();
991
+ return;
992
+ }
993
+ if (isOptionRight) {
994
+ this.handleWordRight();
995
+ return;
996
+ }
997
+ // Home/Cmd+Left: Ctrl+A (\x01) or ESC [ H
998
+ const isHome = (data.length === 1 && data[0] === 0x01) ||
999
+ (data.length === 3 && data[0] === 0x1b && data[1] === 0x5b && data[2] === 0x48);
1000
+ // End/Cmd+Right: Ctrl+E (\x05) or ESC [ F
1001
+ const isEnd = (data.length === 1 && data[0] === 0x05) ||
1002
+ (data.length === 3 && data[0] === 0x1b && data[1] === 0x5b && data[2] === 0x46);
1003
+ if (isHome) {
1004
+ this.cursorPos = 0;
1005
+ this.needsRender = true;
1006
+ return;
1007
+ }
1008
+ if (isEnd) {
1009
+ this.cursorPos = this.lines[this.currentLine].length;
1010
+ this.needsRender = true;
1011
+ return;
1012
+ }
1013
+ };
1014
+ this.keyHandler = (str, key) => {
1015
+ if (!this.isRunning || this.isPaused)
1016
+ return;
1017
+ // Skip escape here - handled by dataHandler
1018
+ if (key.name === 'escape')
1019
+ return;
1020
+ this.handleKeypress(str, key);
1021
+ };
1022
+ process.stdin.on('data', this.dataHandler);
1023
+ process.stdin.on('keypress', this.keyHandler);
1024
+ process.stdin.resume();
1025
+ }
1026
+ stopKeyboardCapture() {
1027
+ if (this.dataHandler) {
1028
+ process.stdin.removeListener('data', this.dataHandler);
1029
+ this.dataHandler = null;
1030
+ }
1031
+ if (this.keyHandler) {
1032
+ process.stdin.removeListener('keypress', this.keyHandler);
1033
+ this.keyHandler = null;
1034
+ }
1035
+ if (process.stdin.isTTY) {
1036
+ process.stdin.setRawMode(false);
1037
+ }
1038
+ }
1039
+ /**
1040
+ * Handle word left (Option+Left) - move cursor to previous word boundary
1041
+ */
1042
+ handleWordLeft() {
1043
+ const line = this.lines[this.currentLine];
1044
+ if (this.cursorPos > 0) {
1045
+ let pos = this.cursorPos;
1046
+ // Skip spaces
1047
+ while (pos > 0 && line[pos - 1] === ' ')
1048
+ pos--;
1049
+ // Skip word
1050
+ while (pos > 0 && line[pos - 1] !== ' ')
1051
+ pos--;
1052
+ this.cursorPos = pos;
1053
+ this.needsRender = true;
1054
+ }
1055
+ else if (this.currentLine > 0) {
1056
+ // Move to end of previous line
1057
+ this.currentLine--;
1058
+ this.cursorPos = this.lines[this.currentLine].length;
1059
+ this.needsRender = true;
1060
+ }
1061
+ }
1062
+ /**
1063
+ * Handle word right (Option+Right) - move cursor to next word boundary
1064
+ */
1065
+ handleWordRight() {
1066
+ const line = this.lines[this.currentLine];
1067
+ if (this.cursorPos < line.length) {
1068
+ let pos = this.cursorPos;
1069
+ // Skip word
1070
+ while (pos < line.length && line[pos] !== ' ')
1071
+ pos++;
1072
+ // Skip spaces
1073
+ while (pos < line.length && line[pos] === ' ')
1074
+ pos++;
1075
+ this.cursorPos = pos;
1076
+ this.needsRender = true;
1077
+ }
1078
+ else if (this.currentLine < this.lines.length - 1) {
1079
+ // Move to start of next line
1080
+ this.currentLine++;
1081
+ this.cursorPos = 0;
1082
+ this.needsRender = true;
1083
+ }
1084
+ }
1085
+ /**
1086
+ * Handle Escape key - called from raw data handler for reliable detection
1087
+ */
1088
+ handleEscape() {
1089
+ const now = Date.now();
1090
+ const timeSinceLastEsc = now - this.lastEscapeTime;
1091
+ const isDoubleEsc = timeSinceLastEsc < 500;
1092
+ this.lastEscapeTime = now;
1093
+ if (this.fileAutocomplete.active) {
1094
+ // 1. Close file autocomplete first
1095
+ this.closeAutocomplete();
1096
+ this.needsRender = true;
1097
+ }
1098
+ else if (this.autocomplete.active) {
1099
+ // 2. Close command autocomplete
1100
+ this.closeAutocomplete();
1101
+ this.needsRender = true;
1102
+ }
1103
+ else if (isDoubleEsc && this.getInputValue().length > 0) {
1104
+ // 3. Double Esc clears input (if there's content)
1105
+ this.clearInput();
1106
+ this.resetHistoryNavigation();
1107
+ this.needsRender = true;
1108
+ }
1109
+ else if (this.agentRunning) {
1110
+ // 4. Single Esc cancels agent
1111
+ this.emit('cancel');
1112
+ }
1113
+ else {
1114
+ // 5. Single Esc emits escape
1115
+ this.emit('escape');
1116
+ }
1117
+ }
1118
+ handleKeypress(str, key) {
1119
+ // Ctrl+C - interrupt/exit
1120
+ if (key.ctrl && key.name === 'c') {
1121
+ this.emit('interrupt');
1122
+ return;
1123
+ }
1124
+ // Tab - accept suggestion, or autocomplete
1125
+ if (key.name === 'tab' && !key.shift) {
1126
+ // Accept ghost text suggestion if input is empty
1127
+ if (this.suggestion && this.getInputValue() === '') {
1128
+ this.lines[0] = this.suggestion;
1129
+ this.cursorPos = this.suggestion.length;
1130
+ this.suggestion = null;
1131
+ this.needsRender = true;
1132
+ return;
1133
+ }
1134
+ if (this.fileAutocomplete.active && this.fileAutocomplete.matches.length > 0) {
1135
+ this.acceptAutocomplete();
1136
+ this.needsRender = true;
1137
+ return;
1138
+ }
1139
+ if (this.autocomplete.active && this.autocomplete.matches.length > 0) {
1140
+ this.acceptAutocomplete();
1141
+ this.needsRender = true;
1142
+ return;
1143
+ }
1144
+ // Otherwise ignore tab (or could insert spaces)
1145
+ return;
1146
+ }
1147
+ // Enter - handle multiline continuation, autocomplete, then execute
1148
+ if (key.name === 'return') {
1149
+ // Check for backslash continuation at end of current line
1150
+ const currentLine = this.getCurrentLineContent();
1151
+ if (currentLine.endsWith('\\')) {
1152
+ // Remove backslash and add new line
1153
+ this.lines[this.currentLine] = currentLine.slice(0, -1);
1154
+ this.lines.push('');
1155
+ this.currentLine++;
1156
+ this.cursorPos = 0;
1157
+ this.closeAutocomplete();
1158
+ this.resetHistoryNavigation();
1159
+ this.needsRender = true;
1160
+ return;
1161
+ }
1162
+ // If file autocomplete is active, accept the selection and submit
1163
+ if (this.fileAutocomplete.active && this.fileAutocomplete.matches.length > 0) {
1164
+ this.acceptAutocomplete();
1165
+ // Fall through to submit the message
1166
+ }
1167
+ // If command autocomplete is active, accept the selection first
1168
+ if (this.autocomplete.active && this.autocomplete.matches.length > 0) {
1169
+ this.acceptAutocomplete();
1170
+ // Fall through to execute the command
1171
+ }
1172
+ let input = this.getInputValue().trim();
1173
+ // If input is empty but we have a ghost text suggestion, accept it
1174
+ if (!input && this.suggestion) {
1175
+ input = this.suggestion;
1176
+ this.suggestion = null;
1177
+ }
1178
+ if (input) {
1179
+ // Add to history before processing (store as single line for history)
1180
+ this.addToHistory(input.replace(/\n/g, ' '));
1181
+ this.resetHistoryNavigation();
1182
+ if (this.agentRunning) {
1183
+ this.queuedInputs.push(input);
1184
+ this.needsRender = true;
1185
+ }
1186
+ else if (input.startsWith('/')) {
1187
+ const spaceIndex = input.indexOf(' ');
1188
+ const cmd = spaceIndex > 0 ? input.slice(1, spaceIndex) : input.slice(1);
1189
+ const args = spaceIndex > 0 ? input.slice(spaceIndex + 1) : '';
1190
+ this.closeAutocomplete();
1191
+ this.emit('command', cmd, args);
1192
+ }
1193
+ else {
1194
+ this.emit('submit', input);
1195
+ }
1196
+ this.clearInput();
1197
+ this.closeAutocomplete();
1198
+ this.needsRender = true;
1199
+ }
1200
+ return;
1201
+ }
1202
+ // Arrow Up - autocomplete > multiline navigation > history
1203
+ if (key.name === 'up') {
1204
+ // 1. Autocomplete navigation takes priority
1205
+ if (this.navigateAutocompleteUp()) {
1206
+ this.needsRender = true;
1207
+ return;
1208
+ }
1209
+ // 2. Navigate to previous line in multiline input
1210
+ if (this.currentLine > 0) {
1211
+ this.currentLine--;
1212
+ // Try to keep same cursor position, but clamp to line length
1213
+ this.cursorPos = Math.min(this.cursorPos, this.lines[this.currentLine].length);
1214
+ this.needsRender = true;
1215
+ return;
1216
+ }
1217
+ // 3. History navigation (only when at first line)
1218
+ if (this.navigateHistoryUp()) {
1219
+ this.closeAutocomplete();
1220
+ this.needsRender = true;
1221
+ }
1222
+ return;
1223
+ }
1224
+ // Arrow Down - autocomplete > multiline navigation > history
1225
+ if (key.name === 'down') {
1226
+ // 1. Autocomplete navigation takes priority
1227
+ if (this.navigateAutocompleteDown()) {
1228
+ this.needsRender = true;
1229
+ return;
1230
+ }
1231
+ // 2. Navigate to next line in multiline input
1232
+ if (this.currentLine < this.lines.length - 1) {
1233
+ this.currentLine++;
1234
+ // Try to keep same cursor position, but clamp to line length
1235
+ this.cursorPos = Math.min(this.cursorPos, this.lines[this.currentLine].length);
1236
+ this.needsRender = true;
1237
+ return;
1238
+ }
1239
+ // 3. History forward (only when at last line)
1240
+ if (this.historyIndex >= 0 && this.navigateHistoryDown()) {
1241
+ this.needsRender = true;
1242
+ }
1243
+ return;
1244
+ }
1245
+ // Backspace
1246
+ if (key.name === 'backspace') {
1247
+ if (this.cursorPos > 0) {
1248
+ // Delete character in current line
1249
+ const line = this.lines[this.currentLine];
1250
+ this.lines[this.currentLine] = line.slice(0, this.cursorPos - 1) + line.slice(this.cursorPos);
1251
+ this.cursorPos--;
1252
+ this.resetHistoryNavigation();
1253
+ this.updateAutocomplete();
1254
+ this.needsRender = true;
1255
+ }
1256
+ else if (this.currentLine > 0) {
1257
+ // At start of line - merge with previous line
1258
+ const currentLine = this.lines[this.currentLine];
1259
+ const prevLine = this.lines[this.currentLine - 1];
1260
+ this.lines[this.currentLine - 1] = prevLine + currentLine;
1261
+ this.lines.splice(this.currentLine, 1);
1262
+ this.currentLine--;
1263
+ this.cursorPos = prevLine.length;
1264
+ this.resetHistoryNavigation();
1265
+ this.updateAutocomplete();
1266
+ this.needsRender = true;
1267
+ }
1268
+ return;
1269
+ }
1270
+ // Delete
1271
+ if (key.name === 'delete') {
1272
+ const line = this.lines[this.currentLine];
1273
+ if (this.cursorPos < line.length) {
1274
+ // Delete character in current line
1275
+ this.lines[this.currentLine] = line.slice(0, this.cursorPos) + line.slice(this.cursorPos + 1);
1276
+ this.resetHistoryNavigation();
1277
+ this.updateAutocomplete();
1278
+ this.needsRender = true;
1279
+ }
1280
+ else if (this.currentLine < this.lines.length - 1) {
1281
+ // At end of line - merge with next line
1282
+ const nextLine = this.lines[this.currentLine + 1];
1283
+ this.lines[this.currentLine] = line + nextLine;
1284
+ this.lines.splice(this.currentLine + 1, 1);
1285
+ this.resetHistoryNavigation();
1286
+ this.updateAutocomplete();
1287
+ this.needsRender = true;
1288
+ }
1289
+ return;
1290
+ }
1291
+ // Arrow keys (left/right for cursor movement with multiline support)
1292
+ if (key.name === 'left') {
1293
+ if (this.cursorPos > 0) {
1294
+ this.cursorPos--;
1295
+ this.needsRender = true;
1296
+ }
1297
+ else if (this.currentLine > 0) {
1298
+ // Move to end of previous line
1299
+ this.currentLine--;
1300
+ this.cursorPos = this.lines[this.currentLine].length;
1301
+ this.needsRender = true;
1302
+ }
1303
+ return;
1304
+ }
1305
+ if (key.name === 'right') {
1306
+ const lineLen = this.lines[this.currentLine].length;
1307
+ if (this.cursorPos < lineLen) {
1308
+ this.cursorPos++;
1309
+ this.needsRender = true;
1310
+ }
1311
+ else if (this.currentLine < this.lines.length - 1) {
1312
+ // Move to start of next line
1313
+ this.currentLine++;
1314
+ this.cursorPos = 0;
1315
+ this.needsRender = true;
1316
+ }
1317
+ return;
1318
+ }
1319
+ if (key.name === 'home') {
1320
+ this.cursorPos = 0;
1321
+ this.needsRender = true;
1322
+ return;
1323
+ }
1324
+ if (key.name === 'end') {
1325
+ this.cursorPos = this.lines[this.currentLine].length;
1326
+ this.needsRender = true;
1327
+ return;
1328
+ }
1329
+ // Shift+Tab - mode change
1330
+ if (key.shift && key.name === 'tab') {
1331
+ this.emit('modeChange');
1332
+ return;
1333
+ }
1334
+ // Regular character
1335
+ if (str && str.length === 1 && str.charCodeAt(0) >= 32) {
1336
+ // Clear ghost text suggestion when user starts typing
1337
+ if (this.suggestion) {
1338
+ this.suggestion = null;
1339
+ }
1340
+ const line = this.lines[this.currentLine];
1341
+ this.lines[this.currentLine] =
1342
+ line.slice(0, this.cursorPos) + str + line.slice(this.cursorPos);
1343
+ this.cursorPos++;
1344
+ this.resetHistoryNavigation();
1345
+ this.updateAutocomplete();
1346
+ this.needsRender = true;
1347
+ }
1348
+ }
1349
+ }