@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,751 @@
1
+ /**
2
+ * Terminal Renderer
3
+ *
4
+ * Single point of terminal output. All rendering goes through this class.
5
+ * Eliminates race conditions by ensuring only one writer.
6
+ *
7
+ * Key responsibilities:
8
+ * - Manage render modes (MENU, REPL, OVERLAY)
9
+ * - Set up and manage scroll regions (REPL mode)
10
+ * - Collect content from providers
11
+ * - Write to terminal (single writer)
12
+ * - Handle resize events
13
+ * - Route keyboard input
14
+ */
15
+ import { EventEmitter } from 'events';
16
+ import { RenderMode, isValidTransition } from './render-modes.js';
17
+ // Re-export RenderMode for convenience
18
+ export { RenderMode } from './render-modes.js';
19
+ import * as codes from './terminal-codes.js';
20
+ import { getPhysicalLineCount } from './line-utils.js';
21
+ // =============================================================================
22
+ // TerminalRenderer Class
23
+ // =============================================================================
24
+ export class TerminalRenderer extends EventEmitter {
25
+ // === Configuration ===
26
+ renderDebounceMs;
27
+ debug;
28
+ useScrollRegions;
29
+ // === State ===
30
+ mode;
31
+ terminalRows = 24;
32
+ terminalCols = 80;
33
+ footerHeight = 0;
34
+ scrollRegionBottom = 0;
35
+ isRendering = false;
36
+ renderPending = false;
37
+ renderTimer = null;
38
+ started = false;
39
+ simpleMode = false;
40
+ // === Content Queues ===
41
+ pendingScrollContent = [];
42
+ // === Scroll Position Tracking ===
43
+ // Track where the next output should go in scroll zone
44
+ scrollCursorRow = 1; // 1-indexed, starts at top
45
+ // === Providers ===
46
+ providers = {};
47
+ // === Footer (for scroll region mode) ===
48
+ footer = null;
49
+ // === Resize Handler ===
50
+ resizeHandler = null;
51
+ // === Render Loop (for scroll region mode) ===
52
+ renderLoopTimer = null;
53
+ renderLoopInterval = 60; // 60ms = ~16fps
54
+ constructor(options = {}) {
55
+ super();
56
+ this.mode = options.initialMode ?? RenderMode.REPL;
57
+ this.renderDebounceMs = options.renderDebounceMs ?? 16;
58
+ this.debug = options.debug ?? false;
59
+ this.useScrollRegions = options.useScrollRegions ?? false;
60
+ this.updateDimensions();
61
+ }
62
+ // ===========================================================================
63
+ // Lifecycle
64
+ // ===========================================================================
65
+ /**
66
+ * Start the renderer.
67
+ * Sets up terminal state and begins render loop.
68
+ */
69
+ start() {
70
+ if (this.started) {
71
+ this.log('Already started, ignoring start() call');
72
+ return;
73
+ }
74
+ this.log(`Starting in ${this.mode} mode`);
75
+ // Check if stdout is a TTY
76
+ if (!process.stdout.isTTY) {
77
+ this.log('Not a TTY, using simple mode');
78
+ this.simpleMode = true;
79
+ this.started = true;
80
+ return;
81
+ }
82
+ // Update dimensions
83
+ this.updateDimensions();
84
+ // Set up resize handler
85
+ this.resizeHandler = () => {
86
+ this.handleResize();
87
+ };
88
+ process.stdout.on('resize', this.resizeHandler);
89
+ // Mark as started
90
+ this.started = true;
91
+ // Initial mode setup
92
+ this.setupMode(this.mode);
93
+ // Start render loop if using scroll regions
94
+ if (this.useScrollRegions) {
95
+ this.startRenderLoop();
96
+ }
97
+ this.log('Started successfully');
98
+ }
99
+ /**
100
+ * Start the render loop (for scroll region mode)
101
+ * Polls footer for changes and re-renders
102
+ */
103
+ startRenderLoop() {
104
+ if (this.renderLoopTimer)
105
+ return;
106
+ this.log('Starting render loop');
107
+ this.renderLoopTimer = setInterval(() => {
108
+ if (this.started && !this.simpleMode) {
109
+ // In scroll region mode, always re-render to pick up footer changes
110
+ this.forceRender();
111
+ }
112
+ }, this.renderLoopInterval);
113
+ }
114
+ /**
115
+ * Stop the render loop
116
+ */
117
+ stopRenderLoop() {
118
+ if (this.renderLoopTimer) {
119
+ clearInterval(this.renderLoopTimer);
120
+ this.renderLoopTimer = null;
121
+ this.log('Render loop stopped');
122
+ }
123
+ }
124
+ /**
125
+ * Stop the renderer.
126
+ * Cleans up terminal state and stops render loop.
127
+ */
128
+ stop() {
129
+ if (!this.started) {
130
+ this.log('Not started, ignoring stop() call');
131
+ return;
132
+ }
133
+ this.log('Stopping');
134
+ // Stop render loop
135
+ this.stopRenderLoop();
136
+ // Clear any pending render
137
+ if (this.renderTimer) {
138
+ clearTimeout(this.renderTimer);
139
+ this.renderTimer = null;
140
+ }
141
+ // Remove resize handler
142
+ if (this.resizeHandler) {
143
+ process.stdout.off('resize', this.resizeHandler);
144
+ this.resizeHandler = null;
145
+ }
146
+ // Reset terminal state (only if TTY)
147
+ if (!this.simpleMode) {
148
+ // Reset scroll region
149
+ this.write(codes.resetScrollRegion);
150
+ // Show cursor
151
+ this.write(codes.showCursor);
152
+ }
153
+ this.started = false;
154
+ this.log('Stopped');
155
+ }
156
+ /**
157
+ * Check if renderer is running
158
+ */
159
+ isStarted() {
160
+ return this.started;
161
+ }
162
+ // ===========================================================================
163
+ // Mode Management
164
+ // ===========================================================================
165
+ /**
166
+ * Get current render mode
167
+ */
168
+ getMode() {
169
+ return this.mode;
170
+ }
171
+ /**
172
+ * Switch to a different render mode
173
+ */
174
+ setMode(newMode, reason = 'programmatic') {
175
+ if (newMode === this.mode) {
176
+ this.log(`Already in ${newMode} mode, ignoring`);
177
+ return;
178
+ }
179
+ if (!isValidTransition(this.mode, newMode)) {
180
+ this.log(`Invalid transition: ${this.mode} -> ${newMode}`);
181
+ this.emit('error', new Error(`Invalid mode transition: ${this.mode} -> ${newMode}`));
182
+ return;
183
+ }
184
+ const transition = {
185
+ from: this.mode,
186
+ to: newMode,
187
+ reason,
188
+ timestamp: Date.now(),
189
+ };
190
+ this.log(`Mode transition: ${this.mode} -> ${newMode} (${reason})`);
191
+ // Cleanup current mode
192
+ this.cleanupMode(this.mode);
193
+ // Switch mode
194
+ this.mode = newMode;
195
+ // Setup new mode
196
+ this.setupMode(newMode);
197
+ // Emit event
198
+ this.emit('mode-change', transition);
199
+ // Force render
200
+ this.forceRender();
201
+ }
202
+ /**
203
+ * Set up terminal for a specific mode
204
+ */
205
+ setupMode(mode) {
206
+ if (this.simpleMode)
207
+ return;
208
+ switch (mode) {
209
+ case RenderMode.MENU:
210
+ // Full screen, no scroll region, hidden cursor
211
+ this.write(codes.resetScrollRegion);
212
+ this.write(codes.clearScreenAndHome);
213
+ this.write(codes.hideCursor);
214
+ break;
215
+ case RenderMode.REPL:
216
+ // Set up scroll region (will be adjusted on first render)
217
+ this.footerHeight = 0; // Will be calculated
218
+ this.write(codes.resetScrollRegion);
219
+ this.write(codes.showCursor);
220
+ break;
221
+ case RenderMode.OVERLAY:
222
+ // Full screen, no scroll region
223
+ this.write(codes.resetScrollRegion);
224
+ this.write(codes.clearScreenAndHome);
225
+ // Cursor visibility depends on overlay
226
+ break;
227
+ }
228
+ }
229
+ /**
230
+ * Clean up terminal state when leaving a mode
231
+ */
232
+ cleanupMode(mode) {
233
+ if (this.simpleMode)
234
+ return;
235
+ switch (mode) {
236
+ case RenderMode.MENU:
237
+ // Nothing special to clean up
238
+ break;
239
+ case RenderMode.REPL:
240
+ // Reset scroll region before leaving
241
+ this.write(codes.resetScrollRegion);
242
+ break;
243
+ case RenderMode.OVERLAY:
244
+ // Nothing special to clean up
245
+ break;
246
+ }
247
+ }
248
+ // ===========================================================================
249
+ // Provider Management
250
+ // ===========================================================================
251
+ /**
252
+ * Register content providers
253
+ */
254
+ setProviders(providers) {
255
+ this.providers = { ...this.providers, ...providers };
256
+ this.log(`Providers updated: ${Object.keys(providers).join(', ')}`);
257
+ }
258
+ /**
259
+ * Get a specific provider
260
+ */
261
+ getProvider(key) {
262
+ return this.providers[key];
263
+ }
264
+ // ===========================================================================
265
+ // Footer Integration (for scroll region mode)
266
+ // ===========================================================================
267
+ /**
268
+ * Set the Footer instance for scroll region rendering
269
+ * When useScrollRegions is true, TerminalRenderer uses footer.getFooterLines()
270
+ * and manages the scroll region. Footer's own render loop is disabled.
271
+ */
272
+ setFooter(footer) {
273
+ this.footer = footer;
274
+ this.log('Footer connected');
275
+ // When scroll regions are enabled, disable Footer's internal render loop
276
+ // TerminalRenderer will handle all rendering
277
+ if (this.useScrollRegions) {
278
+ footer.setExternalRenderer(true);
279
+ this.log('Footer external renderer enabled (scroll region mode)');
280
+ }
281
+ }
282
+ /**
283
+ * Get the connected Footer instance
284
+ */
285
+ getFooter() {
286
+ return this.footer;
287
+ }
288
+ // ===========================================================================
289
+ // Scroll Zone (REPL mode)
290
+ // ===========================================================================
291
+ /**
292
+ * Queue content for scroll zone (REPL mode only)
293
+ * Content will be written on next render
294
+ *
295
+ * In legacy mode (useScrollRegions=false), writes immediately via console.log
296
+ * In scroll region mode, queues for next render cycle
297
+ */
298
+ appendToScrollZone(content) {
299
+ if (this.mode !== RenderMode.REPL) {
300
+ this.log('appendToScrollZone called outside REPL mode, ignoring');
301
+ return;
302
+ }
303
+ const lines = Array.isArray(content) ? content : content.split('\n');
304
+ if (!this.useScrollRegions) {
305
+ // Legacy mode: write immediately via console.log
306
+ // This works with existing Footer class that manages its own rendering
307
+ for (const line of lines) {
308
+ console.log(line);
309
+ }
310
+ return;
311
+ }
312
+ // Scroll region mode: queue for render
313
+ this.pendingScrollContent.push(...lines);
314
+ this.requestRender();
315
+ }
316
+ /**
317
+ * Flush pending scroll content to terminal
318
+ *
319
+ * Uses tracked scroll position to append content naturally.
320
+ * When content would exceed scroll region, scrolling occurs automatically.
321
+ */
322
+ flushScrollContent() {
323
+ if (this.pendingScrollContent.length === 0)
324
+ return;
325
+ this.log(`Flushing ${String(this.pendingScrollContent.length)} lines at row ${String(this.scrollCursorRow)}`);
326
+ // Move to current scroll position (within scroll region)
327
+ this.write(codes.cursorTo(this.scrollCursorRow, 1));
328
+ // Write each line
329
+ for (const line of this.pendingScrollContent) {
330
+ this.write(line);
331
+ this.write('\n');
332
+ // Track position (clamped to scroll region bottom)
333
+ this.scrollCursorRow++;
334
+ if (this.scrollCursorRow > this.scrollRegionBottom) {
335
+ // Content will scroll, cursor stays at bottom of scroll region
336
+ this.scrollCursorRow = this.scrollRegionBottom;
337
+ }
338
+ }
339
+ this.pendingScrollContent = [];
340
+ }
341
+ // ===========================================================================
342
+ // Rendering
343
+ // ===========================================================================
344
+ /**
345
+ * Request a render (debounced)
346
+ */
347
+ requestRender() {
348
+ if (!this.started || this.simpleMode)
349
+ return;
350
+ if (this.renderPending)
351
+ return;
352
+ this.renderPending = true;
353
+ if (this.renderTimer) {
354
+ clearTimeout(this.renderTimer);
355
+ }
356
+ this.renderTimer = setTimeout(() => {
357
+ this.doRender();
358
+ }, this.renderDebounceMs);
359
+ }
360
+ /**
361
+ * Force immediate render (bypasses debounce)
362
+ */
363
+ forceRender() {
364
+ if (!this.started)
365
+ return;
366
+ // Clear any pending debounced render
367
+ if (this.renderTimer) {
368
+ clearTimeout(this.renderTimer);
369
+ this.renderTimer = null;
370
+ }
371
+ this.renderPending = false;
372
+ this.doRender();
373
+ }
374
+ /**
375
+ * Main render method
376
+ */
377
+ doRender() {
378
+ if (this.isRendering) {
379
+ // Already rendering, schedule another
380
+ this.renderPending = true;
381
+ return;
382
+ }
383
+ this.isRendering = true;
384
+ this.renderPending = false;
385
+ try {
386
+ if (this.simpleMode) {
387
+ this.renderSimpleMode();
388
+ }
389
+ else {
390
+ switch (this.mode) {
391
+ case RenderMode.MENU:
392
+ this.renderMenuMode();
393
+ break;
394
+ case RenderMode.REPL:
395
+ this.renderReplMode();
396
+ break;
397
+ case RenderMode.OVERLAY:
398
+ this.renderOverlayMode();
399
+ break;
400
+ }
401
+ }
402
+ this.emit('render', this.mode);
403
+ }
404
+ catch (error) {
405
+ this.emit('error', error instanceof Error ? error : new Error(String(error)));
406
+ }
407
+ finally {
408
+ this.isRendering = false;
409
+ // Check if another render was requested during this one
410
+ // (renderPending can be set by callbacks during render)
411
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
412
+ if (this.renderPending) {
413
+ this.requestRender();
414
+ }
415
+ }
416
+ }
417
+ /**
418
+ * Render in simple mode (non-TTY)
419
+ */
420
+ renderSimpleMode() {
421
+ // In simple mode, just flush any pending content
422
+ for (const line of this.pendingScrollContent) {
423
+ console.log(line);
424
+ }
425
+ this.pendingScrollContent = [];
426
+ }
427
+ /**
428
+ * Render MENU mode
429
+ */
430
+ renderMenuMode() {
431
+ const menuProvider = this.providers.menu;
432
+ if (!menuProvider) {
433
+ this.log('No menu provider, skipping menu render');
434
+ return;
435
+ }
436
+ const lines = menuProvider.getLines();
437
+ // Clear and render from top
438
+ this.write(codes.cursorHome);
439
+ this.write(codes.clearToEndOfScreen);
440
+ for (const line of lines) {
441
+ this.write(line + '\n');
442
+ }
443
+ }
444
+ /**
445
+ * Render REPL mode
446
+ */
447
+ renderReplMode() {
448
+ // If scroll regions disabled (legacy mode), do nothing
449
+ // Footer manages its own rendering and scroll content goes via console.log
450
+ if (!this.useScrollRegions) {
451
+ return;
452
+ }
453
+ // 1. Build footer content (from Footer or providers)
454
+ const footerLines = this.footer ? this.footer.getFooterLines() : this.buildFooterLines();
455
+ const newFooterHeight = this.calculatePhysicalHeight(footerLines);
456
+ this.log(`renderReplMode: footerHeight=${String(newFooterHeight)}, pending=${String(this.pendingScrollContent.length)}, scrollCursor=${String(this.scrollCursorRow)}`);
457
+ // 2. Adjust scroll region if footer height changed
458
+ if (newFooterHeight !== this.footerHeight) {
459
+ this.adjustScrollRegion(newFooterHeight);
460
+ }
461
+ // 3. Flush pending scroll content
462
+ if (this.pendingScrollContent.length > 0) {
463
+ this.flushScrollContent();
464
+ }
465
+ // 4. Render footer
466
+ this.renderFooter(footerLines);
467
+ // 5. Position cursor if footer is connected
468
+ if (this.footer) {
469
+ this.positionFooterCursor();
470
+ }
471
+ }
472
+ /**
473
+ * Build footer lines from providers
474
+ */
475
+ buildFooterLines() {
476
+ const lines = [];
477
+ // 1. Subagent status (if any)
478
+ if (this.providers.subagent) {
479
+ lines.push(...this.providers.subagent.getLines());
480
+ }
481
+ // 2. Spinner OR Todo list (mutually exclusive)
482
+ if (this.providers.spinner?.isAgentRunning()) {
483
+ lines.push(...(this.providers.spinner.getLines()));
484
+ }
485
+ else if (this.providers.todo) {
486
+ lines.push(...this.providers.todo.getLines());
487
+ }
488
+ // 3. Input prompt (includes separator)
489
+ if (this.providers.input) {
490
+ lines.push(...this.providers.input.getLines());
491
+ }
492
+ // 4. Queued messages
493
+ if (this.providers.queue) {
494
+ lines.push(...this.providers.queue.getLines());
495
+ }
496
+ // 5. Mode indicator
497
+ if (this.providers.mode) {
498
+ lines.push(...this.providers.mode.getLines());
499
+ }
500
+ // Cap at 80% of terminal height
501
+ const maxHeight = Math.floor(this.terminalRows * 0.8);
502
+ const physicalHeight = this.calculatePhysicalHeight(lines);
503
+ if (physicalHeight > maxHeight) {
504
+ // TODO: Implement truncation (Phase 4)
505
+ this.log(`Footer too tall: ${String(physicalHeight)} > ${String(maxHeight)}`);
506
+ }
507
+ return lines;
508
+ }
509
+ /**
510
+ * Calculate physical height of lines (accounting for wrapping)
511
+ */
512
+ calculatePhysicalHeight(lines) {
513
+ let height = 0;
514
+ for (const line of lines) {
515
+ height += getPhysicalLineCount(line, this.terminalCols);
516
+ }
517
+ return height;
518
+ }
519
+ /**
520
+ * Adjust scroll region when footer height changes
521
+ */
522
+ adjustScrollRegion(newFooterHeight) {
523
+ if (newFooterHeight === this.footerHeight)
524
+ return;
525
+ const oldBottom = this.scrollRegionBottom;
526
+ this.footerHeight = newFooterHeight;
527
+ this.scrollRegionBottom = this.terminalRows - newFooterHeight;
528
+ this.log(`Scroll region: [1-${String(oldBottom)}] -> [1-${String(this.scrollRegionBottom)}] (footer: ${String(newFooterHeight)} lines)`);
529
+ // Set new scroll region (1-indexed)
530
+ this.write(codes.setScrollRegion(1, this.scrollRegionBottom));
531
+ // Keep scroll cursor within new bounds
532
+ if (this.scrollCursorRow > this.scrollRegionBottom) {
533
+ this.scrollCursorRow = this.scrollRegionBottom;
534
+ }
535
+ }
536
+ /**
537
+ * Render footer content
538
+ */
539
+ renderFooter(lines) {
540
+ if (lines.length === 0)
541
+ return;
542
+ // Move to footer area (row after scroll region)
543
+ const footerStartRow = this.scrollRegionBottom + 1;
544
+ this.write(codes.cursorTo(footerStartRow, 1));
545
+ // Clear footer area
546
+ this.write(codes.clearToEndOfScreen);
547
+ // Write footer lines
548
+ for (let i = 0; i < lines.length; i++) {
549
+ if (i > 0)
550
+ this.write('\n');
551
+ this.write(lines[i]);
552
+ }
553
+ // Position cursor for input
554
+ this.positionCursor();
555
+ }
556
+ /**
557
+ * Position cursor for input (provider-based)
558
+ */
559
+ positionCursor() {
560
+ const inputProvider = this.providers.input;
561
+ if (!inputProvider)
562
+ return;
563
+ const cursorPos = inputProvider.getCursorPosition();
564
+ // Calculate absolute row: footer start + provider's relative row
565
+ const footerStartRow = this.scrollRegionBottom + 1;
566
+ // Find which line in footer is the input
567
+ // For now, assume input is after subagent + spinner/todo lines
568
+ let inputStartRow = footerStartRow;
569
+ if (this.providers.subagent) {
570
+ inputStartRow += this.providers.subagent.getLines().length;
571
+ }
572
+ if (this.providers.spinner?.isAgentRunning()) {
573
+ inputStartRow += this.providers.spinner.getLines().length;
574
+ }
575
+ else if (this.providers.todo) {
576
+ inputStartRow += this.providers.todo.getLines().length;
577
+ }
578
+ // Add provider's relative row
579
+ const absoluteRow = inputStartRow + cursorPos.row;
580
+ const absoluteCol = cursorPos.col + 1; // 1-indexed
581
+ this.write(codes.cursorTo(absoluteRow, absoluteCol));
582
+ this.write(codes.showCursor);
583
+ }
584
+ /**
585
+ * Position cursor for Footer integration (scroll region mode)
586
+ */
587
+ positionFooterCursor() {
588
+ if (!this.footer)
589
+ return;
590
+ const cursorPos = this.footer.getFooterCursorPosition();
591
+ const footerStartRow = this.scrollRegionBottom + 1;
592
+ // cursorPos.row is 0-indexed from footer start
593
+ const absoluteRow = footerStartRow + cursorPos.row;
594
+ const absoluteCol = cursorPos.col + 1; // 1-indexed
595
+ this.write(codes.cursorTo(absoluteRow, absoluteCol));
596
+ this.write(codes.showCursor);
597
+ }
598
+ /**
599
+ * Render OVERLAY mode
600
+ */
601
+ renderOverlayMode() {
602
+ const overlayProvider = this.providers.overlay;
603
+ if (!overlayProvider || !overlayProvider.isActive()) {
604
+ this.log('No active overlay provider');
605
+ return;
606
+ }
607
+ const lines = overlayProvider.getLines();
608
+ // Clear and render from top
609
+ this.write(codes.cursorHome);
610
+ this.write(codes.clearToEndOfScreen);
611
+ for (let i = 0; i < lines.length; i++) {
612
+ if (i > 0)
613
+ this.write('\n');
614
+ this.write(lines[i]);
615
+ }
616
+ // Position cursor if overlay needs it
617
+ const cursorPos = overlayProvider.getCursorPosition();
618
+ if (cursorPos) {
619
+ this.write(codes.showCursor);
620
+ this.write(codes.cursorTo(cursorPos.row + 1, cursorPos.col + 1));
621
+ }
622
+ else {
623
+ this.write(codes.hideCursor);
624
+ }
625
+ }
626
+ // ===========================================================================
627
+ // Resize Handling
628
+ // ===========================================================================
629
+ /**
630
+ * Handle terminal resize
631
+ */
632
+ handleResize() {
633
+ const oldCols = this.terminalCols;
634
+ const oldRows = this.terminalRows;
635
+ this.updateDimensions();
636
+ this.log(`Resize: ${String(oldCols)}x${String(oldRows)} -> ${String(this.terminalCols)}x${String(this.terminalRows)}`);
637
+ // Emit event
638
+ this.emit('resize', this.terminalCols, this.terminalRows);
639
+ // Mode-specific handling
640
+ switch (this.mode) {
641
+ case RenderMode.REPL:
642
+ // Recalculate scroll region
643
+ if (this.footerHeight > 0) {
644
+ this.scrollRegionBottom = this.terminalRows - this.footerHeight;
645
+ this.write(codes.setScrollRegion(1, this.scrollRegionBottom));
646
+ }
647
+ break;
648
+ case RenderMode.MENU:
649
+ case RenderMode.OVERLAY:
650
+ // Full screen modes just re-render
651
+ break;
652
+ }
653
+ // Force full re-render
654
+ this.forceRender();
655
+ }
656
+ /**
657
+ * Update terminal dimensions from stdout
658
+ */
659
+ updateDimensions() {
660
+ this.terminalRows = process.stdout.rows || 24;
661
+ this.terminalCols = process.stdout.columns || 80;
662
+ }
663
+ /**
664
+ * Get current terminal dimensions
665
+ */
666
+ getDimensions() {
667
+ return { rows: this.terminalRows, cols: this.terminalCols };
668
+ }
669
+ // ===========================================================================
670
+ // Low-Level Output
671
+ // ===========================================================================
672
+ /**
673
+ * Write to stdout (single output point)
674
+ */
675
+ write(text) {
676
+ if (text.length === 0)
677
+ return;
678
+ process.stdout.write(text);
679
+ }
680
+ // ===========================================================================
681
+ // Keyboard Routing (stub for Phase 2+)
682
+ // ===========================================================================
683
+ /**
684
+ * Route keyboard input to appropriate handler
685
+ */
686
+ handleKeyboard(key) {
687
+ switch (this.mode) {
688
+ case RenderMode.MENU:
689
+ if (this.providers.menu) {
690
+ const action = this.providers.menu.handleKey(key);
691
+ if (action === 'select') {
692
+ // TODO: Handle menu selection (Phase 2)
693
+ }
694
+ this.requestRender();
695
+ }
696
+ break;
697
+ case RenderMode.REPL:
698
+ if (this.providers.input) {
699
+ const action = this.providers.input.handleKey(key);
700
+ if (action) {
701
+ // TODO: Handle input action (Phase 4)
702
+ }
703
+ this.requestRender();
704
+ }
705
+ break;
706
+ case RenderMode.OVERLAY:
707
+ if (this.providers.overlay) {
708
+ const action = this.providers.overlay.handleKey(key);
709
+ if (action?.type === 'close') {
710
+ this.setMode(RenderMode.REPL, 'overlay-closed');
711
+ }
712
+ this.requestRender();
713
+ }
714
+ break;
715
+ }
716
+ }
717
+ // ===========================================================================
718
+ // Debug Logging
719
+ // ===========================================================================
720
+ /**
721
+ * Log debug message if debug mode enabled
722
+ */
723
+ log(message) {
724
+ if (this.debug) {
725
+ const timestamp = new Date().toISOString().slice(11, 23);
726
+ console.error(`[TerminalRenderer ${timestamp}] ${message}`);
727
+ }
728
+ }
729
+ }
730
+ // =============================================================================
731
+ // Singleton Export (optional)
732
+ // =============================================================================
733
+ let defaultRenderer = null;
734
+ /**
735
+ * Get or create the default renderer instance
736
+ */
737
+ export function getRenderer(options) {
738
+ if (!defaultRenderer) {
739
+ defaultRenderer = new TerminalRenderer(options);
740
+ }
741
+ return defaultRenderer;
742
+ }
743
+ /**
744
+ * Reset the default renderer (for testing)
745
+ */
746
+ export function resetRenderer() {
747
+ if (defaultRenderer) {
748
+ defaultRenderer.stop();
749
+ defaultRenderer = null;
750
+ }
751
+ }