@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,1071 @@
1
+ /**
2
+ * Document Detail Overlay V2
3
+ *
4
+ * Fullscreen markdown document viewer AND editor with vim-style navigation.
5
+ * Separate from DocsOverlayV2 (inline list) to allow proper fullscreen rendering.
6
+ *
7
+ * Features:
8
+ * - Full-screen document viewing with markdown rendering
9
+ * - Vim-style navigation (j/k, g/G, PgUp/PgDn)
10
+ * - Edit mode with raw markdown editing
11
+ * - Save to database (Ctrl+S)
12
+ * - Dirty check with save prompt on exit
13
+ */
14
+ import chalk from 'chalk';
15
+ import hljs from 'highlight.js';
16
+ // =============================================================================
17
+ // Alternate Screen Buffer Management
18
+ // =============================================================================
19
+ let inAlternateScreen = false;
20
+ function enterAlternateScreen() {
21
+ if (inAlternateScreen)
22
+ return;
23
+ process.stdout.write('\x1b[?1049h'); // Switch to alternate screen
24
+ process.stdout.write('\x1b[H'); // Move cursor to home
25
+ inAlternateScreen = true;
26
+ }
27
+ function exitAlternateScreen() {
28
+ if (!inAlternateScreen)
29
+ return;
30
+ process.stdout.write('\x1b[?1049l'); // Switch back to main screen
31
+ inAlternateScreen = false;
32
+ }
33
+ // Ensure we exit alternate screen on process termination
34
+ function setupAlternateScreenCleanup() {
35
+ const cleanup = () => {
36
+ exitAlternateScreen();
37
+ };
38
+ // Handle normal exit
39
+ process.on('exit', cleanup);
40
+ // Handle Ctrl+C
41
+ process.on('SIGINT', () => {
42
+ cleanup();
43
+ process.exit(130);
44
+ });
45
+ // Handle termination
46
+ process.on('SIGTERM', () => {
47
+ cleanup();
48
+ process.exit(143);
49
+ });
50
+ // Handle uncaught exceptions
51
+ process.on('uncaughtException', (err) => {
52
+ cleanup();
53
+ console.error('Uncaught exception:', err);
54
+ process.exit(1);
55
+ });
56
+ // Handle unhandled promise rejections
57
+ process.on('unhandledRejection', (reason) => {
58
+ cleanup();
59
+ console.error('Unhandled rejection:', reason);
60
+ process.exit(1);
61
+ });
62
+ }
63
+ // Setup cleanup handlers once when module loads
64
+ setupAlternateScreenCleanup();
65
+ import { marked } from 'marked';
66
+ import { markedTerminal } from 'marked-terminal';
67
+ import { BaseOverlayV2 } from '../../base/overlay-base-v2.js';
68
+ import { renderBorder, isCtrlC, isClose } from '../../base/index.js';
69
+ import { documentRepository } from '../../../db/repositories/document-repository.js';
70
+ import { getCurrentTheme } from '../../../themes/index.js';
71
+ import * as terminal from '../../terminal.js';
72
+ // =============================================================================
73
+ // Register Additional Languages for Syntax Highlighting
74
+ // =============================================================================
75
+ // Register aliases for common language names that map to existing languages
76
+ const languageAliases = {
77
+ // Shell variants → bash
78
+ 'sh': 'bash', 'zsh': 'bash', 'fish': 'bash',
79
+ 'console': 'bash', 'terminal': 'bash', 'command': 'bash',
80
+ // JSX/TSX → base language (highlighting won't be perfect but won't error)
81
+ 'jsx': 'javascript', 'tsx': 'typescript',
82
+ // HTML variants → xml
83
+ 'htm': 'xml', 'html': 'xml', 'vue': 'xml', 'svelte': 'xml', 'astro': 'xml',
84
+ // Plain text variants
85
+ 'text': 'plaintext', 'txt': 'plaintext', 'log': 'plaintext',
86
+ 'output': 'plaintext', 'result': 'plaintext',
87
+ 'gitignore': 'plaintext', 'dockerignore': 'plaintext', 'ignore': 'plaintext',
88
+ 'requirements': 'plaintext', 'csv': 'plaintext', 'tsv': 'plaintext',
89
+ // Diff variants
90
+ 'patch': 'diff',
91
+ // JSON variants
92
+ 'jsonc': 'json', 'json5': 'json', 'jsonl': 'json', 'ndjson': 'json',
93
+ // Config file variants → ini
94
+ 'conf': 'ini', 'config': 'ini', 'env': 'ini', 'dotenv': 'ini',
95
+ 'editorconfig': 'ini', 'toml': 'ini', 'pipfile': 'ini',
96
+ // Infrastructure as code (approximate)
97
+ 'tf': 'json', 'hcl': 'json', 'terraform': 'json',
98
+ };
99
+ for (const [alias, target] of Object.entries(languageAliases)) {
100
+ try {
101
+ hljs.registerAliases(alias, { languageName: target });
102
+ }
103
+ catch {
104
+ // Ignore if alias already exists
105
+ }
106
+ }
107
+ // Register custom language definitions for diagram/DSL languages
108
+ // Mermaid diagrams
109
+ hljs.registerLanguage('mermaid', () => ({
110
+ name: 'Mermaid',
111
+ case_insensitive: true,
112
+ keywords: {
113
+ keyword: 'graph subgraph end flowchart sequenceDiagram classDiagram stateDiagram erDiagram gantt pie journey gitGraph',
114
+ built_in: 'TD TB BT RL LR participant actor loop alt else opt par and critical break'
115
+ },
116
+ contains: [
117
+ hljs.QUOTE_STRING_MODE,
118
+ hljs.COMMENT('%%', '$'),
119
+ { className: 'operator', begin: /-->|--o|--x|<-->|---|->|<-|==>|==|-.->|-.-/ },
120
+ { className: 'string', begin: /\|/, end: /\|/ },
121
+ { className: 'title', begin: /\[/, end: /\]/ }
122
+ ]
123
+ }));
124
+ // PlantUML diagrams
125
+ hljs.registerLanguage('plantuml', () => ({
126
+ name: 'PlantUML',
127
+ case_insensitive: true,
128
+ keywords: {
129
+ keyword: 'startuml enduml participant actor usecase class interface abstract enum component package node database cloud frame folder rectangle',
130
+ built_in: 'left right up down over note end activate deactivate return'
131
+ },
132
+ contains: [
133
+ hljs.QUOTE_STRING_MODE,
134
+ hljs.COMMENT("'", '$'),
135
+ { className: 'operator', begin: /-->|->|<--|<-|\.\.>|<\.\.|--|-/ }
136
+ ]
137
+ }));
138
+ // GraphQL
139
+ hljs.registerLanguage('graphql', () => ({
140
+ name: 'GraphQL',
141
+ keywords: {
142
+ keyword: 'query mutation subscription fragment on type interface union enum scalar input extend implements directive',
143
+ literal: 'true false null'
144
+ },
145
+ contains: [
146
+ hljs.QUOTE_STRING_MODE,
147
+ hljs.HASH_COMMENT_MODE,
148
+ { className: 'variable', begin: /\$\w+/ },
149
+ { className: 'type', begin: /\b[A-Z]\w*\b/ }
150
+ ]
151
+ }));
152
+ hljs.registerAliases('gql', { languageName: 'graphql' });
153
+ // Graphviz DOT
154
+ hljs.registerLanguage('dot', () => ({
155
+ name: 'DOT',
156
+ keywords: {
157
+ keyword: 'graph digraph subgraph node edge strict',
158
+ built_in: 'shape label color style fillcolor fontcolor fontsize rankdir rank'
159
+ },
160
+ contains: [
161
+ hljs.QUOTE_STRING_MODE,
162
+ hljs.C_LINE_COMMENT_MODE,
163
+ hljs.C_BLOCK_COMMENT_MODE,
164
+ { className: 'operator', begin: /->|--/ }
165
+ ]
166
+ }));
167
+ hljs.registerAliases('graphviz', { languageName: 'dot' });
168
+ // =============================================================================
169
+ // Constants
170
+ // =============================================================================
171
+ const DOC_TYPE_LABELS = {
172
+ 'prd': 'PRD',
173
+ 'architecture': 'ARCH',
174
+ 'design': 'DESIGN',
175
+ 'notes': 'NOTES',
176
+ };
177
+ const TAB_SIZE = 2;
178
+ // =============================================================================
179
+ // Markdown Rendering
180
+ // =============================================================================
181
+ function getThemedMarkedOptions(termWidth) {
182
+ const theme = getCurrentTheme();
183
+ const primaryColor = chalk.hex(theme.colors.primary);
184
+ const secondaryColor = chalk.hex(theme.colors.secondary || theme.colors.primary);
185
+ const mutedColor = chalk.hex(theme.colors.muted);
186
+ const borderColor = chalk.hex(theme.colors.border);
187
+ const fgColor = chalk.hex(theme.colors.foreground);
188
+ return {
189
+ showSectionPrefix: false,
190
+ reflowText: false,
191
+ width: Math.min(100, termWidth - 4),
192
+ unescape: true,
193
+ tab: 2,
194
+ heading: primaryColor.bold,
195
+ firstHeading: primaryColor.bold,
196
+ strong: fgColor.bold,
197
+ em: fgColor.italic,
198
+ link: secondaryColor,
199
+ href: secondaryColor.underline,
200
+ blockquote: mutedColor.italic,
201
+ del: mutedColor.strikethrough,
202
+ hr: borderColor,
203
+ codespan: secondaryColor,
204
+ code: secondaryColor,
205
+ html: mutedColor,
206
+ table: fgColor,
207
+ listitem: fgColor,
208
+ paragraph: fgColor,
209
+ tableOptions: {
210
+ style: { head: ['bold'], border: [] },
211
+ chars: {
212
+ 'top': '─', 'top-mid': '─', 'top-left': '─', 'top-right': '─',
213
+ 'bottom': '─', 'bottom-mid': '─', 'bottom-left': '─', 'bottom-right': '─',
214
+ 'left': ' ', 'left-mid': ' ', 'mid': '─', 'mid-mid': '─',
215
+ 'right': ' ', 'right-mid': ' ', 'middle': ' ',
216
+ },
217
+ },
218
+ };
219
+ }
220
+ function preprocessMarkdown(content) {
221
+ const lines = content.split('\n');
222
+ return lines.map(line => /^(\s*)\* /.test(line) ? line.replace(/^(\s*)\* /, '$1- ') : line).join('\n');
223
+ }
224
+ function postProcessInlineFormatting(text) {
225
+ return text.replace(/\*\*([^*]+)\*\*/g, (_match, content) => chalk.bold(content));
226
+ }
227
+ function renderMarkdownSync(content, termWidth) {
228
+ const preprocessed = preprocessMarkdown(content);
229
+ const options = getThemedMarkedOptions(termWidth);
230
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-explicit-any
231
+ marked.use(markedTerminal(options));
232
+ const rendered = marked.parse(preprocessed, { async: false });
233
+ const postProcessed = postProcessInlineFormatting(rendered);
234
+ return postProcessed.split('\n');
235
+ }
236
+ // =============================================================================
237
+ // Edit Mode Syntax Highlighting
238
+ // =============================================================================
239
+ // Import cli-highlight for code block highlighting
240
+ import { highlight as cliHighlight, supportsLanguage } from 'cli-highlight';
241
+ /**
242
+ * Highlight a line, considering code block context.
243
+ * Returns the highlighted line and updated code block state.
244
+ */
245
+ function highlightEditLine(line, codeBlockState, theme) {
246
+ const secondary = chalk.hex(theme.colors.secondary || theme.colors.primary);
247
+ // Check for code fence start/end
248
+ const fenceMatch = line.match(/^(`{3,}|~{3,})(\w*)\s*$/);
249
+ if (fenceMatch) {
250
+ const [, fence, lang] = fenceMatch;
251
+ const fenceChar = fence[0];
252
+ const fenceLength = fence.length;
253
+ if (!codeBlockState.inCodeBlock) {
254
+ // Starting a code block
255
+ return {
256
+ highlighted: secondary(line),
257
+ newState: {
258
+ inCodeBlock: true,
259
+ language: lang || 'plaintext',
260
+ fenceChar,
261
+ fenceLength,
262
+ },
263
+ };
264
+ }
265
+ else if (fenceChar === codeBlockState.fenceChar && fenceLength >= codeBlockState.fenceLength) {
266
+ // Ending a code block (matching or longer fence)
267
+ return {
268
+ highlighted: secondary(line),
269
+ newState: { inCodeBlock: false, language: '', fenceChar: '', fenceLength: 0 },
270
+ };
271
+ }
272
+ }
273
+ // Inside a code block - apply language-specific highlighting
274
+ if (codeBlockState.inCodeBlock) {
275
+ return {
276
+ highlighted: highlightCodeLine(line, codeBlockState.language, theme),
277
+ newState: codeBlockState,
278
+ };
279
+ }
280
+ // Regular markdown line
281
+ return {
282
+ highlighted: highlightMarkdownLine(line, theme),
283
+ newState: codeBlockState,
284
+ };
285
+ }
286
+ /**
287
+ * Highlight a single line of code using cli-highlight.
288
+ */
289
+ function highlightCodeLine(line, language, theme) {
290
+ // Map language aliases to supported languages
291
+ const langMap = {
292
+ 'js': 'javascript',
293
+ 'ts': 'typescript',
294
+ 'py': 'python',
295
+ 'rb': 'ruby',
296
+ 'sh': 'bash',
297
+ 'shell': 'bash',
298
+ 'zsh': 'bash',
299
+ 'yml': 'yaml',
300
+ 'md': 'markdown',
301
+ 'jsx': 'javascript',
302
+ 'tsx': 'typescript',
303
+ 'mermaid': 'plaintext', // highlight.js handles via our custom registration
304
+ 'plantuml': 'plaintext',
305
+ 'graphql': 'graphql',
306
+ 'gql': 'graphql',
307
+ 'dot': 'plaintext',
308
+ 'graphviz': 'plaintext',
309
+ };
310
+ const mappedLang = langMap[language.toLowerCase()] || language.toLowerCase();
311
+ // Check if language is supported
312
+ if (!supportsLanguage(mappedLang) && mappedLang !== 'plaintext') {
313
+ // Fall back to plaintext styling for unsupported languages
314
+ const secondary = chalk.hex(theme.colors.secondary || theme.colors.primary);
315
+ return secondary(line);
316
+ }
317
+ try {
318
+ // cli-highlight for supported languages
319
+ if (mappedLang === 'plaintext') {
320
+ const secondary = chalk.hex(theme.colors.secondary || theme.colors.primary);
321
+ return secondary(line);
322
+ }
323
+ return cliHighlight(line, { language: mappedLang, ignoreIllegals: true });
324
+ }
325
+ catch {
326
+ // Fallback to secondary color on error
327
+ const secondary = chalk.hex(theme.colors.secondary || theme.colors.primary);
328
+ return secondary(line);
329
+ }
330
+ }
331
+ /**
332
+ * Apply syntax highlighting to a raw markdown line for edit mode.
333
+ * Shows the raw markdown characters with visual styling.
334
+ */
335
+ function highlightMarkdownLine(line, theme) {
336
+ const primary = chalk.hex(theme.colors.primary);
337
+ const secondary = chalk.hex(theme.colors.secondary || theme.colors.primary);
338
+ const muted = chalk.hex(theme.colors.muted);
339
+ // Empty line
340
+ if (!line.trim()) {
341
+ return line;
342
+ }
343
+ // Headers: # ## ### etc.
344
+ const headerMatch = line.match(/^(#{1,6})\s(.*)$/);
345
+ if (headerMatch) {
346
+ return primary(headerMatch[1]) + ' ' + primary.bold(headerMatch[2]);
347
+ }
348
+ // Blockquote: > text
349
+ const blockquoteMatch = line.match(/^(>\s*)(.*)$/);
350
+ if (blockquoteMatch) {
351
+ return muted(blockquoteMatch[1]) + muted.italic(blockquoteMatch[2]);
352
+ }
353
+ // Horizontal rule: --- or *** or ___
354
+ if (line.match(/^([-*_])\1{2,}\s*$/)) {
355
+ return muted(line);
356
+ }
357
+ // Unordered list: - or * or +
358
+ const ulMatch = line.match(/^(\s*)([-*+])\s+(.*)$/);
359
+ if (ulMatch) {
360
+ const [, indent, bullet, content] = ulMatch;
361
+ return indent + secondary(bullet) + ' ' + highlightInlineMarkdown(content, theme);
362
+ }
363
+ // Ordered list: 1. 2. etc.
364
+ const olMatch = line.match(/^(\s*)(\d+\.)\s+(.*)$/);
365
+ if (olMatch) {
366
+ const [, indent, num, content] = olMatch;
367
+ return indent + secondary(num) + ' ' + highlightInlineMarkdown(content, theme);
368
+ }
369
+ // Regular line - apply inline formatting
370
+ return highlightInlineMarkdown(line, theme);
371
+ }
372
+ /**
373
+ * Apply inline markdown highlighting (bold, italic, code, links).
374
+ */
375
+ function highlightInlineMarkdown(text, theme) {
376
+ const primary = chalk.hex(theme.colors.primary);
377
+ const secondary = chalk.hex(theme.colors.secondary || theme.colors.primary);
378
+ const muted = chalk.hex(theme.colors.muted);
379
+ const fg = chalk.hex(theme.colors.foreground);
380
+ let result = text;
381
+ // Images first (before links): ![alt](url)
382
+ result = result.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, (_match, alt, url) => {
383
+ return muted('![') + fg(alt) + muted('](') + secondary(url) + muted(')');
384
+ });
385
+ // Links: [text](url)
386
+ result = result.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_match, linkText, url) => {
387
+ return muted('[') + primary(linkText) + muted('](') + secondary(url) + muted(')');
388
+ });
389
+ // Inline code: `code` (must be before bold/italic to avoid conflicts)
390
+ result = result.replace(/`([^`]+)`/g, (_match, code) => {
391
+ return muted('`') + secondary(code) + muted('`');
392
+ });
393
+ // Bold: **text** or __text__
394
+ result = result.replace(/(\*\*|__)(.+?)\1/g, (_match, marker, content) => {
395
+ return muted(marker) + fg.bold(content) + muted(marker);
396
+ });
397
+ // Italic: *text* or _text_
398
+ result = result.replace(/(\*|_)(.+?)\1/g, (_match, marker, content) => {
399
+ return muted(marker) + fg.italic(content) + muted(marker);
400
+ });
401
+ return result;
402
+ }
403
+ // =============================================================================
404
+ // Key Detection Helpers
405
+ // =============================================================================
406
+ function isNavigateUp(data) {
407
+ const str = data.toString();
408
+ return str === '\x1b[A' || str === 'k';
409
+ }
410
+ function isNavigateDown(data) {
411
+ const str = data.toString();
412
+ return str === '\x1b[B' || str === 'j';
413
+ }
414
+ function isArrowUp(data) {
415
+ return data.toString() === '\x1b[A';
416
+ }
417
+ function isArrowDown(data) {
418
+ return data.toString() === '\x1b[B';
419
+ }
420
+ function isArrowLeft(data) {
421
+ return data.toString() === '\x1b[D';
422
+ }
423
+ function isArrowRight(data) {
424
+ return data.toString() === '\x1b[C';
425
+ }
426
+ function isHome(data) {
427
+ const str = data.toString();
428
+ return str === '\x1b[H' || str === '\x1b[1~';
429
+ }
430
+ function isEnd(data) {
431
+ const str = data.toString();
432
+ return str === '\x1b[F' || str === '\x1b[4~';
433
+ }
434
+ function isPageUp(data) {
435
+ return data.toString() === '\x1b[5~';
436
+ }
437
+ function isPageDown(data) {
438
+ return data.toString() === '\x1b[6~';
439
+ }
440
+ function isSpace(data) {
441
+ return data.length === 1 && data[0] === 0x20;
442
+ }
443
+ function isBackspace(data) {
444
+ return data.length === 1 && (data[0] === 0x7f || data[0] === 0x08);
445
+ }
446
+ function isDelete(data) {
447
+ return data.toString() === '\x1b[3~';
448
+ }
449
+ function isEnter(data) {
450
+ return data.length === 1 && (data[0] === 0x0d || data[0] === 0x0a);
451
+ }
452
+ function isTab(data) {
453
+ return data.length === 1 && data[0] === 0x09;
454
+ }
455
+ function isCtrlS(data) {
456
+ return data.length === 1 && data[0] === 0x13;
457
+ }
458
+ function isEscape(data) {
459
+ return data.length === 1 && data[0] === 0x1b;
460
+ }
461
+ function isEditKey(data) {
462
+ const char = data.length === 1 && data[0] >= 0x20 && data[0] < 0x7f
463
+ ? String.fromCharCode(data[0])
464
+ : null;
465
+ return char === 'e' || char === 'i';
466
+ }
467
+ function isPrintableChar(data) {
468
+ if (data.length === 1 && data[0] >= 0x20 && data[0] < 0x7f) {
469
+ return String.fromCharCode(data[0]);
470
+ }
471
+ return null;
472
+ }
473
+ // =============================================================================
474
+ // Document Detail Overlay V2
475
+ // =============================================================================
476
+ /**
477
+ * Fullscreen document viewer and editor overlay.
478
+ * Shows a single document with vim-style scrolling navigation.
479
+ * Supports edit mode for modifying raw markdown content.
480
+ */
481
+ export class DocumentDetailOverlayV2 extends BaseOverlayV2 {
482
+ document;
483
+ // Use 'inline' type so terminal-ui doesn't clear screen - we manage alternate screen ourselves
484
+ type = 'inline';
485
+ id = 'document-detail-overlay-v2';
486
+ // Tell terminal-ui we manage our own screen buffer - skip clearing on close
487
+ usesAlternateScreen = true;
488
+ constructor(document) {
489
+ // Initialize state with rendered markdown
490
+ const termWidth = terminal.getTerminalWidth();
491
+ const contentLines = renderMarkdownSync(document.content, termWidth);
492
+ const rawLines = document.content.split('\n');
493
+ super({
494
+ contentLines,
495
+ scrollOffset: 0,
496
+ isEditMode: false,
497
+ rawLines,
498
+ cursorLine: 0,
499
+ cursorColumn: 0,
500
+ editScrollOffset: 0,
501
+ isDirty: false,
502
+ originalContent: document.content,
503
+ showSavePrompt: false,
504
+ saveMessage: null,
505
+ });
506
+ this.document = document;
507
+ }
508
+ /**
509
+ * Enter alternate screen buffer when overlay mounts.
510
+ * This preserves the main screen content.
511
+ */
512
+ onMount() {
513
+ enterAlternateScreen();
514
+ }
515
+ /**
516
+ * Exit alternate screen buffer when overlay unmounts.
517
+ * This restores the main screen content.
518
+ */
519
+ onUnmount() {
520
+ exitAlternateScreen();
521
+ }
522
+ renderContent(context) {
523
+ if (this.state.isEditMode) {
524
+ return this.renderEditMode(context);
525
+ }
526
+ return this.renderViewMode(context);
527
+ }
528
+ // ===========================================================================
529
+ // View Mode Rendering
530
+ // ===========================================================================
531
+ renderViewMode(context) {
532
+ const s = context.styles;
533
+ const cols = context.width;
534
+ const rows = context.height;
535
+ const border = renderBorder(cols, s);
536
+ const lines = [];
537
+ // Header
538
+ const typeLabel = DOC_TYPE_LABELS[this.document.docType] || this.document.docType.toUpperCase();
539
+ lines.push(border);
540
+ lines.push(` ${s.primaryBold(typeLabel)}${s.muted(' │ ')}${chalk.bold(this.document.title)}`);
541
+ lines.push(border);
542
+ // Calculate content area
543
+ const headerLines = 3;
544
+ const footerLines = 3;
545
+ const contentHeight = rows - headerLines - footerLines;
546
+ // Get visible lines
547
+ const totalLines = this.state.contentLines.length;
548
+ const visibleLines = [];
549
+ let physicalLinesUsed = 0;
550
+ for (let i = this.state.scrollOffset; i < totalLines && physicalLinesUsed < contentHeight; i++) {
551
+ const line = this.state.contentLines[i];
552
+ // eslint-disable-next-line no-control-regex
553
+ const visualLen = line.replace(/\x1b\[[0-9;]*m/g, '').length;
554
+ const linePhysicalHeight = Math.max(1, Math.ceil(visualLen / cols));
555
+ if (physicalLinesUsed + linePhysicalHeight <= contentHeight) {
556
+ visibleLines.push(line);
557
+ physicalLinesUsed += linePhysicalHeight;
558
+ }
559
+ else {
560
+ break;
561
+ }
562
+ }
563
+ // Content
564
+ for (const line of visibleLines) {
565
+ lines.push(line);
566
+ }
567
+ // Pad remaining space
568
+ for (let i = physicalLinesUsed; i < contentHeight; i++) {
569
+ lines.push('');
570
+ }
571
+ // Footer with position info
572
+ const canScrollUp = this.state.scrollOffset > 0;
573
+ const canScrollDown = this.state.scrollOffset + contentHeight < totalLines;
574
+ const arrows = (canScrollUp ? '▲' : ' ') + (canScrollDown ? '▼' : ' ');
575
+ const position = totalLines > 0
576
+ ? `Line ${String(this.state.scrollOffset + 1)}-${String(Math.min(this.state.scrollOffset + contentHeight, totalLines))} of ${String(totalLines)}`
577
+ : '';
578
+ const scrollPercent = totalLines > contentHeight
579
+ ? Math.round(((this.state.scrollOffset + contentHeight) / totalLines) * 100)
580
+ : 100;
581
+ lines.push(border);
582
+ lines.push(s.muted(` ${arrows} ${position} (${String(scrollPercent)}%) ↑↓/jk Scroll · e/i Edit · q/Esc Back`));
583
+ lines.push(border);
584
+ return lines;
585
+ }
586
+ // ===========================================================================
587
+ // Edit Mode Rendering
588
+ // ===========================================================================
589
+ renderEditMode(context) {
590
+ const s = context.styles;
591
+ const cols = context.width;
592
+ const rows = context.height;
593
+ const border = renderBorder(cols, s);
594
+ const lines = [];
595
+ // Header with [EDIT] indicator
596
+ const typeLabel = DOC_TYPE_LABELS[this.document.docType] || this.document.docType.toUpperCase();
597
+ const editIndicator = this.state.isDirty ? s.warning('[EDIT*]') : s.primary('[EDIT]');
598
+ lines.push(border);
599
+ lines.push(` ${s.primaryBold(typeLabel)}${s.muted(' │ ')}${chalk.bold(this.document.title)} ${editIndicator}`);
600
+ lines.push(border);
601
+ // Calculate content area
602
+ const headerLines = 3;
603
+ const footerLines = 3;
604
+ const contentHeight = rows - headerLines - footerLines;
605
+ // Ensure cursor is visible (adjust scroll if needed)
606
+ this.ensureCursorVisible(contentHeight);
607
+ // Get visible lines with line numbers
608
+ const totalLines = this.state.rawLines.length;
609
+ const lineNumWidth = String(totalLines).length + 1;
610
+ // Get current theme for syntax highlighting
611
+ const theme = getCurrentTheme();
612
+ // Pre-compute code block state for all lines up to and including visible area
613
+ // This ensures we know if we're inside a code block when rendering
614
+ let codeBlockState = { inCodeBlock: false, language: '', fenceChar: '', fenceLength: 0 };
615
+ const lineStates = [];
616
+ for (let i = 0; i < totalLines; i++) {
617
+ const line = this.state.rawLines[i];
618
+ // Check for code fence (we only need to track state, not highlight)
619
+ const fenceMatch = line.match(/^(`{3,}|~{3,})(\w*)\s*$/);
620
+ if (fenceMatch) {
621
+ const [, fence, lang] = fenceMatch;
622
+ const fenceChar = fence[0];
623
+ const fenceLength = fence.length;
624
+ if (!codeBlockState.inCodeBlock) {
625
+ codeBlockState = { inCodeBlock: true, language: lang || 'plaintext', fenceChar, fenceLength };
626
+ }
627
+ else if (fenceChar === codeBlockState.fenceChar && fenceLength >= codeBlockState.fenceLength) {
628
+ codeBlockState = { inCodeBlock: false, language: '', fenceChar: '', fenceLength: 0 };
629
+ }
630
+ }
631
+ lineStates[i] = { ...codeBlockState };
632
+ }
633
+ for (let i = 0; i < contentHeight; i++) {
634
+ const lineIndex = this.state.editScrollOffset + i;
635
+ if (lineIndex < totalLines) {
636
+ const lineNum = String(lineIndex + 1).padStart(lineNumWidth);
637
+ const lineContent = this.state.rawLines[lineIndex];
638
+ const isCursorLine = lineIndex === this.state.cursorLine;
639
+ // Render line with cursor if this is the cursor line (no highlighting for accurate positioning)
640
+ if (isCursorLine) {
641
+ const renderedLine = this.renderLineWithCursor(lineContent, this.state.cursorColumn, cols - lineNumWidth - 2);
642
+ lines.push(s.muted(`${lineNum}│`) + renderedLine);
643
+ }
644
+ else {
645
+ // Apply syntax highlighting to non-cursor lines
646
+ const maxLen = cols - lineNumWidth - 2;
647
+ const truncated = lineContent.length > maxLen;
648
+ const displayContent = truncated
649
+ ? lineContent.slice(0, maxLen - 1)
650
+ : lineContent;
651
+ // Get the state BEFORE this line to know if we're inside a code block
652
+ const prevState = lineIndex > 0 ? lineStates[lineIndex - 1] : { inCodeBlock: false, language: '', fenceChar: '', fenceLength: 0 };
653
+ const { highlighted } = highlightEditLine(displayContent, prevState, theme);
654
+ lines.push(s.muted(`${lineNum}│`) + highlighted + (truncated ? s.muted('…') : ''));
655
+ }
656
+ }
657
+ else {
658
+ // Empty line beyond document
659
+ const lineNum = ' '.repeat(lineNumWidth);
660
+ lines.push(s.muted(`${lineNum}│`) + s.muted('~'));
661
+ }
662
+ }
663
+ // Footer
664
+ lines.push(border);
665
+ if (this.state.showSavePrompt) {
666
+ // Save prompt
667
+ lines.push(s.warning(' Unsaved changes. Save? ') + s.primary('(y)') + 'es / ' + s.primary('(n)') + 'o / ' + s.primary('(c)') + 'ancel');
668
+ }
669
+ else if (this.state.saveMessage) {
670
+ // Brief save message
671
+ lines.push(s.success(` ${this.state.saveMessage}`));
672
+ }
673
+ else {
674
+ // Normal edit mode footer
675
+ const cursorPos = `Ln ${String(this.state.cursorLine + 1)}, Col ${String(this.state.cursorColumn + 1)}`;
676
+ const modifiedIndicator = this.state.isDirty ? s.warning('[modified]') : '';
677
+ lines.push(s.primaryBold(' ── INSERT ── ') + s.muted(cursorPos) + ' ' + modifiedIndicator + s.muted(' Ctrl+S Save · Esc Exit'));
678
+ }
679
+ lines.push(border);
680
+ return lines;
681
+ }
682
+ renderLineWithCursor(line, cursorCol, maxLen) {
683
+ const theme = getCurrentTheme();
684
+ const cursorStyle = chalk.inverse;
685
+ // Handle cursor beyond line end
686
+ const effectiveCursorCol = Math.min(cursorCol, line.length);
687
+ // Calculate visible portion
688
+ let viewStart = 0;
689
+ if (effectiveCursorCol >= maxLen - 5) {
690
+ viewStart = effectiveCursorCol - maxLen + 10;
691
+ }
692
+ viewStart = Math.max(0, viewStart);
693
+ const viewEnd = viewStart + maxLen;
694
+ const visibleLine = line.slice(viewStart, viewEnd);
695
+ const cursorPosInView = effectiveCursorCol - viewStart;
696
+ // Build the line with cursor
697
+ let result = '';
698
+ for (let i = 0; i < visibleLine.length; i++) {
699
+ if (i === cursorPosInView) {
700
+ result += cursorStyle(visibleLine[i]);
701
+ }
702
+ else {
703
+ result += visibleLine[i];
704
+ }
705
+ }
706
+ // If cursor is at end of line (or beyond), show cursor as block
707
+ if (cursorPosInView >= visibleLine.length) {
708
+ result += cursorStyle(' ');
709
+ }
710
+ // Add truncation indicator
711
+ if (viewStart > 0) {
712
+ result = chalk.hex(theme.colors.muted)('…') + result.slice(1);
713
+ }
714
+ if (line.length > viewEnd) {
715
+ result = result.slice(0, -1) + chalk.hex(theme.colors.muted)('…');
716
+ }
717
+ return result;
718
+ }
719
+ ensureCursorVisible(contentHeight) {
720
+ // Scroll up if cursor is above visible area
721
+ if (this.state.cursorLine < this.state.editScrollOffset) {
722
+ this.state.editScrollOffset = this.state.cursorLine;
723
+ }
724
+ // Scroll down if cursor is below visible area
725
+ if (this.state.cursorLine >= this.state.editScrollOffset + contentHeight) {
726
+ this.state.editScrollOffset = this.state.cursorLine - contentHeight + 1;
727
+ }
728
+ }
729
+ // ===========================================================================
730
+ // Key Handling
731
+ // ===========================================================================
732
+ handleKey(key) {
733
+ const data = key.raw;
734
+ // Handle save prompt state
735
+ if (this.state.showSavePrompt) {
736
+ return this.handleSavePromptKey(data);
737
+ }
738
+ // Handle edit mode
739
+ if (this.state.isEditMode) {
740
+ return this.handleEditModeKey(data);
741
+ }
742
+ // Handle view mode
743
+ return this.handleViewModeKey(data);
744
+ }
745
+ // ===========================================================================
746
+ // View Mode Key Handling
747
+ // ===========================================================================
748
+ handleViewModeKey(data) {
749
+ const char = isPrintableChar(data);
750
+ const keyStr = data.toString();
751
+ const rows = terminal.getTerminalHeight();
752
+ const contentHeight = rows - 6;
753
+ const totalLines = this.state.contentLines.length;
754
+ const maxScroll = Math.max(0, totalLines - contentHeight);
755
+ // Ctrl+C closes everything (no going back)
756
+ if (isCtrlC(data)) {
757
+ return this.close({ goBack: false });
758
+ }
759
+ // Escape or q - go back to list
760
+ if (isClose(data)) {
761
+ return this.close({ goBack: true });
762
+ }
763
+ // e or i - enter edit mode
764
+ if (isEditKey(data)) {
765
+ this.state.isEditMode = true;
766
+ this.state.cursorLine = 0;
767
+ this.state.cursorColumn = 0;
768
+ this.state.editScrollOffset = 0;
769
+ this.state.saveMessage = null;
770
+ return this.rerender();
771
+ }
772
+ // Up / k
773
+ if (isNavigateUp(data)) {
774
+ if (this.state.scrollOffset > 0) {
775
+ this.state.scrollOffset--;
776
+ return this.rerender();
777
+ }
778
+ return this.noAction();
779
+ }
780
+ // Down / j
781
+ if (isNavigateDown(data)) {
782
+ if (this.state.scrollOffset < maxScroll) {
783
+ this.state.scrollOffset++;
784
+ return this.rerender();
785
+ }
786
+ return this.noAction();
787
+ }
788
+ // Page Up
789
+ if (keyStr === '\x1b[5~') {
790
+ this.state.scrollOffset = Math.max(0, this.state.scrollOffset - contentHeight);
791
+ return this.rerender();
792
+ }
793
+ // Page Down / Space
794
+ if (keyStr === '\x1b[6~' || isSpace(data)) {
795
+ this.state.scrollOffset = Math.min(maxScroll, this.state.scrollOffset + contentHeight);
796
+ return this.rerender();
797
+ }
798
+ // g = top
799
+ if (char === 'g') {
800
+ this.state.scrollOffset = 0;
801
+ return this.rerender();
802
+ }
803
+ // G = bottom
804
+ if (char === 'G') {
805
+ this.state.scrollOffset = maxScroll;
806
+ return this.rerender();
807
+ }
808
+ // Home
809
+ if (keyStr === '\x1b[H') {
810
+ this.state.scrollOffset = 0;
811
+ return this.rerender();
812
+ }
813
+ // End
814
+ if (keyStr === '\x1b[F') {
815
+ this.state.scrollOffset = maxScroll;
816
+ return this.rerender();
817
+ }
818
+ return this.noAction();
819
+ }
820
+ // ===========================================================================
821
+ // Edit Mode Key Handling
822
+ // ===========================================================================
823
+ handleEditModeKey(data) {
824
+ // Clear save message on any key
825
+ if (this.state.saveMessage) {
826
+ this.state.saveMessage = null;
827
+ }
828
+ // Ctrl+C - exit immediately without saving
829
+ if (isCtrlC(data)) {
830
+ return this.close({ goBack: false });
831
+ }
832
+ // Ctrl+S - save
833
+ if (isCtrlS(data)) {
834
+ this.saveDocument();
835
+ return this.rerender();
836
+ }
837
+ // Escape - exit edit mode (prompt if dirty)
838
+ if (isEscape(data)) {
839
+ if (this.state.isDirty) {
840
+ this.state.showSavePrompt = true;
841
+ return this.rerender();
842
+ }
843
+ // Exit edit mode, re-render markdown
844
+ return this.exitEditMode();
845
+ }
846
+ // Navigation
847
+ if (isArrowUp(data)) {
848
+ return this.moveCursorUp();
849
+ }
850
+ if (isArrowDown(data)) {
851
+ return this.moveCursorDown();
852
+ }
853
+ if (isArrowLeft(data)) {
854
+ return this.moveCursorLeft();
855
+ }
856
+ if (isArrowRight(data)) {
857
+ return this.moveCursorRight();
858
+ }
859
+ if (isHome(data)) {
860
+ this.state.cursorColumn = 0;
861
+ return this.rerender();
862
+ }
863
+ if (isEnd(data)) {
864
+ this.state.cursorColumn = this.getCurrentLine().length;
865
+ return this.rerender();
866
+ }
867
+ if (isPageUp(data)) {
868
+ const rows = terminal.getTerminalHeight();
869
+ const pageSize = rows - 6;
870
+ this.state.cursorLine = Math.max(0, this.state.cursorLine - pageSize);
871
+ this.clampCursorColumn();
872
+ return this.rerender();
873
+ }
874
+ if (isPageDown(data)) {
875
+ const rows = terminal.getTerminalHeight();
876
+ const pageSize = rows - 6;
877
+ this.state.cursorLine = Math.min(this.state.rawLines.length - 1, this.state.cursorLine + pageSize);
878
+ this.clampCursorColumn();
879
+ return this.rerender();
880
+ }
881
+ // Editing
882
+ if (isBackspace(data)) {
883
+ return this.handleBackspace();
884
+ }
885
+ if (isDelete(data)) {
886
+ return this.handleDelete();
887
+ }
888
+ if (isEnter(data)) {
889
+ return this.handleEnter();
890
+ }
891
+ if (isTab(data)) {
892
+ return this.insertText(' '.repeat(TAB_SIZE));
893
+ }
894
+ // Printable characters
895
+ const char = isPrintableChar(data);
896
+ if (char) {
897
+ return this.insertText(char);
898
+ }
899
+ return this.noAction();
900
+ }
901
+ // ===========================================================================
902
+ // Save Prompt Key Handling
903
+ // ===========================================================================
904
+ handleSavePromptKey(data) {
905
+ const char = isPrintableChar(data);
906
+ // y = save and exit edit mode
907
+ if (char === 'y' || char === 'Y') {
908
+ this.saveDocument();
909
+ this.state.showSavePrompt = false;
910
+ return this.exitEditMode();
911
+ }
912
+ // n = discard and exit edit mode
913
+ if (char === 'n' || char === 'N') {
914
+ // Revert to original content
915
+ this.state.rawLines = this.state.originalContent.split('\n');
916
+ this.state.isDirty = false;
917
+ this.state.showSavePrompt = false;
918
+ return this.exitEditMode();
919
+ }
920
+ // c or Escape = cancel, stay in edit mode
921
+ if (char === 'c' || char === 'C' || isEscape(data)) {
922
+ this.state.showSavePrompt = false;
923
+ return this.rerender();
924
+ }
925
+ // Ctrl+C = exit everything
926
+ if (isCtrlC(data)) {
927
+ return this.close({ goBack: false });
928
+ }
929
+ return this.noAction();
930
+ }
931
+ // ===========================================================================
932
+ // Cursor Movement
933
+ // ===========================================================================
934
+ moveCursorUp() {
935
+ if (this.state.cursorLine > 0) {
936
+ this.state.cursorLine--;
937
+ this.clampCursorColumn();
938
+ }
939
+ return this.rerender();
940
+ }
941
+ moveCursorDown() {
942
+ if (this.state.cursorLine < this.state.rawLines.length - 1) {
943
+ this.state.cursorLine++;
944
+ this.clampCursorColumn();
945
+ }
946
+ return this.rerender();
947
+ }
948
+ moveCursorLeft() {
949
+ if (this.state.cursorColumn > 0) {
950
+ this.state.cursorColumn--;
951
+ }
952
+ else if (this.state.cursorLine > 0) {
953
+ // Wrap to end of previous line
954
+ this.state.cursorLine--;
955
+ this.state.cursorColumn = this.getCurrentLine().length;
956
+ }
957
+ return this.rerender();
958
+ }
959
+ moveCursorRight() {
960
+ const lineLen = this.getCurrentLine().length;
961
+ if (this.state.cursorColumn < lineLen) {
962
+ this.state.cursorColumn++;
963
+ }
964
+ else if (this.state.cursorLine < this.state.rawLines.length - 1) {
965
+ // Wrap to start of next line
966
+ this.state.cursorLine++;
967
+ this.state.cursorColumn = 0;
968
+ }
969
+ return this.rerender();
970
+ }
971
+ clampCursorColumn() {
972
+ const lineLen = this.getCurrentLine().length;
973
+ this.state.cursorColumn = Math.min(this.state.cursorColumn, lineLen);
974
+ }
975
+ getCurrentLine() {
976
+ return this.state.rawLines[this.state.cursorLine] || '';
977
+ }
978
+ // ===========================================================================
979
+ // Text Editing
980
+ // ===========================================================================
981
+ insertText(text) {
982
+ const line = this.getCurrentLine();
983
+ const before = line.slice(0, this.state.cursorColumn);
984
+ const after = line.slice(this.state.cursorColumn);
985
+ this.state.rawLines[this.state.cursorLine] = before + text + after;
986
+ this.state.cursorColumn += text.length;
987
+ this.state.isDirty = true;
988
+ return this.rerender();
989
+ }
990
+ handleBackspace() {
991
+ if (this.state.cursorColumn > 0) {
992
+ // Delete character before cursor
993
+ const line = this.getCurrentLine();
994
+ const before = line.slice(0, this.state.cursorColumn - 1);
995
+ const after = line.slice(this.state.cursorColumn);
996
+ this.state.rawLines[this.state.cursorLine] = before + after;
997
+ this.state.cursorColumn--;
998
+ this.state.isDirty = true;
999
+ }
1000
+ else if (this.state.cursorLine > 0) {
1001
+ // Join with previous line
1002
+ const currentLine = this.getCurrentLine();
1003
+ const prevLine = this.state.rawLines[this.state.cursorLine - 1];
1004
+ this.state.rawLines[this.state.cursorLine - 1] = prevLine + currentLine;
1005
+ this.state.rawLines.splice(this.state.cursorLine, 1);
1006
+ this.state.cursorLine--;
1007
+ this.state.cursorColumn = prevLine.length;
1008
+ this.state.isDirty = true;
1009
+ }
1010
+ return this.rerender();
1011
+ }
1012
+ handleDelete() {
1013
+ const line = this.getCurrentLine();
1014
+ if (this.state.cursorColumn < line.length) {
1015
+ // Delete character at cursor
1016
+ const before = line.slice(0, this.state.cursorColumn);
1017
+ const after = line.slice(this.state.cursorColumn + 1);
1018
+ this.state.rawLines[this.state.cursorLine] = before + after;
1019
+ this.state.isDirty = true;
1020
+ }
1021
+ else if (this.state.cursorLine < this.state.rawLines.length - 1) {
1022
+ // Join with next line
1023
+ const nextLine = this.state.rawLines[this.state.cursorLine + 1];
1024
+ this.state.rawLines[this.state.cursorLine] = line + nextLine;
1025
+ this.state.rawLines.splice(this.state.cursorLine + 1, 1);
1026
+ this.state.isDirty = true;
1027
+ }
1028
+ return this.rerender();
1029
+ }
1030
+ handleEnter() {
1031
+ const line = this.getCurrentLine();
1032
+ const before = line.slice(0, this.state.cursorColumn);
1033
+ const after = line.slice(this.state.cursorColumn);
1034
+ this.state.rawLines[this.state.cursorLine] = before;
1035
+ this.state.rawLines.splice(this.state.cursorLine + 1, 0, after);
1036
+ this.state.cursorLine++;
1037
+ this.state.cursorColumn = 0;
1038
+ this.state.isDirty = true;
1039
+ return this.rerender();
1040
+ }
1041
+ // ===========================================================================
1042
+ // Save and Exit
1043
+ // ===========================================================================
1044
+ saveDocument() {
1045
+ const content = this.state.rawLines.join('\n');
1046
+ try {
1047
+ documentRepository.update(this.document.id, { content });
1048
+ this.state.originalContent = content;
1049
+ this.state.isDirty = false;
1050
+ this.state.saveMessage = 'Saved!';
1051
+ // Clear message after a delay (will be cleared on next keypress anyway)
1052
+ setTimeout(() => {
1053
+ if (this.state.saveMessage === 'Saved!') {
1054
+ this.state.saveMessage = null;
1055
+ }
1056
+ }, 2000);
1057
+ }
1058
+ catch (error) {
1059
+ this.state.saveMessage = `Error: ${error.message}`;
1060
+ }
1061
+ }
1062
+ exitEditMode() {
1063
+ this.state.isEditMode = false;
1064
+ // Re-render markdown from current content
1065
+ const content = this.state.rawLines.join('\n');
1066
+ const termWidth = terminal.getTerminalWidth();
1067
+ this.state.contentLines = renderMarkdownSync(content, termWidth);
1068
+ this.state.scrollOffset = 0;
1069
+ return this.rerender();
1070
+ }
1071
+ }