nastech-tui 0.0.1

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 (424) hide show
  1. package/.prettierrc +11 -0
  2. package/README.md +346 -0
  3. package/eslint.config.mjs +111 -0
  4. package/package.json +51 -0
  5. package/packages/nastech-ink/ambient.d.ts +83 -0
  6. package/packages/nastech-ink/index.d.ts +40 -0
  7. package/packages/nastech-ink/index.js +1 -0
  8. package/packages/nastech-ink/nastech-ink/ambient.d.ts +83 -0
  9. package/packages/nastech-ink/nastech-ink/index.d.ts +40 -0
  10. package/packages/nastech-ink/nastech-ink/index.js +1 -0
  11. package/packages/nastech-ink/nastech-ink/package.json +54 -0
  12. package/packages/nastech-ink/nastech-ink/src/bootstrap/state.ts +9 -0
  13. package/packages/nastech-ink/nastech-ink/src/entry-exports.ts +32 -0
  14. package/packages/nastech-ink/nastech-ink/src/hooks/use-stderr.ts +15 -0
  15. package/packages/nastech-ink/nastech-ink/src/hooks/use-stdout.ts +15 -0
  16. package/packages/nastech-ink/nastech-ink/src/ink/Ansi.tsx +435 -0
  17. package/packages/nastech-ink/nastech-ink/src/ink/app-mouse.test.ts +123 -0
  18. package/packages/nastech-ink/nastech-ink/src/ink/bidi.ts +145 -0
  19. package/packages/nastech-ink/nastech-ink/src/ink/cache-eviction.ts +45 -0
  20. package/packages/nastech-ink/nastech-ink/src/ink/clearTerminal.ts +68 -0
  21. package/packages/nastech-ink/nastech-ink/src/ink/colorize.test.ts +60 -0
  22. package/packages/nastech-ink/nastech-ink/src/ink/colorize.ts +277 -0
  23. package/packages/nastech-ink/nastech-ink/src/ink/components/AlternateScreen.tsx +133 -0
  24. package/packages/nastech-ink/nastech-ink/src/ink/components/App.tsx +830 -0
  25. package/packages/nastech-ink/nastech-ink/src/ink/components/AppContext.ts +20 -0
  26. package/packages/nastech-ink/nastech-ink/src/ink/components/Box.tsx +294 -0
  27. package/packages/nastech-ink/nastech-ink/src/ink/components/Button.tsx +236 -0
  28. package/packages/nastech-ink/nastech-ink/src/ink/components/ClockContext.tsx +133 -0
  29. package/packages/nastech-ink/nastech-ink/src/ink/components/CursorAdvanceContext.ts +35 -0
  30. package/packages/nastech-ink/nastech-ink/src/ink/components/CursorDeclarationContext.ts +28 -0
  31. package/packages/nastech-ink/nastech-ink/src/ink/components/ErrorOverview.tsx +130 -0
  32. package/packages/nastech-ink/nastech-ink/src/ink/components/Link.tsx +38 -0
  33. package/packages/nastech-ink/nastech-ink/src/ink/components/Newline.tsx +43 -0
  34. package/packages/nastech-ink/nastech-ink/src/ink/components/NoSelect.tsx +73 -0
  35. package/packages/nastech-ink/nastech-ink/src/ink/components/RawAnsi.tsx +61 -0
  36. package/packages/nastech-ink/nastech-ink/src/ink/components/ScrollBox.tsx +290 -0
  37. package/packages/nastech-ink/nastech-ink/src/ink/components/Spacer.tsx +23 -0
  38. package/packages/nastech-ink/nastech-ink/src/ink/components/StdinContext.ts +25 -0
  39. package/packages/nastech-ink/nastech-ink/src/ink/components/TerminalFocusContext.tsx +63 -0
  40. package/packages/nastech-ink/nastech-ink/src/ink/components/TerminalSizeContext.tsx +7 -0
  41. package/packages/nastech-ink/nastech-ink/src/ink/components/Text.test.ts +38 -0
  42. package/packages/nastech-ink/nastech-ink/src/ink/components/Text.tsx +336 -0
  43. package/packages/nastech-ink/nastech-ink/src/ink/constants.ts +6 -0
  44. package/packages/nastech-ink/nastech-ink/src/ink/cursor.ts +5 -0
  45. package/packages/nastech-ink/nastech-ink/src/ink/devtools.ts +2 -0
  46. package/packages/nastech-ink/nastech-ink/src/ink/dom.ts +495 -0
  47. package/packages/nastech-ink/nastech-ink/src/ink/events/click-event.ts +38 -0
  48. package/packages/nastech-ink/nastech-ink/src/ink/events/cmd-shortcuts.test.ts +65 -0
  49. package/packages/nastech-ink/nastech-ink/src/ink/events/dispatcher.ts +242 -0
  50. package/packages/nastech-ink/nastech-ink/src/ink/events/emitter.ts +40 -0
  51. package/packages/nastech-ink/nastech-ink/src/ink/events/event-handlers.ts +84 -0
  52. package/packages/nastech-ink/nastech-ink/src/ink/events/event.ts +11 -0
  53. package/packages/nastech-ink/nastech-ink/src/ink/events/focus-event.ts +18 -0
  54. package/packages/nastech-ink/nastech-ink/src/ink/events/input-event.ts +176 -0
  55. package/packages/nastech-ink/nastech-ink/src/ink/events/keyboard-event.ts +57 -0
  56. package/packages/nastech-ink/nastech-ink/src/ink/events/mouse-event.ts +18 -0
  57. package/packages/nastech-ink/nastech-ink/src/ink/events/paste-event.ts +10 -0
  58. package/packages/nastech-ink/nastech-ink/src/ink/events/resize-event.ts +12 -0
  59. package/packages/nastech-ink/nastech-ink/src/ink/events/terminal-event.ts +107 -0
  60. package/packages/nastech-ink/nastech-ink/src/ink/events/terminal-focus-event.ts +19 -0
  61. package/packages/nastech-ink/nastech-ink/src/ink/focus.ts +219 -0
  62. package/packages/nastech-ink/nastech-ink/src/ink/frame.ts +124 -0
  63. package/packages/nastech-ink/nastech-ink/src/ink/get-max-width.ts +27 -0
  64. package/packages/nastech-ink/nastech-ink/src/ink/global.d.ts +1 -0
  65. package/packages/nastech-ink/nastech-ink/src/ink/hit-test.test.ts +38 -0
  66. package/packages/nastech-ink/nastech-ink/src/ink/hit-test.ts +224 -0
  67. package/packages/nastech-ink/nastech-ink/src/ink/hooks/use-animation-frame.ts +62 -0
  68. package/packages/nastech-ink/nastech-ink/src/ink/hooks/use-app.ts +9 -0
  69. package/packages/nastech-ink/nastech-ink/src/ink/hooks/use-cursor-advance.ts +33 -0
  70. package/packages/nastech-ink/nastech-ink/src/ink/hooks/use-declared-cursor.ts +75 -0
  71. package/packages/nastech-ink/nastech-ink/src/ink/hooks/use-external-process.ts +27 -0
  72. package/packages/nastech-ink/nastech-ink/src/ink/hooks/use-input.ts +95 -0
  73. package/packages/nastech-ink/nastech-ink/src/ink/hooks/use-interval.ts +71 -0
  74. package/packages/nastech-ink/package.json +57 -0
  75. package/packages/nastech-ink/src/bootstrap/state.ts +9 -0
  76. package/packages/nastech-ink/src/entry-exports.ts +32 -0
  77. package/packages/nastech-ink/src/hooks/use-stderr.ts +15 -0
  78. package/packages/nastech-ink/src/hooks/use-stdout.ts +15 -0
  79. package/packages/nastech-ink/src/ink/Ansi.tsx +435 -0
  80. package/packages/nastech-ink/src/ink/app-mouse.test.ts +123 -0
  81. package/packages/nastech-ink/src/ink/app-rawmode-mouse.test.ts +91 -0
  82. package/packages/nastech-ink/src/ink/bidi.ts +145 -0
  83. package/packages/nastech-ink/src/ink/cache-eviction.ts +45 -0
  84. package/packages/nastech-ink/src/ink/clearTerminal.ts +68 -0
  85. package/packages/nastech-ink/src/ink/colorize.test.ts +60 -0
  86. package/packages/nastech-ink/src/ink/colorize.ts +277 -0
  87. package/packages/nastech-ink/src/ink/components/AlternateScreen.tsx +133 -0
  88. package/packages/nastech-ink/src/ink/components/App.tsx +855 -0
  89. package/packages/nastech-ink/src/ink/components/AppContext.ts +20 -0
  90. package/packages/nastech-ink/src/ink/components/Box.tsx +294 -0
  91. package/packages/nastech-ink/src/ink/components/Button.tsx +236 -0
  92. package/packages/nastech-ink/src/ink/components/ClockContext.tsx +133 -0
  93. package/packages/nastech-ink/src/ink/components/CursorAdvanceContext.ts +35 -0
  94. package/packages/nastech-ink/src/ink/components/CursorDeclarationContext.ts +28 -0
  95. package/packages/nastech-ink/src/ink/components/ErrorOverview.tsx +130 -0
  96. package/packages/nastech-ink/src/ink/components/Link.tsx +38 -0
  97. package/packages/nastech-ink/src/ink/components/Newline.tsx +43 -0
  98. package/packages/nastech-ink/src/ink/components/NoSelect.tsx +73 -0
  99. package/packages/nastech-ink/src/ink/components/RawAnsi.tsx +61 -0
  100. package/packages/nastech-ink/src/ink/components/ScrollBox.tsx +290 -0
  101. package/packages/nastech-ink/src/ink/components/Spacer.tsx +23 -0
  102. package/packages/nastech-ink/src/ink/components/StdinContext.ts +25 -0
  103. package/packages/nastech-ink/src/ink/components/TerminalFocusContext.tsx +63 -0
  104. package/packages/nastech-ink/src/ink/components/TerminalSizeContext.tsx +7 -0
  105. package/packages/nastech-ink/src/ink/components/Text.test.ts +38 -0
  106. package/packages/nastech-ink/src/ink/components/Text.tsx +336 -0
  107. package/packages/nastech-ink/src/ink/constants.ts +6 -0
  108. package/packages/nastech-ink/src/ink/cursor.ts +5 -0
  109. package/packages/nastech-ink/src/ink/devtools.ts +2 -0
  110. package/packages/nastech-ink/src/ink/dom.ts +495 -0
  111. package/packages/nastech-ink/src/ink/events/click-event.ts +38 -0
  112. package/packages/nastech-ink/src/ink/events/cmd-shortcuts.test.ts +65 -0
  113. package/packages/nastech-ink/src/ink/events/dispatcher.ts +242 -0
  114. package/packages/nastech-ink/src/ink/events/emitter.ts +40 -0
  115. package/packages/nastech-ink/src/ink/events/event-handlers.ts +84 -0
  116. package/packages/nastech-ink/src/ink/events/event.ts +11 -0
  117. package/packages/nastech-ink/src/ink/events/focus-event.ts +18 -0
  118. package/packages/nastech-ink/src/ink/events/input-event.ts +176 -0
  119. package/packages/nastech-ink/src/ink/events/keyboard-event.ts +57 -0
  120. package/packages/nastech-ink/src/ink/events/mouse-event.ts +18 -0
  121. package/packages/nastech-ink/src/ink/events/paste-event.ts +10 -0
  122. package/packages/nastech-ink/src/ink/events/resize-event.ts +12 -0
  123. package/packages/nastech-ink/src/ink/events/terminal-event.ts +107 -0
  124. package/packages/nastech-ink/src/ink/events/terminal-focus-event.ts +19 -0
  125. package/packages/nastech-ink/src/ink/focus.ts +219 -0
  126. package/packages/nastech-ink/src/ink/frame.ts +124 -0
  127. package/packages/nastech-ink/src/ink/get-max-width.ts +27 -0
  128. package/packages/nastech-ink/src/ink/global.d.ts +1 -0
  129. package/packages/nastech-ink/src/ink/hit-test.test.ts +38 -0
  130. package/packages/nastech-ink/src/ink/hit-test.ts +224 -0
  131. package/packages/nastech-ink/src/ink/hooks/use-animation-frame.ts +62 -0
  132. package/packages/nastech-ink/src/ink/hooks/use-app.ts +9 -0
  133. package/packages/nastech-ink/src/ink/hooks/use-cursor-advance.ts +33 -0
  134. package/packages/nastech-ink/src/ink/hooks/use-declared-cursor.ts +75 -0
  135. package/packages/nastech-ink/src/ink/hooks/use-external-process.ts +27 -0
  136. package/packages/nastech-ink/src/ink/hooks/use-input.ts +95 -0
  137. package/packages/nastech-ink/src/ink/hooks/use-interval.ts +71 -0
  138. package/packages/nastech-ink/src/ink/hooks/use-search-highlight.ts +56 -0
  139. package/packages/nastech-ink/src/ink/hooks/use-selection.ts +101 -0
  140. package/packages/nastech-ink/src/ink/hooks/use-stdin.ts +9 -0
  141. package/packages/nastech-ink/src/ink/hooks/use-tab-status.ts +71 -0
  142. package/packages/nastech-ink/src/ink/hooks/use-terminal-focus.ts +18 -0
  143. package/packages/nastech-ink/src/ink/hooks/use-terminal-title.ts +34 -0
  144. package/packages/nastech-ink/src/ink/hooks/use-terminal-viewport.ts +100 -0
  145. package/packages/nastech-ink/src/ink/hyperlinkHover.ts +52 -0
  146. package/packages/nastech-ink/src/ink/ink-cursor-advance.test.ts +234 -0
  147. package/packages/nastech-ink/src/ink/ink-resize.test.ts +50 -0
  148. package/packages/nastech-ink/src/ink/ink.tsx +2705 -0
  149. package/packages/nastech-ink/src/ink/instances.ts +10 -0
  150. package/packages/nastech-ink/src/ink/layout/engine.ts +6 -0
  151. package/packages/nastech-ink/src/ink/layout/geometry.ts +98 -0
  152. package/packages/nastech-ink/src/ink/layout/node.ts +145 -0
  153. package/packages/nastech-ink/src/ink/layout/yoga.ts +313 -0
  154. package/packages/nastech-ink/src/ink/line-width-cache.ts +38 -0
  155. package/packages/nastech-ink/src/ink/log-update.test.ts +223 -0
  156. package/packages/nastech-ink/src/ink/log-update.ts +752 -0
  157. package/packages/nastech-ink/src/ink/lru.ts +14 -0
  158. package/packages/nastech-ink/src/ink/measure-element.ts +23 -0
  159. package/packages/nastech-ink/src/ink/measure-text.ts +50 -0
  160. package/packages/nastech-ink/src/ink/node-cache.ts +53 -0
  161. package/packages/nastech-ink/src/ink/optimizer.ts +99 -0
  162. package/packages/nastech-ink/src/ink/output.ts +845 -0
  163. package/packages/nastech-ink/src/ink/parse-keypress.test.ts +133 -0
  164. package/packages/nastech-ink/src/ink/parse-keypress.ts +848 -0
  165. package/packages/nastech-ink/src/ink/reconciler.ts +382 -0
  166. package/packages/nastech-ink/src/ink/render-border.ts +206 -0
  167. package/packages/nastech-ink/src/ink/render-node-to-output.ts +1582 -0
  168. package/packages/nastech-ink/src/ink/render-to-screen.ts +236 -0
  169. package/packages/nastech-ink/src/ink/renderer.ts +169 -0
  170. package/packages/nastech-ink/src/ink/root.ts +204 -0
  171. package/packages/nastech-ink/src/ink/screen.ts +1590 -0
  172. package/packages/nastech-ink/src/ink/searchHighlight.ts +91 -0
  173. package/packages/nastech-ink/src/ink/selection.test.ts +82 -0
  174. package/packages/nastech-ink/src/ink/selection.ts +1143 -0
  175. package/packages/nastech-ink/src/ink/squash-text-nodes.ts +74 -0
  176. package/packages/nastech-ink/src/ink/stringWidth.ts +341 -0
  177. package/packages/nastech-ink/src/ink/styles.ts +750 -0
  178. package/packages/nastech-ink/src/ink/supports-hyperlinks.ts +51 -0
  179. package/packages/nastech-ink/src/ink/tabstops.ts +44 -0
  180. package/packages/nastech-ink/src/ink/terminal-focus-state.ts +52 -0
  181. package/packages/nastech-ink/src/ink/terminal-querier.ts +222 -0
  182. package/packages/nastech-ink/src/ink/terminal.test.ts +15 -0
  183. package/packages/nastech-ink/src/ink/terminal.ts +299 -0
  184. package/packages/nastech-ink/src/ink/termio/ansi.ts +75 -0
  185. package/packages/nastech-ink/src/ink/termio/csi.ts +334 -0
  186. package/packages/nastech-ink/src/ink/termio/dec.ts +99 -0
  187. package/packages/nastech-ink/src/ink/termio/esc.ts +69 -0
  188. package/packages/nastech-ink/src/ink/termio/osc.test.ts +191 -0
  189. package/packages/nastech-ink/src/ink/termio/osc.ts +724 -0
  190. package/packages/nastech-ink/src/ink/termio/parser.ts +467 -0
  191. package/packages/nastech-ink/src/ink/termio/sgr.ts +362 -0
  192. package/packages/nastech-ink/src/ink/termio/tokenize.test.ts +185 -0
  193. package/packages/nastech-ink/src/ink/termio/tokenize.ts +350 -0
  194. package/packages/nastech-ink/src/ink/termio/types.ts +230 -0
  195. package/packages/nastech-ink/src/ink/termio.ts +42 -0
  196. package/packages/nastech-ink/src/ink/useTerminalNotification.ts +110 -0
  197. package/packages/nastech-ink/src/ink/warn.ts +15 -0
  198. package/packages/nastech-ink/src/ink/widest-line.ts +22 -0
  199. package/packages/nastech-ink/src/ink/wrap-text.test.ts +17 -0
  200. package/packages/nastech-ink/src/ink/wrap-text.ts +144 -0
  201. package/packages/nastech-ink/src/ink/wrapAnsi.ts +13 -0
  202. package/packages/nastech-ink/src/native-ts/yoga-layout/enums.ts +112 -0
  203. package/packages/nastech-ink/src/native-ts/yoga-layout/index.ts +2326 -0
  204. package/packages/nastech-ink/src/utils/debug.ts +6 -0
  205. package/packages/nastech-ink/src/utils/earlyInput.ts +131 -0
  206. package/packages/nastech-ink/src/utils/env.ts +66 -0
  207. package/packages/nastech-ink/src/utils/envUtils.ts +13 -0
  208. package/packages/nastech-ink/src/utils/execFileNoThrow.test.ts +146 -0
  209. package/packages/nastech-ink/src/utils/execFileNoThrow.ts +115 -0
  210. package/packages/nastech-ink/src/utils/fullscreen.ts +3 -0
  211. package/packages/nastech-ink/src/utils/intl.ts +87 -0
  212. package/packages/nastech-ink/src/utils/log.ts +7 -0
  213. package/packages/nastech-ink/src/utils/semver.ts +57 -0
  214. package/packages/nastech-ink/src/utils/sliceAnsi.ts +106 -0
  215. package/packages/nastech-ink/text-input.d.ts +2 -0
  216. package/packages/nastech-ink/text-input.js +1 -0
  217. package/scripts/build.mjs +61 -0
  218. package/scripts/profile-tui.mjs +121 -0
  219. package/src/__tests__/activeSessionSwitcher.test.ts +157 -0
  220. package/src/__tests__/appChromeStatusRule.test.tsx +84 -0
  221. package/src/__tests__/appChromeStatusRuleDevCredits.test.tsx +73 -0
  222. package/src/__tests__/approvalAction.test.ts +50 -0
  223. package/src/__tests__/asCommandDispatch.test.ts +27 -0
  224. package/src/__tests__/blockLayout.test.ts +122 -0
  225. package/src/__tests__/clipboard.test.ts +369 -0
  226. package/src/__tests__/constants.test.ts +53 -0
  227. package/src/__tests__/createGatewayEventHandler.test.ts +1091 -0
  228. package/src/__tests__/createSlashHandler.test.ts +822 -0
  229. package/src/__tests__/creditsCommand.test.ts +144 -0
  230. package/src/__tests__/cursorDriftRegression.test.ts +114 -0
  231. package/src/__tests__/details.test.ts +115 -0
  232. package/src/__tests__/emoji.test.ts +64 -0
  233. package/src/__tests__/externalLink.test.ts +144 -0
  234. package/src/__tests__/forceTruecolor.test.ts +191 -0
  235. package/src/__tests__/gatewayClient.test.ts +394 -0
  236. package/src/__tests__/gatewayRecovery.test.ts +47 -0
  237. package/src/__tests__/markdown.test.ts +331 -0
  238. package/src/__tests__/mathUnicode.test.ts +293 -0
  239. package/src/__tests__/memoryMonitor.test.ts +102 -0
  240. package/src/__tests__/messageLine.test.ts +19 -0
  241. package/src/__tests__/messages.test.ts +92 -0
  242. package/src/__tests__/orchestratorPromptSession.test.ts +64 -0
  243. package/src/__tests__/osc52.test.ts +67 -0
  244. package/src/__tests__/parentLog.test.ts +75 -0
  245. package/src/__tests__/paths.test.ts +70 -0
  246. package/src/__tests__/platform.test.ts +556 -0
  247. package/src/__tests__/precisionWheel.test.ts +44 -0
  248. package/src/__tests__/prompt.test.ts +31 -0
  249. package/src/__tests__/providers.test.ts +65 -0
  250. package/src/__tests__/reasoning.test.ts +76 -0
  251. package/src/__tests__/rpc.test.ts +27 -0
  252. package/src/__tests__/scroll.test.ts +99 -0
  253. package/src/__tests__/slashParity.test.ts +123 -0
  254. package/src/__tests__/spawnHistoryStore.test.ts +46 -0
  255. package/src/__tests__/stateIsolation.test.ts +46 -0
  256. package/src/__tests__/statusBarTicker.test.ts +18 -0
  257. package/src/__tests__/statusRule.test.ts +32 -0
  258. package/src/__tests__/streamingMarkdown.test.ts +121 -0
  259. package/src/__tests__/subagentTree.test.ts +407 -0
  260. package/src/__tests__/syntax.test.ts +45 -0
  261. package/src/__tests__/terminalModes.test.ts +39 -0
  262. package/src/__tests__/terminalParity.test.ts +77 -0
  263. package/src/__tests__/terminalSetup.test.ts +386 -0
  264. package/src/__tests__/termux.test.ts +35 -0
  265. package/src/__tests__/termuxComposerLayout.test.ts +40 -0
  266. package/src/__tests__/text.test.ts +233 -0
  267. package/src/__tests__/textInputBurstInput.test.ts +40 -0
  268. package/src/__tests__/textInputCursorSourceOfTruth.test.ts +50 -0
  269. package/src/__tests__/textInputFastEcho.test.ts +200 -0
  270. package/src/__tests__/textInputLineNav.test.ts +55 -0
  271. package/src/__tests__/textInputPassThrough.test.ts +59 -0
  272. package/src/__tests__/textInputRightClick.test.ts +48 -0
  273. package/src/__tests__/textInputWrap.test.ts +151 -0
  274. package/src/__tests__/theme.test.ts +311 -0
  275. package/src/__tests__/turnControllerNotice.test.ts +43 -0
  276. package/src/__tests__/turnStore.test.ts +66 -0
  277. package/src/__tests__/useCompletion.test.ts +35 -0
  278. package/src/__tests__/useComposerState.test.ts +59 -0
  279. package/src/__tests__/useConfigSync.test.ts +460 -0
  280. package/src/__tests__/useInputHandlers.test.ts +77 -0
  281. package/src/__tests__/useQueue.test.ts +28 -0
  282. package/src/__tests__/useSessionLifecycle.test.ts +60 -0
  283. package/src/__tests__/useVirtualHistoryHeights.test.ts +39 -0
  284. package/src/__tests__/viewport.test.ts +58 -0
  285. package/src/__tests__/viewportStore.test.ts +85 -0
  286. package/src/__tests__/virtualHeights.test.ts +96 -0
  287. package/src/__tests__/virtualHistoryClamp.test.ts +19 -0
  288. package/src/__tests__/virtualHistoryOffsetCache.test.ts +282 -0
  289. package/src/__tests__/wheelAccel.test.ts +138 -0
  290. package/src/app/createGatewayEventHandler.ts +833 -0
  291. package/src/app/createSlashHandler.ts +130 -0
  292. package/src/app/delegationStore.ts +77 -0
  293. package/src/app/gatewayContext.tsx +19 -0
  294. package/src/app/gatewayRecovery.ts +35 -0
  295. package/src/app/inputSelectionStore.ts +15 -0
  296. package/src/app/interfaces.ts +394 -0
  297. package/src/app/overlayStore.ts +53 -0
  298. package/src/app/scroll.ts +71 -0
  299. package/src/app/setupHandoff.ts +54 -0
  300. package/src/app/slash/commands/core.ts +648 -0
  301. package/src/app/slash/commands/credits.ts +57 -0
  302. package/src/app/slash/commands/debug.ts +48 -0
  303. package/src/app/slash/commands/ops.ts +717 -0
  304. package/src/app/slash/commands/session.ts +554 -0
  305. package/src/app/slash/commands/setup.ts +20 -0
  306. package/src/app/slash/registry.ts +20 -0
  307. package/src/app/slash/types.ts +21 -0
  308. package/src/app/spawnHistoryStore.ts +159 -0
  309. package/src/app/turnController.ts +866 -0
  310. package/src/app/turnStore.ts +85 -0
  311. package/src/app/uiStore.ts +44 -0
  312. package/src/app/useComposerState.ts +367 -0
  313. package/src/app/useConfigSync.ts +288 -0
  314. package/src/app/useInputHandlers.ts +576 -0
  315. package/src/app/useLongRunToolCharms.ts +69 -0
  316. package/src/app/useMainApp.ts +1039 -0
  317. package/src/app/useSessionLifecycle.ts +366 -0
  318. package/src/app/useSubmission.ts +429 -0
  319. package/src/app.tsx +25 -0
  320. package/src/banner.ts +93 -0
  321. package/src/components/activeSessionSwitcher.tsx +635 -0
  322. package/src/components/agentsOverlay.tsx +1073 -0
  323. package/src/components/appChrome.tsx +554 -0
  324. package/src/components/appLayout.tsx +444 -0
  325. package/src/components/appOverlays.tsx +254 -0
  326. package/src/components/branding.tsx +466 -0
  327. package/src/components/fpsOverlay.tsx +30 -0
  328. package/src/components/helpHint.tsx +73 -0
  329. package/src/components/markdown.tsx +1119 -0
  330. package/src/components/maskedPrompt.tsx +34 -0
  331. package/src/components/messageLine.tsx +237 -0
  332. package/src/components/modelPicker.tsx +527 -0
  333. package/src/components/overlayControls.tsx +50 -0
  334. package/src/components/pluginsHub.tsx +238 -0
  335. package/src/components/prompts.tsx +276 -0
  336. package/src/components/queuedMessages.tsx +64 -0
  337. package/src/components/sessionPicker.tsx +227 -0
  338. package/src/components/skillsHub.tsx +308 -0
  339. package/src/components/streamingAssistant.tsx +110 -0
  340. package/src/components/streamingMarkdown.tsx +174 -0
  341. package/src/components/textInput.tsx +1340 -0
  342. package/src/components/themed.tsx +30 -0
  343. package/src/components/thinking.tsx +1224 -0
  344. package/src/components/todoPanel.tsx +93 -0
  345. package/src/config/env.ts +64 -0
  346. package/src/config/limits.ts +13 -0
  347. package/src/config/timing.ts +6 -0
  348. package/src/content/charms.ts +1 -0
  349. package/src/content/faces.ts +17 -0
  350. package/src/content/fortunes.ts +30 -0
  351. package/src/content/hotkeys.ts +37 -0
  352. package/src/content/placeholders.ts +13 -0
  353. package/src/content/setup.ts +17 -0
  354. package/src/content/verbs.ts +38 -0
  355. package/src/domain/blockLayout.ts +146 -0
  356. package/src/domain/details.ts +76 -0
  357. package/src/domain/messages.ts +91 -0
  358. package/src/domain/paths.ts +16 -0
  359. package/src/domain/providers.ts +11 -0
  360. package/src/domain/roles.ts +9 -0
  361. package/src/domain/slash.ts +10 -0
  362. package/src/domain/usage.ts +3 -0
  363. package/src/domain/viewport.ts +51 -0
  364. package/src/entry.tsx +104 -0
  365. package/src/gatewayClient.ts +730 -0
  366. package/src/gatewayTypes.ts +568 -0
  367. package/src/hooks/useCompletion.ts +112 -0
  368. package/src/hooks/useGitBranch.ts +72 -0
  369. package/src/hooks/useInputHistory.ts +11 -0
  370. package/src/hooks/useQueue.ts +76 -0
  371. package/src/hooks/useVirtualHistory.ts +554 -0
  372. package/src/lib/circularBuffer.ts +48 -0
  373. package/src/lib/clipboard.ts +182 -0
  374. package/src/lib/editor.test.ts +74 -0
  375. package/src/lib/editor.ts +47 -0
  376. package/src/lib/emoji.ts +55 -0
  377. package/src/lib/externalCli.ts +16 -0
  378. package/src/lib/externalLink.ts +435 -0
  379. package/src/lib/forceTruecolor.ts +60 -0
  380. package/src/lib/fpsStore.ts +51 -0
  381. package/src/lib/fuzzy.test.ts +109 -0
  382. package/src/lib/fuzzy.ts +177 -0
  383. package/src/lib/gracefulExit.ts +47 -0
  384. package/src/lib/history.ts +82 -0
  385. package/src/lib/inputMetrics.ts +203 -0
  386. package/src/lib/liveProgress.test.ts +116 -0
  387. package/src/lib/liveProgress.ts +79 -0
  388. package/src/lib/mathUnicode.ts +770 -0
  389. package/src/lib/memory.test.ts +155 -0
  390. package/src/lib/memory.ts +188 -0
  391. package/src/lib/memoryMonitor.ts +109 -0
  392. package/src/lib/messages.test.ts +29 -0
  393. package/src/lib/messages.ts +8 -0
  394. package/src/lib/openExternalUrl.test.ts +217 -0
  395. package/src/lib/openExternalUrl.ts +158 -0
  396. package/src/lib/osc52.ts +73 -0
  397. package/src/lib/parentLog.ts +57 -0
  398. package/src/lib/perfPane.tsx +107 -0
  399. package/src/lib/platform.ts +409 -0
  400. package/src/lib/precisionWheel.ts +48 -0
  401. package/src/lib/prompt.ts +35 -0
  402. package/src/lib/reasoning.ts +55 -0
  403. package/src/lib/rpc.ts +41 -0
  404. package/src/lib/subagentTree.ts +355 -0
  405. package/src/lib/syntax.ts +117 -0
  406. package/src/lib/terminalModes.ts +51 -0
  407. package/src/lib/terminalParity.ts +78 -0
  408. package/src/lib/terminalSetup.ts +444 -0
  409. package/src/lib/termux.ts +29 -0
  410. package/src/lib/text.test.ts +18 -0
  411. package/src/lib/text.ts +339 -0
  412. package/src/lib/todo.test.ts +21 -0
  413. package/src/lib/todo.ts +9 -0
  414. package/src/lib/viewportStore.ts +124 -0
  415. package/src/lib/virtualHeights.ts +145 -0
  416. package/src/lib/wheelAccel.ts +190 -0
  417. package/src/protocol/interpolation.ts +3 -0
  418. package/src/protocol/paste.ts +1 -0
  419. package/src/theme.ts +589 -0
  420. package/src/types/nastech-ink.d.ts +176 -0
  421. package/src/types.ts +212 -0
  422. package/tsconfig.build.json +9 -0
  423. package/tsconfig.json +19 -0
  424. package/vitest.config.ts +7 -0
@@ -0,0 +1,1119 @@
1
+ import { Box, Link, stringWidth, Text } from '@nastechai/ink'
2
+ import { Fragment, memo, type ReactNode, useMemo } from 'react'
3
+
4
+ import { ensureEmojiPresentation } from '../lib/emoji.js'
5
+ import { normalizeExternalUrl, urlSlugTitleLabel, useLinkTitle } from '../lib/externalLink.js'
6
+ import { BOX_CLOSE, BOX_OPEN, texToUnicode } from '../lib/mathUnicode.js'
7
+ import { highlightLine, isHighlightable } from '../lib/syntax.js'
8
+ import type { Theme } from '../theme.js'
9
+
10
+ // `\boxed{X}` regions in `texToUnicode` output are marked with the
11
+ // non-printable U+0001 / U+0002 sentinels. Split on them and render the
12
+ // boxed segment with `inverse + bold` so it reads as a highlighter-pen
13
+ // emphasis on top of whatever color the parent `<Text>` is using (the
14
+ // theme accent for math). The leading / trailing space inside the
15
+ // highlight gives a one-cell visual margin so the highlight reads as a
16
+ // block, not a hug.
17
+ const renderMath = (text: string): ReactNode => {
18
+ if (!text.includes(BOX_OPEN)) {
19
+ return text
20
+ }
21
+
22
+ const out: ReactNode[] = []
23
+ let i = 0
24
+ let key = 0
25
+
26
+ while (i < text.length) {
27
+ const start = text.indexOf(BOX_OPEN, i)
28
+
29
+ if (start < 0) {
30
+ out.push(text.slice(i))
31
+
32
+ break
33
+ }
34
+
35
+ if (start > i) {
36
+ out.push(text.slice(i, start))
37
+ }
38
+
39
+ const end = text.indexOf(BOX_CLOSE, start + 1)
40
+
41
+ if (end < 0) {
42
+ out.push(text.slice(start))
43
+
44
+ break
45
+ }
46
+
47
+ out.push(
48
+ <Text bold inverse key={key++}>
49
+ {' '}
50
+ {text.slice(start + 1, end)}{' '}
51
+ </Text>
52
+ )
53
+
54
+ i = end + 1
55
+ }
56
+
57
+ return out
58
+ }
59
+
60
+ const FENCE_RE = /^\s*(`{3,}|~{3,})(.*)$/
61
+ const FENCE_CLOSE_RE = /^\s*(`{3,}|~{3,})\s*$/
62
+ const HR_RE = /^ {0,3}([-*_])(?:\s*\1){2,}\s*$/
63
+ const HEADING_RE = /^\s{0,3}(#{1,6})\s+(.*?)(?:\s+#+\s*)?$/
64
+ const SETEXT_RE = /^\s{0,3}(=+|-+)\s*$/
65
+ const FOOTNOTE_RE = /^\[\^([^\]]+)\]:\s*(.*)$/
66
+ const DEF_RE = /^\s*:\s+(.+)$/
67
+ const BULLET_RE = /^(\s*)[-+*]\s+(.*)$/
68
+ const TASK_RE = /^\[( |x|X)\]\s+(.*)$/
69
+ const NUMBERED_RE = /^(\s*)(\d+)[.)]\s+(.*)$/
70
+ const QUOTE_RE = /^\s*(?:>\s*)+/
71
+ const TABLE_DIVIDER_CELL_RE = /^:?-{3,}:?$/
72
+ const MD_URL_RE = '((?:[^\\s()]|\\([^\\s()]*\\))+?)'
73
+ const MD_IDENTIFIER_RE = '[A-Za-z_][A-Za-z0-9_]*'
74
+ const MD_DUNDER_IDENTIFIER_RE = `(?:${MD_IDENTIFIER_RE}__(?!\\w))`
75
+ const MD_UNDERSCORE_BOLD_RE = `(?<!\\w)__(?!${MD_DUNDER_IDENTIFIER_RE})(.+?)__(?!\\w)`
76
+ const MD_UNDERSCORE_ITALIC_RE = `(?<![\\w_])_(?!_)(.+?)(?<!_)_(?![\\w_])`
77
+ const STRIP_UNDERSCORE_BOLD_RE = new RegExp(MD_UNDERSCORE_BOLD_RE, 'g')
78
+ const STRIP_UNDERSCORE_ITALIC_RE = new RegExp(MD_UNDERSCORE_ITALIC_RE, 'g')
79
+
80
+ // Display math openers: `$$ ... $$` (TeX) and `\[ ... \]` (LaTeX). The
81
+ // opener is matched only when `$$` / `\[` appears at the very start of the
82
+ // trimmed line — `startsWith('$$')` used to fire on prose like
83
+ // `$$x+y$$ followed by more`, opening a block that never closed because the
84
+ // trailing `$$` on the same line was invisible to the close-scan loop.
85
+ const MATH_BLOCK_OPEN_RE = /^\s*(\$\$|\\\[)(.*)$/
86
+ const MATH_BLOCK_CLOSE_DOLLAR_RE = /^(.*?)\$\$\s*$/
87
+ const MATH_BLOCK_CLOSE_BRACKET_RE = /^(.*?)\\\]\s*$/
88
+
89
+ export const MEDIA_LINE_RE = /^\s*[`"']?MEDIA:\s*(\S+?)[`"']?\s*$/
90
+ export const AUDIO_DIRECTIVE_RE = /^\s*\[\[audio_as_voice\]\]\s*$/
91
+
92
+ // Inline markdown tokens, in priority order. The outer regex picks the
93
+ // leftmost match at each position, preferring earlier alternatives on tie —
94
+ // so `**` must come before `*`, `__` before `_`, etc. Each pattern owns its
95
+ // own capture groups; MdInline dispatches on which group matched.
96
+ //
97
+ // Subscript (`~x~`) is restricted to short alphanumeric runs so prose like
98
+ // `thing ~! more ~?` from Kimi / Qwen / GLM (kaomoji-style decorators)
99
+ // doesn't pair up the first `~` with the next one on the line and swallow
100
+ // the text between them as a dim `_`-prefixed span.
101
+ //
102
+ // Inline math (`$x$` and `\(x\)`) takes precedence over emphasis at the
103
+ // same start position because regex alternation is leftmost-first; a
104
+ // dollar-delimited span at column N wins over a `*` at column N+1, so
105
+ // `$P=a*b*c$` renders as math instead of having `*b*` corrupted into
106
+ // italics. Single-character minimums and "no space adjacent to delimiter"
107
+ // rules keep currency prose like `$5 to $10` from being swallowed.
108
+ export const INLINE_RE = new RegExp(
109
+ [
110
+ `!\\[(.*?)\\]\\(${MD_URL_RE}\\)`, // 1,2 image
111
+ `\\[(.+?)\\]\\(${MD_URL_RE}\\)`, // 3,4 link
112
+ `<((?:https?:\\/\\/|mailto:)[^>\\s]+|[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,})>`, // 5 autolink
113
+ `~~(.+?)~~`, // 6 strike
114
+ `\`([^\\\`]+)\``, // 7 code
115
+ `\\*\\*(.+?)\\*\\*`, // 8 bold *
116
+ MD_UNDERSCORE_BOLD_RE, // 9 bold _
117
+ `\\*(.+?)\\*`, // 10 italic *
118
+ MD_UNDERSCORE_ITALIC_RE, // 11 italic _
119
+ `==(.+?)==`, // 12 highlight
120
+ `\\[\\^([^\\]]+)\\]`, // 13 footnote ref
121
+ `\\^([^^\\s][^^]*?)\\^`, // 14 superscript
122
+ `~([A-Za-z0-9]{1,8})~`, // 15 subscript
123
+ `(https?:\\/\\/[^\\s<]+)`, // 16 bare URL — wrapped so it owns its own
124
+ // capture group; without this, the math
125
+ // spans below would land in m[16] and the
126
+ // MdInline dispatcher would treat them as
127
+ // bare URLs and render them as autolinks.
128
+ `(?<!\\$)\\$([^\\s$](?:[^$\\n]*?[^\\s$])?)\\$(?!\\$)`, // 17 inline math $...$
129
+ `\\\\\\(([^\\n]+?)\\\\\\)` // 18 inline math \(...\)
130
+ ].join('|'),
131
+ 'g'
132
+ )
133
+
134
+ const indentDepth = (s: string) => Math.floor(s.replace(/\t/g, ' ').length / 2)
135
+
136
+ const splitRow = (row: string) =>
137
+ row
138
+ .trim()
139
+ .replace(/^\|/, '')
140
+ .replace(/\|$/, '')
141
+ .split('|')
142
+ .map(c => c.trim())
143
+
144
+ const isTableDivider = (row: string) => {
145
+ const cells = splitRow(row)
146
+
147
+ return cells.length > 1 && cells.every(c => TABLE_DIVIDER_CELL_RE.test(c))
148
+ }
149
+
150
+ const autolinkUrl = (raw: string) =>
151
+ raw.startsWith('mailto:') || raw.startsWith('http') || !raw.includes('@') ? raw : `mailto:${raw}`
152
+
153
+ const defaultLinkLabel = (url: string) =>
154
+ url.startsWith('mailto:') ? url.replace(/^mailto:/, '') : /^https?:\/\//i.test(url) ? urlSlugTitleLabel(url) : url
155
+
156
+ const pickFallbackLabel = (label: string | undefined, target: string): string | undefined => {
157
+ const trimmed = label?.trim()
158
+
159
+ if (!trimmed) {
160
+ return undefined
161
+ }
162
+
163
+ return normalizeExternalUrl(trimmed) === target ? undefined : trimmed
164
+ }
165
+
166
+ interface ResolvedLinkProps {
167
+ fallbackLabel?: string
168
+ t: Theme
169
+ url: string
170
+ }
171
+
172
+ function ResolvedLink({ fallbackLabel, t, url }: ResolvedLinkProps) {
173
+ const fetched = useLinkTitle(url)
174
+ const display = fetched || fallbackLabel || defaultLinkLabel(url)
175
+
176
+ return (
177
+ <Link url={url}>
178
+ <Text color={t.color.accent} underline>
179
+ {display}
180
+ </Text>
181
+ </Link>
182
+ )
183
+ }
184
+
185
+ const renderResolvedLink = (k: number, t: Theme, rawUrl: string, label?: string) => {
186
+ const target = normalizeExternalUrl(rawUrl)
187
+
188
+ return <ResolvedLink fallbackLabel={pickFallbackLabel(label, target)} key={k} t={t} url={target} />
189
+ }
190
+
191
+ export const stripInlineMarkup = (v: string) =>
192
+ v
193
+ .replace(/!\[(.*?)\]\(((?:[^\s()]|\([^\s()]*\))+?)\)/g, '[image: $1] $2')
194
+ .replace(/\[(.+?)\]\(((?:[^\s()]|\([^\s()]*\))+?)\)/g, '$1')
195
+ .replace(/<((?:https?:\/\/|mailto:)[^>\s]+|[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,})>/g, '$1')
196
+ .replace(/~~(.+?)~~/g, '$1')
197
+ .replace(/`([^`]+)`/g, '$1')
198
+ .replace(/\*\*(.+?)\*\*/g, '$1')
199
+ .replace(STRIP_UNDERSCORE_BOLD_RE, '$1')
200
+ .replace(/\*(.+?)\*/g, '$1')
201
+ .replace(STRIP_UNDERSCORE_ITALIC_RE, '$1')
202
+ .replace(/==(.+?)==/g, '$1')
203
+ .replace(/\[\^([^\]]+)\]/g, '[$1]')
204
+ .replace(/\^([^^\s][^^]*?)\^/g, '^$1')
205
+ .replace(/~([A-Za-z0-9]{1,8})~/g, '_$1')
206
+ .replace(/(?<!\$)\$([^\s$](?:[^$\n]*?[^\s$])?)\$(?!\$)/g, '$1')
207
+ .replace(/\\\(([^\n]+?)\\\)/g, '$1')
208
+
209
+ const SAFETY_MARGIN = 4
210
+ const MIN_COL_WIDTH = 3
211
+ const COL_GAP = 2 // the ' ' between columns
212
+ const TABLE_PADDING_LEFT = 2 // paddingLeft={2} on the outer <Box>
213
+
214
+ const renderTable = (k: number, rows: string[][], t: Theme, cols?: number) => {
215
+ // Guard: empty table
216
+ if (rows.length === 0 || rows[0]!.length === 0) return null
217
+
218
+ const cellDisplayWidth = (raw: string) => stringWidth(stripInlineMarkup(raw))
219
+
220
+ // Minimum width: longest word in a cell (to avoid breaking words)
221
+ const minCellWidth = (raw: string) => {
222
+ const text = stripInlineMarkup(raw)
223
+ const words = text.split(/\s+/).filter(w => w.length > 0)
224
+ if (words.length === 0) return MIN_COL_WIDTH
225
+ return Math.max(...words.map(w => stringWidth(w)), MIN_COL_WIDTH)
226
+ }
227
+
228
+ const numCols = rows[0]!.length
229
+
230
+ // Normalize ragged rows: ensure every row has exactly numCols cells
231
+ const normalizedRows = rows.map(row => {
232
+ if (row.length >= numCols) return row.slice(0, numCols)
233
+ return [...row, ...Array<string>(numCols - row.length).fill('')]
234
+ })
235
+
236
+ // Ideal widths: max cell content per column
237
+ const idealWidths = normalizedRows[0]!.map((_, ci) =>
238
+ Math.max(...normalizedRows.map(r => cellDisplayWidth(r[ci] ?? '')), MIN_COL_WIDTH)
239
+ )
240
+
241
+ // Min widths: longest word per column
242
+ const minWidths = normalizedRows[0]!.map((_, ci) =>
243
+ Math.max(...normalizedRows.map(r => minCellWidth(r[ci] ?? '')), MIN_COL_WIDTH)
244
+ )
245
+
246
+ // Available width: cols minus table padding minus column gaps minus safety.
247
+ // transcriptBodyWidth (source of cols) subtracts message gutter + scrollbar,
248
+ // but NOT this table's paddingLeft — we subtract it here.
249
+ const gapOverhead = (numCols - 1) * COL_GAP
250
+ const availableWidth = cols
251
+ ? Math.max(cols - TABLE_PADDING_LEFT - gapOverhead - SAFETY_MARGIN, numCols * MIN_COL_WIDTH)
252
+ : Infinity
253
+
254
+ const totalIdeal = idealWidths.reduce((a, b) => a + b, 0)
255
+ const totalMin = minWidths.reduce((a, b) => a + b, 0)
256
+
257
+ let columnWidths: number[]
258
+ let needsWrap = false
259
+
260
+ if (totalIdeal <= availableWidth) {
261
+ // Tier 1: everything fits at ideal widths
262
+ columnWidths = idealWidths
263
+ } else if (totalMin <= availableWidth) {
264
+ // Tier 2: proportional shrink — distribute extra space beyond minimums
265
+ needsWrap = true
266
+ const extraSpace = availableWidth - totalMin
267
+ const overflows = idealWidths.map((ideal, i) => ideal - minWidths[i]!)
268
+ const totalOverflow = overflows.reduce((a, b) => a + b, 0)
269
+ if (totalOverflow === 0) {
270
+ columnWidths = [...minWidths]
271
+ } else {
272
+ const rawAlloc = minWidths.map((min, i) =>
273
+ min + (overflows[i]! / totalOverflow) * extraSpace
274
+ )
275
+ columnWidths = rawAlloc.map(v => Math.floor(v))
276
+ // Distribute rounding remainders to columns with largest fractional part
277
+ let remainder = availableWidth - columnWidths.reduce((a, b) => a + b, 0)
278
+ const fracs = rawAlloc.map((v, i) => ({ i, frac: v - Math.floor(v) }))
279
+ .sort((a, b) => b.frac - a.frac)
280
+ for (const { i } of fracs) {
281
+ if (remainder <= 0) break
282
+ columnWidths[i]!++
283
+ remainder--
284
+ }
285
+ }
286
+ } else {
287
+ // Tier 3: even min-widths don't fit — scale proportionally, allow hard breaks.
288
+ // NOTE: Math.max(..., MIN_COL_WIDTH) can push total above availableWidth when
289
+ // many columns are scaled below 3. This is caught by safetyOverflow → vertical fallback.
290
+ needsWrap = true
291
+ const scaleFactor = availableWidth / totalMin
292
+ const rawAlloc = minWidths.map(w => w * scaleFactor)
293
+ columnWidths = rawAlloc.map(v => Math.max(Math.floor(v), MIN_COL_WIDTH))
294
+ let remainder = availableWidth - columnWidths.reduce((a, b) => a + b, 0)
295
+ const fracs = rawAlloc.map((v, i) => ({ i, frac: v - Math.floor(v) }))
296
+ .sort((a, b) => b.frac - a.frac)
297
+ for (const { i } of fracs) {
298
+ if (remainder <= 0) break
299
+ columnWidths[i]!++
300
+ remainder--
301
+ }
302
+ }
303
+
304
+ // Grapheme-safe hard-break: prefer Intl.Segmenter, fall back to code-point split
305
+ const segmenter = typeof Intl !== 'undefined' && 'Segmenter' in Intl
306
+ ? new (Intl as any).Segmenter(undefined, { granularity: 'grapheme' })
307
+ : null
308
+
309
+ const graphemes = (s: string): string[] =>
310
+ segmenter
311
+ ? [...segmenter.segment(s)].map((seg: { segment: string }) => seg.segment)
312
+ : [...s]
313
+
314
+ // Word-wrap plain text to fit within `width` display columns.
315
+ // Operates on stripped text for correct width measurement.
316
+ const wrapCell = (raw: string, width: number, hard: boolean): string[] => {
317
+ const text = stripInlineMarkup(raw)
318
+ if (width <= 0) return [text]
319
+ if (stringWidth(text) <= width) return [text]
320
+
321
+ const words = text.split(/\s+/).filter(w => w.length > 0)
322
+ const lines: string[] = []
323
+ let current = ''
324
+ let currentWidth = 0
325
+
326
+ for (const word of words) {
327
+ const w = stringWidth(word)
328
+ if (currentWidth === 0) {
329
+ if (hard && w > width) {
330
+ for (const ch of graphemes(word)) {
331
+ const cw = stringWidth(ch)
332
+ if (currentWidth + cw > width && current) {
333
+ lines.push(current)
334
+ current = ''
335
+ currentWidth = 0
336
+ }
337
+ current += ch
338
+ currentWidth += cw
339
+ }
340
+ } else {
341
+ current = word
342
+ currentWidth = w
343
+ }
344
+ } else if (currentWidth + 1 + w <= width) {
345
+ current += ' ' + word
346
+ currentWidth += 1 + w
347
+ } else {
348
+ lines.push(current)
349
+ current = word
350
+ currentWidth = w
351
+ }
352
+ }
353
+ if (current) lines.push(current)
354
+ return lines.length > 0 ? lines : ['']
355
+ }
356
+
357
+ const isHard = totalMin > availableWidth // tier 3 needs hard word breaks
358
+ const sep = columnWidths.map(w => '─'.repeat(Math.max(1, w))).join(' ')
359
+
360
+ // When wrapping isn't needed, build single-line strings per row.
361
+ // All cells render as plain text via stripInlineMarkup.
362
+ // TODO: follow-up — format to ANSI then wrap with wrapAnsi for inline markdown preservation.
363
+ // See free-code/src/components/MarkdownTable.tsx L44-L62 for approach.
364
+ if (!needsWrap) {
365
+ const buildRowString = (row: string[]): string =>
366
+ row.map((cell, ci) => {
367
+ const text = stripInlineMarkup(cell)
368
+ const pad = ' '.repeat(Math.max(0, columnWidths[ci]! - stringWidth(text)))
369
+ const gap = ci < numCols - 1 ? ' ' : ''
370
+ return text + pad + gap
371
+ }).join('')
372
+
373
+ return (
374
+ <Box flexDirection="column" key={k} paddingLeft={TABLE_PADDING_LEFT}>
375
+ {normalizedRows.map((row, ri) => (
376
+ <Fragment key={ri}>
377
+ <Text
378
+ bold={ri === 0}
379
+ color={ri === 0 ? t.color.accent : undefined}
380
+ wrap="truncate-end"
381
+ >
382
+ {buildRowString(row)}
383
+ </Text>
384
+ {ri === 0 && normalizedRows.length > 1 ? (
385
+ <Text color={t.color.muted} dimColor wrap="truncate-end">{sep}</Text>
386
+ ) : null}
387
+ </Fragment>
388
+ ))}
389
+ </Box>
390
+ )
391
+ }
392
+
393
+ // Wrapping path: build multi-line rows as complete strings.
394
+ type LineEntry = { text: string; kind: 'header' | 'separator' | 'body' }
395
+
396
+ const buildRowLines = (row: string[]): string[] => {
397
+ const cellLines = row.map((cell, ci) =>
398
+ wrapCell(cell, columnWidths[ci]!, isHard)
399
+ )
400
+ const maxLines = Math.max(...cellLines.map(l => l.length), 1)
401
+
402
+ const result: string[] = []
403
+ for (let li = 0; li < maxLines; li++) {
404
+ let line = ''
405
+ for (let ci = 0; ci < numCols; ci++) {
406
+ const cl = cellLines[ci] ?? ['']
407
+ const cellText = li < cl.length ? cl[li]! : ''
408
+ const pad = ' '.repeat(Math.max(0, columnWidths[ci]! - stringWidth(cellText)))
409
+ line += cellText + pad
410
+ if (ci < numCols - 1) line += ' '
411
+ }
412
+ result.push(line)
413
+ }
414
+ return result
415
+ }
416
+
417
+ // Build all lines with metadata for styling, tracking tallest body row
418
+ const allEntries: LineEntry[] = []
419
+ let tallestBodyRow = 0
420
+ normalizedRows.forEach((row, ri) => {
421
+ const kind = ri === 0 ? 'header' as const : 'body' as const
422
+ const rowLines = buildRowLines(row)
423
+ rowLines.forEach(text => allEntries.push({ text, kind }))
424
+ if (ri > 0) tallestBodyRow = Math.max(tallestBodyRow, rowLines.length)
425
+ if (ri === 0 && normalizedRows.length > 1) {
426
+ allEntries.push({ text: sep, kind: 'separator' })
427
+ }
428
+ })
429
+
430
+ // Post-render safety condition: compute max line width.
431
+ const maxLineWidth = Math.max(...allEntries.map(e => stringWidth(e.text)))
432
+ const safetyOverflow = cols != null && maxLineWidth > cols - TABLE_PADDING_LEFT - SAFETY_MARGIN
433
+
434
+ // Scaled vertical threshold — 2-3 col tables stay tabular even with tall cells
435
+ const maxRowLinesThreshold = numCols <= 3 ? 8 : numCols <= 6 ? 5 : 4
436
+
437
+ const useVertical = tallestBodyRow > maxRowLinesThreshold || safetyOverflow
438
+
439
+ if (useVertical) {
440
+ // Edge case: header-only table
441
+ if (normalizedRows.length <= 1) {
442
+ return (
443
+ <Box flexDirection="column" key={k} paddingLeft={TABLE_PADDING_LEFT}>
444
+ <Text bold color={t.color.accent} wrap="wrap-trim">
445
+ {normalizedRows[0]!.map(h => stripInlineMarkup(h)).join(' · ')}
446
+ </Text>
447
+ </Box>
448
+ )
449
+ }
450
+
451
+ const headers = normalizedRows[0]!
452
+ const dataRows = normalizedRows.slice(1)
453
+ const sepWidth = Math.max(1, cols ? Math.min(cols - TABLE_PADDING_LEFT - 1, 40) : 40)
454
+
455
+ return (
456
+ <Box flexDirection="column" key={k} paddingLeft={TABLE_PADDING_LEFT}>
457
+ {dataRows.map((row, ri) => (
458
+ <Fragment key={ri}>
459
+ {ri > 0 ? (
460
+ <Text color={t.color.muted} dimColor>{'─'.repeat(sepWidth)}</Text>
461
+ ) : null}
462
+ {headers.map((header, ci) => {
463
+ const cell = row[ci] ?? ''
464
+ const label = stripInlineMarkup(header) || `Col ${ci + 1}`
465
+ return (
466
+ <Text key={ci} wrap="wrap-trim">
467
+ <Text bold color={t.color.accent}>{label}:</Text>
468
+ {' '}{stripInlineMarkup(cell)}
469
+ </Text>
470
+ )
471
+ })}
472
+ </Fragment>
473
+ ))}
474
+ </Box>
475
+ )
476
+ }
477
+
478
+ // Render wrapped horizontal rows — one <Text> per visual line.
479
+ return (
480
+ <Box flexDirection="column" key={k} paddingLeft={TABLE_PADDING_LEFT}>
481
+ {allEntries.map((entry, i) => (
482
+ <Text
483
+ bold={entry.kind === 'header'}
484
+ color={entry.kind === 'header' ? t.color.accent : entry.kind === 'separator' ? t.color.muted : undefined}
485
+ dimColor={entry.kind === 'separator'}
486
+ key={i}
487
+ wrap="truncate-end"
488
+ >
489
+ {entry.text}
490
+ </Text>
491
+ ))}
492
+ </Box>
493
+ )
494
+ }
495
+
496
+ function MdInline({ t, text }: { t: Theme; text: string }) {
497
+ const parts: ReactNode[] = []
498
+
499
+ let last = 0
500
+
501
+ for (const m of text.matchAll(INLINE_RE)) {
502
+ const i = m.index ?? 0
503
+ const k = parts.length
504
+
505
+ if (i > last) {
506
+ parts.push(<Text key={k}>{text.slice(last, i)}</Text>)
507
+ }
508
+
509
+ if (m[1] && m[2]) {
510
+ parts.push(
511
+ <Text color={t.color.muted} key={parts.length}>
512
+ [image: {m[1]}] {m[2]}
513
+ </Text>
514
+ )
515
+ } else if (m[3] && m[4]) {
516
+ parts.push(renderResolvedLink(parts.length, t, m[4], m[3]))
517
+ } else if (m[5]) {
518
+ parts.push(renderResolvedLink(parts.length, t, autolinkUrl(m[5]), m[5].replace(/^mailto:/, '')))
519
+ } else if (m[6]) {
520
+ parts.push(
521
+ <Text key={parts.length} strikethrough>
522
+ <MdInline t={t} text={m[6]} />
523
+ </Text>
524
+ )
525
+ } else if (m[7]) {
526
+ // Code is the one wrap that does NOT recurse — inline `code` spans
527
+ // are verbatim by definition. Letting MdInline reprocess them
528
+ // would corrupt regex examples and shell snippets.
529
+ parts.push(
530
+ <Text color={t.color.accent} dimColor key={parts.length}>
531
+ {m[7]}
532
+ </Text>
533
+ )
534
+ } else if (m[8] ?? m[9]) {
535
+ // Recurse into bold / italic / strike / highlight so nested
536
+ // `$...$` math (and other inline tokens) inside a `**bolded
537
+ // statement with $\mathbb{Z}$ math**` actually render. Without
538
+ // this the inner content is dropped into a single `<Text bold>`
539
+ // verbatim and the math renderer never sees it.
540
+ parts.push(
541
+ <Text bold key={parts.length}>
542
+ <MdInline t={t} text={m[8] ?? m[9]!} />
543
+ </Text>
544
+ )
545
+ } else if (m[10] ?? m[11]) {
546
+ parts.push(
547
+ <Text italic key={parts.length}>
548
+ <MdInline t={t} text={m[10] ?? m[11]!} />
549
+ </Text>
550
+ )
551
+ } else if (m[12]) {
552
+ parts.push(
553
+ <Text backgroundColor={t.color.diffAdded} color={t.color.diffAddedWord} key={parts.length}>
554
+ <MdInline t={t} text={m[12]} />
555
+ </Text>
556
+ )
557
+ } else if (m[13]) {
558
+ parts.push(
559
+ <Text color={t.color.muted} key={parts.length}>
560
+ [{m[13]}]
561
+ </Text>
562
+ )
563
+ } else if (m[14]) {
564
+ parts.push(
565
+ <Text color={t.color.muted} key={parts.length}>
566
+ ^{m[14]}
567
+ </Text>
568
+ )
569
+ } else if (m[15]) {
570
+ parts.push(
571
+ <Text color={t.color.muted} key={parts.length}>
572
+ _{m[15]}
573
+ </Text>
574
+ )
575
+ } else if (m[16]) {
576
+ // Bare URL — trim trailing prose punctuation into a sibling text node
577
+ // so `see https://x.com/, which…` keeps the comma outside the link.
578
+ const url = m[16].replace(/[),.;:!?]+$/g, '')
579
+
580
+ parts.push(renderResolvedLink(parts.length, t, url))
581
+
582
+ if (url.length < m[16].length) {
583
+ parts.push(<Text key={parts.length}>{m[16].slice(url.length)}</Text>)
584
+ }
585
+ } else if (m[17] ?? m[18]) {
586
+ // Inline math is run through `texToUnicode` (Greek letters, ℕℤℚℝ,
587
+ // operators, sub/superscripts, fractions) and rendered in italic
588
+ // accent. Italic is the disambiguator — links use accent+underline,
589
+ // so without italic readers can't tell `\mathbb{R}` (math) from a
590
+ // hyperlinked word. Anything `texToUnicode` doesn't recognise is
591
+ // preserved verbatim, so unfamiliar commands just look like their
592
+ // raw LaTeX rather than vanishing.
593
+ parts.push(
594
+ <Text color={t.color.accent} italic key={parts.length}>
595
+ {renderMath(texToUnicode(m[17] ?? m[18]!))}
596
+ </Text>
597
+ )
598
+ }
599
+
600
+ last = i + m[0].length
601
+ }
602
+
603
+ if (last < text.length) {
604
+ parts.push(<Text key={parts.length}>{text.slice(last)}</Text>)
605
+ }
606
+
607
+ return <Text wrap="wrap-trim">{parts.length ? parts : text}</Text>
608
+ }
609
+
610
+ // Cross-instance parsed-children cache: useMemo's per-instance cache dies
611
+ // on remount, so virtualization re-parses every row that scrolls back into
612
+ // view. Theme-keyed WeakMap drops stale palettes; inner Map is LRU-bounded.
613
+ const MD_CACHE_LIMIT = 512
614
+ const mdCache = new WeakMap<Theme, Map<string, ReactNode[]>>()
615
+
616
+ const cacheBucket = (t: Theme) => {
617
+ const b = mdCache.get(t)
618
+
619
+ if (b) {
620
+ return b
621
+ }
622
+
623
+ const fresh = new Map<string, ReactNode[]>()
624
+ mdCache.set(t, fresh)
625
+
626
+ return fresh
627
+ }
628
+
629
+ const cacheGet = (b: Map<string, ReactNode[]>, key: string) => {
630
+ const v = b.get(key)
631
+
632
+ if (v) {
633
+ b.delete(key)
634
+ b.set(key, v)
635
+ }
636
+
637
+ return v
638
+ }
639
+
640
+ const cacheSet = (b: Map<string, ReactNode[]>, key: string, v: ReactNode[]) => {
641
+ b.set(key, v)
642
+
643
+ if (b.size > MD_CACHE_LIMIT) {
644
+ b.delete(b.keys().next().value!)
645
+ }
646
+ }
647
+
648
+ function MdImpl({ cols, compact, t, text }: MdProps) {
649
+ const nodes = useMemo(() => {
650
+ const bucket = cacheBucket(t)
651
+ const cacheKey = `${compact ? '1' : '0'}|${cols ?? ''}|${text}`
652
+ const cached = cacheGet(bucket, cacheKey)
653
+
654
+ if (cached) {
655
+ return cached
656
+ }
657
+
658
+ const lines = ensureEmojiPresentation(text).split('\n')
659
+ const nodes: ReactNode[] = []
660
+
661
+ let prevKind: Kind = null
662
+ let i = 0
663
+
664
+ const gap = () => {
665
+ if (nodes.length && prevKind !== 'blank') {
666
+ nodes.push(<Text key={`gap-${nodes.length}`}> </Text>)
667
+ prevKind = 'blank'
668
+ }
669
+ }
670
+
671
+ const start = (kind: Exclude<Kind, null | 'blank'>) => {
672
+ if (prevKind && prevKind !== 'blank' && prevKind !== kind) {
673
+ gap()
674
+ }
675
+
676
+ prevKind = kind
677
+ }
678
+
679
+ while (i < lines.length) {
680
+ const line = lines[i]!
681
+ const key = nodes.length
682
+
683
+ if (!line.trim()) {
684
+ if (!compact) {
685
+ gap()
686
+ }
687
+
688
+ i++
689
+
690
+ continue
691
+ }
692
+
693
+ if (AUDIO_DIRECTIVE_RE.test(line)) {
694
+ i++
695
+
696
+ continue
697
+ }
698
+
699
+ const media = line.match(MEDIA_LINE_RE)?.[1]
700
+
701
+ if (media) {
702
+ start('paragraph')
703
+ nodes.push(
704
+ <Text color={t.color.muted} key={key} wrap="wrap-trim">
705
+ {'▸ '}
706
+
707
+ <Link url={/^(?:\/|[a-z]:[\\/])/i.test(media) ? `file://${media}` : media}>
708
+ <Text color={t.color.accent} underline>
709
+ {media}
710
+ </Text>
711
+ </Link>
712
+ </Text>
713
+ )
714
+ i++
715
+
716
+ continue
717
+ }
718
+
719
+ const fence = line.match(FENCE_RE)
720
+
721
+ if (fence) {
722
+ const char = fence[1]![0] as '`' | '~'
723
+ const len = fence[1]!.length
724
+ const lang = fence[2]!.trim().toLowerCase()
725
+ const block: string[] = []
726
+
727
+ for (i++; i < lines.length; i++) {
728
+ const close = lines[i]!.match(FENCE_CLOSE_RE)?.[1]
729
+
730
+ if (close && close[0] === char && close.length >= len) {
731
+ break
732
+ }
733
+
734
+ block.push(lines[i]!)
735
+ }
736
+
737
+ if (i < lines.length) {
738
+ i++
739
+ }
740
+
741
+ if (['md', 'markdown'].includes(lang)) {
742
+ start('paragraph')
743
+ nodes.push(<Md cols={cols} compact={compact} key={key} t={t} text={block.join('\n')} />)
744
+
745
+ continue
746
+ }
747
+
748
+ start('code')
749
+
750
+ const isDiff = lang === 'diff'
751
+ const highlighted = !isDiff && isHighlightable(lang)
752
+
753
+ nodes.push(
754
+ <Box flexDirection="column" key={key} paddingLeft={2}>
755
+ {lang && !isDiff && <Text color={t.color.muted}>{'─ ' + lang}</Text>}
756
+
757
+ {block.map((l, j) => {
758
+ if (highlighted) {
759
+ return (
760
+ <Text key={j}>
761
+ {highlightLine(l, lang, t).map(([color, text], kk) =>
762
+ color ? (
763
+ <Text color={color} key={kk}>
764
+ {text}
765
+ </Text>
766
+ ) : (
767
+ <Text key={kk}>{text}</Text>
768
+ )
769
+ )}
770
+ </Text>
771
+ )
772
+ }
773
+
774
+ const add = isDiff && l.startsWith('+')
775
+ const del = isDiff && l.startsWith('-')
776
+ const hunk = isDiff && l.startsWith('@@')
777
+
778
+ return (
779
+ <Text
780
+ backgroundColor={add ? t.color.diffAdded : del ? t.color.diffRemoved : undefined}
781
+ color={add ? t.color.diffAddedWord : del ? t.color.diffRemovedWord : hunk ? t.color.muted : undefined}
782
+ dimColor={isDiff && !add && !del && !hunk && l.startsWith(' ')}
783
+ key={j}
784
+ >
785
+ {l}
786
+ </Text>
787
+ )
788
+ })}
789
+ </Box>
790
+ )
791
+
792
+ continue
793
+ }
794
+
795
+ const mathOpen = line.match(MATH_BLOCK_OPEN_RE)
796
+
797
+ if (mathOpen) {
798
+ const opener = mathOpen[1]!
799
+ const closeRe = opener === '$$' ? MATH_BLOCK_CLOSE_DOLLAR_RE : MATH_BLOCK_CLOSE_BRACKET_RE
800
+ const headRest = mathOpen[2] ?? ''
801
+ const block: string[] = []
802
+
803
+ // Single-line block: `$$x + y = z$$` or `\[x\]`. Capture inner content
804
+ // and emit the block immediately. Without this, the close-scan loop
805
+ // skips line `i` and treats the next opener as our closer, swallowing
806
+ // every paragraph in between.
807
+ const sameLineClose = headRest.match(closeRe)
808
+
809
+ if (sameLineClose) {
810
+ const inner = sameLineClose[1]!.trim()
811
+
812
+ start('code')
813
+ nodes.push(
814
+ <Box flexDirection="column" key={key} paddingLeft={2}>
815
+ {inner ? <Text color={t.color.accent}>{renderMath(texToUnicode(inner))}</Text> : null}
816
+ </Box>
817
+ )
818
+ i++
819
+
820
+ continue
821
+ }
822
+
823
+ // Multi-line block: scan ahead for a real closer before committing.
824
+ // If none exists in the rest of the document, render this line as a
825
+ // paragraph instead of consuming everything that follows.
826
+ let closeIdx = -1
827
+
828
+ for (let j = i + 1; j < lines.length; j++) {
829
+ if (closeRe.test(lines[j]!)) {
830
+ closeIdx = j
831
+
832
+ break
833
+ }
834
+ }
835
+
836
+ if (closeIdx < 0) {
837
+ start('paragraph')
838
+ nodes.push(<MdInline key={key} t={t} text={line} />)
839
+ i++
840
+
841
+ continue
842
+ }
843
+
844
+ if (headRest.trim()) {
845
+ block.push(headRest)
846
+ }
847
+
848
+ for (let j = i + 1; j < closeIdx; j++) {
849
+ block.push(lines[j]!)
850
+ }
851
+
852
+ const tail = lines[closeIdx]!.match(closeRe)![1]!.trimEnd()
853
+
854
+ if (tail.trim()) {
855
+ block.push(tail)
856
+ }
857
+
858
+ start('code')
859
+ nodes.push(
860
+ <Box flexDirection="column" key={key} paddingLeft={2}>
861
+ {block.map((l, j) => (
862
+ <Text color={t.color.accent} key={j}>
863
+ {renderMath(texToUnicode(l))}
864
+ </Text>
865
+ ))}
866
+ </Box>
867
+ )
868
+ i = closeIdx + 1
869
+
870
+ continue
871
+ }
872
+
873
+ const heading = line.match(HEADING_RE)?.[2]
874
+
875
+ if (heading) {
876
+ start('heading')
877
+ nodes.push(
878
+ <Text bold color={t.color.accent} key={key} wrap="wrap-trim">
879
+ <MdInline t={t} text={heading} />
880
+ </Text>
881
+ )
882
+ i++
883
+
884
+ continue
885
+ }
886
+
887
+ if (i + 1 < lines.length && SETEXT_RE.test(lines[i + 1]!)) {
888
+ start('heading')
889
+ nodes.push(
890
+ <Text bold color={t.color.accent} key={key} wrap="wrap-trim">
891
+ <MdInline t={t} text={line.trim()} />
892
+ </Text>
893
+ )
894
+ i += 2
895
+
896
+ continue
897
+ }
898
+
899
+ if (HR_RE.test(line)) {
900
+ start('rule')
901
+ nodes.push(
902
+ <Text color={t.color.muted} key={key}>
903
+ {'─'.repeat(36)}
904
+ </Text>
905
+ )
906
+ i++
907
+
908
+ continue
909
+ }
910
+
911
+ const footnote = line.match(FOOTNOTE_RE)
912
+
913
+ if (footnote) {
914
+ start('list')
915
+ nodes.push(
916
+ <Text color={t.color.muted} key={key} wrap="wrap-trim">
917
+ [{footnote[1]}] <MdInline t={t} text={footnote[2] ?? ''} />
918
+ </Text>
919
+ )
920
+ i++
921
+
922
+ while (i < lines.length && /^\s{2,}\S/.test(lines[i]!)) {
923
+ nodes.push(
924
+ <Box key={`${key}-cont-${i}`} paddingLeft={2}>
925
+ <Text color={t.color.muted} wrap="wrap-trim">
926
+ <MdInline t={t} text={lines[i]!.trim()} />
927
+ </Text>
928
+ </Box>
929
+ )
930
+ i++
931
+ }
932
+
933
+ continue
934
+ }
935
+
936
+ if (i + 1 < lines.length && DEF_RE.test(lines[i + 1]!)) {
937
+ start('list')
938
+ nodes.push(
939
+ <Text bold key={key} wrap="wrap-trim">
940
+ {line.trim()}
941
+ </Text>
942
+ )
943
+ i++
944
+
945
+ while (i < lines.length) {
946
+ const def = lines[i]!.match(DEF_RE)?.[1]
947
+
948
+ if (!def) {
949
+ break
950
+ }
951
+
952
+ nodes.push(
953
+ <Text key={`${key}-def-${i}`} wrap="wrap-trim">
954
+ <Text color={t.color.muted}> · </Text>
955
+ <MdInline t={t} text={def} />
956
+ </Text>
957
+ )
958
+ i++
959
+ }
960
+
961
+ continue
962
+ }
963
+
964
+ const bullet = line.match(BULLET_RE)
965
+
966
+ if (bullet) {
967
+ start('list')
968
+
969
+ const task = bullet[2]!.match(TASK_RE)
970
+ const marker = task ? (task[1]!.toLowerCase() === 'x' ? '☑' : '☐') : '•'
971
+
972
+ nodes.push(
973
+ <Box key={key} paddingLeft={indentDepth(bullet[1]!) * 2}>
974
+ <Text wrap="wrap-trim">
975
+ <Text color={t.color.muted}>{marker} </Text>
976
+ <MdInline t={t} text={task ? task[2]! : bullet[2]!} />
977
+ </Text>
978
+ </Box>
979
+ )
980
+ i++
981
+
982
+ continue
983
+ }
984
+
985
+ const numbered = line.match(NUMBERED_RE)
986
+
987
+ if (numbered) {
988
+ start('list')
989
+ nodes.push(
990
+ <Box key={key} paddingLeft={indentDepth(numbered[1]!) * 2}>
991
+ <Text wrap="wrap-trim">
992
+ <Text color={t.color.muted}>{numbered[2]}. </Text>
993
+ <MdInline t={t} text={numbered[3]!} />
994
+ </Text>
995
+ </Box>
996
+ )
997
+ i++
998
+
999
+ continue
1000
+ }
1001
+
1002
+ if (QUOTE_RE.test(line)) {
1003
+ start('quote')
1004
+
1005
+ const quoteLines: Array<{ depth: number; text: string }> = []
1006
+
1007
+ while (i < lines.length && QUOTE_RE.test(lines[i]!)) {
1008
+ const prefix = lines[i]!.match(QUOTE_RE)?.[0] ?? ''
1009
+
1010
+ quoteLines.push({ depth: (prefix.match(/>/g) ?? []).length, text: lines[i]!.slice(prefix.length) })
1011
+ i++
1012
+ }
1013
+
1014
+ nodes.push(
1015
+ <Box flexDirection="column" key={key}>
1016
+ {quoteLines.map((ql, qi) => (
1017
+ <Box key={qi} paddingLeft={Math.max(0, ql.depth - 1) * 2}>
1018
+ <Text color={t.color.muted} wrap="wrap-trim">
1019
+ │ <MdInline t={t} text={ql.text} />
1020
+ </Text>
1021
+ </Box>
1022
+ ))}
1023
+ </Box>
1024
+ )
1025
+
1026
+ continue
1027
+ }
1028
+
1029
+ if (line.includes('|') && i + 1 < lines.length && isTableDivider(lines[i + 1]!)) {
1030
+ start('table')
1031
+
1032
+ const rows: string[][] = [splitRow(line)]
1033
+
1034
+ for (i += 2; i < lines.length && lines[i]!.includes('|') && lines[i]!.trim(); i++) {
1035
+ rows.push(splitRow(lines[i]!))
1036
+ }
1037
+
1038
+ nodes.push(renderTable(key, rows, t, cols))
1039
+
1040
+ continue
1041
+ }
1042
+
1043
+ if (/^<\/?details\b/i.test(line)) {
1044
+ i++
1045
+
1046
+ continue
1047
+ }
1048
+
1049
+ const summary = line.match(/^<summary>(.*?)<\/summary>$/i)?.[1]
1050
+
1051
+ if (summary) {
1052
+ start('paragraph')
1053
+ nodes.push(
1054
+ <Text color={t.color.muted} key={key} wrap="wrap-trim">
1055
+ ▶ {summary}
1056
+ </Text>
1057
+ )
1058
+ i++
1059
+
1060
+ continue
1061
+ }
1062
+
1063
+ if (/^<\/?[^>]+>$/.test(line.trim())) {
1064
+ start('paragraph')
1065
+ nodes.push(
1066
+ <Text color={t.color.muted} key={key} wrap="wrap-trim">
1067
+ {line.trim()}
1068
+ </Text>
1069
+ )
1070
+ i++
1071
+
1072
+ continue
1073
+ }
1074
+
1075
+ if (line.includes('|') && line.trim().startsWith('|')) {
1076
+ start('table')
1077
+
1078
+ const rows: string[][] = []
1079
+
1080
+ while (i < lines.length && lines[i]!.trim().startsWith('|')) {
1081
+ const row = lines[i]!.trim()
1082
+
1083
+ if (!/^[|\s:-]+$/.test(row)) {
1084
+ rows.push(splitRow(row))
1085
+ }
1086
+
1087
+ i++
1088
+ }
1089
+
1090
+ if (rows.length) {
1091
+ nodes.push(renderTable(key, rows, t, cols))
1092
+ }
1093
+
1094
+ continue
1095
+ }
1096
+
1097
+ start('paragraph')
1098
+ nodes.push(<MdInline key={key} t={t} text={line} />)
1099
+ i++
1100
+ }
1101
+
1102
+ cacheSet(bucket, cacheKey, nodes)
1103
+
1104
+ return nodes
1105
+ }, [cols, compact, t, text])
1106
+
1107
+ return <Box flexDirection="column">{nodes}</Box>
1108
+ }
1109
+
1110
+ export const Md = memo(MdImpl)
1111
+
1112
+ type Kind = 'blank' | 'code' | 'heading' | 'list' | 'paragraph' | 'quote' | 'rule' | 'table' | null
1113
+
1114
+ interface MdProps {
1115
+ cols?: number
1116
+ compact?: boolean
1117
+ t: Theme
1118
+ text: string
1119
+ }