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,339 @@
1
+ import {
2
+ LIVE_RENDER_MAX_CHARS,
3
+ LIVE_RENDER_MAX_LINES,
4
+ THINKING_COT_MAX
5
+ } from '../config/limits.js'
6
+ import { VERBS } from '../content/verbs.js'
7
+ import type { ThinkingMode } from '../types.js'
8
+
9
+ const ESC = String.fromCharCode(27)
10
+ const BEL = String.fromCharCode(7)
11
+ const ANSI_CSI_RE = new RegExp(`${ESC}\\[[0-?]*[ -/]*[@-~]`, 'g')
12
+ const ANSI_CSI_WITH_CMD_RE = new RegExp(`${ESC}\\[[0-?]*[ -/]*([@-~])`, 'g')
13
+ const ANSI_INCOMPLETE_CSI_RE = new RegExp(`${ESC}\\[[0-?]*[ -/]*(?=${ESC}|\\n|$)`, 'g')
14
+ const ANSI_OSC_RE = new RegExp(`${ESC}\\][\\s\\S]*?(?:${BEL}|${ESC}\\\\)`, 'g')
15
+ const ANSI_STRING_RE = new RegExp(`${ESC}[PX^_][\\s\\S]*?(?:${BEL}|${ESC}\\\\)`, 'g')
16
+ const ANSI_NON_CSI_ESC_SEQ_RE = new RegExp(`${ESC}(?!\\[|\\]|P|X|\\^|_)[ -/]*[0-~]`, 'g')
17
+ const ANSI_STRAY_ESC_RE = new RegExp(`${ESC}(?!\\[)[\\s\\S]?`, 'g')
18
+ const CONTROL_RE = /[\x00-\x08\x0B\x0C\x0D\x0E-\x1A\x1C-\x1F\x7F]/g
19
+ const WS_RE = /\s+/g
20
+
21
+ export const stripAnsi = (s: string) =>
22
+ s
23
+ .replace(ANSI_OSC_RE, '')
24
+ .replace(ANSI_STRING_RE, '')
25
+ .replace(ANSI_INCOMPLETE_CSI_RE, '')
26
+ .replace(ANSI_CSI_RE, '')
27
+ .replace(ANSI_INCOMPLETE_CSI_RE, '')
28
+ .replace(ANSI_NON_CSI_ESC_SEQ_RE, '')
29
+ .replace(ANSI_STRAY_ESC_RE, '')
30
+ .replace(CONTROL_RE, '')
31
+
32
+ export const sanitizeAnsiForRender = (s: string) =>
33
+ s
34
+ .replace(ANSI_OSC_RE, '')
35
+ .replace(ANSI_STRING_RE, '')
36
+ .replace(ANSI_INCOMPLETE_CSI_RE, '')
37
+ .replace(ANSI_CSI_WITH_CMD_RE, (seq, cmd: string) => (cmd === 'm' ? seq : ''))
38
+ .replace(ANSI_INCOMPLETE_CSI_RE, '')
39
+ .replace(ANSI_NON_CSI_ESC_SEQ_RE, '')
40
+ .replace(ANSI_STRAY_ESC_RE, '')
41
+ .replace(CONTROL_RE, '')
42
+
43
+ export const hasAnsi = (s: string) => s.includes(ESC)
44
+
45
+ const renderEstimateLine = (line: string) => {
46
+ const trimmed = line.trim()
47
+
48
+ if (trimmed.startsWith('|')) {
49
+ return trimmed
50
+ .split('|')
51
+ .filter(Boolean)
52
+ .map(cell => cell.trim())
53
+ .join(' ')
54
+ }
55
+
56
+ return line
57
+ .replace(/!\[(.*?)\]\(([^)\s]+)\)/g, '[image: $1]')
58
+ .replace(/\[(.+?)\]\((https?:\/\/[^\s)]+)\)/g, '$1')
59
+ .replace(/`([^`]+)`/g, '$1')
60
+ .replace(/\*\*(.+?)\*\*/g, '$1')
61
+ .replace(/(?<!\w)__(.+?)__(?!\w)/g, '$1')
62
+ .replace(/\*(.+?)\*/g, '$1')
63
+ .replace(/(?<!\w)_(.+?)_(?!\w)/g, '$1')
64
+ .replace(/~~(.+?)~~/g, '$1')
65
+ .replace(/==(.+?)==/g, '$1')
66
+ .replace(/\[\^([^\]]+)\]/g, '[$1]')
67
+ .replace(/^#{1,6}\s+/, '')
68
+ .replace(/^\s*[-*+]\s+\[( |x|X)\]\s+/, (_m, checked: string) => `• [${checked.toLowerCase() === 'x' ? 'x' : ' '}] `)
69
+ .replace(/^\s*[-*+]\s+/, '• ')
70
+ .replace(/^\s*(\d+)\.\s+/, '$1. ')
71
+ .replace(/^\s*(?:>\s*)+/, '│ ')
72
+ }
73
+
74
+ export const compactPreview = (s: string, max: number) => {
75
+ const one = s.replace(WS_RE, ' ').trim()
76
+
77
+ return !one ? '' : one.length > max ? one.slice(0, max - 1) + '…' : one
78
+ }
79
+
80
+ export const estimateTokensRough = (text: string) => (!text ? 0 : (text.length + 3) >> 2)
81
+
82
+ export const edgePreview = (s: string, head = 16, tail = 28) => {
83
+ const one = s.replace(WS_RE, ' ').trim().replace(/\]\]/g, '] ]')
84
+
85
+ return !one
86
+ ? ''
87
+ : one.length <= head + tail + 4
88
+ ? one
89
+ : `${one.slice(0, head).trimEnd()}.. ${one.slice(-tail).trimStart()}`
90
+ }
91
+
92
+ export const pasteTokenLabel = (text: string, lineCount: number) => {
93
+ const preview = edgePreview(text)
94
+
95
+ if (!preview) {
96
+ return `[[ [${fmtK(lineCount)} lines] ]]`
97
+ }
98
+
99
+ const [head = preview, tail = ''] = preview.split('.. ', 2)
100
+
101
+ return tail
102
+ ? `[[ ${head.trimEnd()}.. [${fmtK(lineCount)} lines] .. ${tail.trimStart()} ]]`
103
+ : `[[ ${preview} [${fmtK(lineCount)} lines] ]]`
104
+ }
105
+
106
+ const THINKING_STATUS_RE = new RegExp(`^(?:${VERBS.join('|')})\\.{0,3}$`, 'i')
107
+ const THINKING_STATUS_CHUNK_RE = new RegExp(`[^A-Za-z\n]+\\s*(?:${VERBS.join('|')})\\.{0,3}\\s*`, 'giu')
108
+
109
+ export const cleanThinkingText = (reasoning: string) =>
110
+ reasoning
111
+ .split('\n')
112
+ .map(line => line.replace(THINKING_STATUS_CHUNK_RE, '').trim())
113
+ .filter(line => line && !THINKING_STATUS_RE.test(line.replace(/\.\.\.$/, '').trim()))
114
+ .join('\n')
115
+ .replace(/([^\n])(?=\*\*[^*\n][^\n]*?\*\*)/g, '$1\n\n')
116
+ .replace(/\n{3,}/g, '\n\n')
117
+ .trim()
118
+
119
+ export const thinkingPreview = (reasoning: string, mode: ThinkingMode, max: number = THINKING_COT_MAX) => {
120
+ const raw = cleanThinkingText(reasoning)
121
+
122
+ return !raw || mode === 'collapsed' ? '' : mode === 'full' ? raw : compactPreview(raw.replace(WS_RE, ' '), max)
123
+ }
124
+
125
+ export const boundedLiveRenderText = (
126
+ text: string,
127
+ { maxChars = LIVE_RENDER_MAX_CHARS, maxLines = LIVE_RENDER_MAX_LINES } = {}
128
+ ) => boundedRenderText(text, 'showing live tail', { maxChars, maxLines })
129
+
130
+ const boundedRenderText = (
131
+ text: string,
132
+ labelPrefix: string,
133
+ { maxChars, maxLines }: { maxChars: number; maxLines: number }
134
+ ) => {
135
+ if (text.length <= maxChars && text.split('\n', maxLines + 1).length <= maxLines) {
136
+ return text
137
+ }
138
+
139
+ let start = 0
140
+ let idx = text.length
141
+
142
+ for (let seen = 0; seen < maxLines && idx > 0; seen++) {
143
+ idx = text.lastIndexOf('\n', idx - 1)
144
+ start = idx < 0 ? 0 : idx + 1
145
+
146
+ if (idx < 0) {
147
+ break
148
+ }
149
+ }
150
+
151
+ const lineStart = start
152
+ start = Math.max(lineStart, text.length - maxChars)
153
+
154
+ if (start > lineStart) {
155
+ const nextBreak = text.indexOf('\n', start)
156
+
157
+ if (nextBreak >= 0 && nextBreak < text.length - 1) {
158
+ start = nextBreak + 1
159
+ }
160
+ }
161
+
162
+ const tail = text.slice(start).trimStart()
163
+ const omittedLines = countNewlines(text, start)
164
+ const omittedChars = Math.max(0, text.length - tail.length)
165
+
166
+ const label =
167
+ omittedLines > 0
168
+ ? `[${labelPrefix}; omitted ${fmtK(omittedLines)} lines / ${fmtK(omittedChars)} chars]\n`
169
+ : `[${labelPrefix}; omitted ${fmtK(omittedChars)} chars]\n`
170
+
171
+ return `${label}${tail}`
172
+ }
173
+
174
+ const countNewlines = (text: string, end: number) => {
175
+ let count = 0
176
+
177
+ for (let i = 0; i < end; i++) {
178
+ if (text.charCodeAt(i) === 10) {
179
+ count++
180
+ }
181
+ }
182
+
183
+ return count
184
+ }
185
+
186
+ export const stripTrailingPasteNewlines = (text: string) => (/[^\n]/.test(text) ? text.replace(/\n+$/, '') : text)
187
+
188
+ export const toolTrailLabel = (name: string) =>
189
+ name
190
+ .split('_')
191
+ .filter(Boolean)
192
+ .map(p => p[0]!.toUpperCase() + p.slice(1))
193
+ .join(' ') || name
194
+
195
+ export const formatToolCall = (name: string, context = '') => {
196
+ const label = toolTrailLabel(name)
197
+ const preview = compactPreview(context, 64)
198
+
199
+ return preview ? `${label}("${preview}")` : label
200
+ }
201
+
202
+ export const buildToolTrailLine = (
203
+ name: string,
204
+ context: string,
205
+ error?: boolean,
206
+ note?: string,
207
+ duration?: number
208
+ ) => {
209
+ const detail = compactPreview(note ?? '', 72)
210
+ const took = duration !== undefined ? ` (${duration.toFixed(1)}s)` : ''
211
+
212
+ return `${formatToolCall(name, context)}${took}${detail ? ` :: ${detail}` : ''} ${error ? '✗' : '✓'}`
213
+ }
214
+
215
+ const verboseToolBlock = (label: string, text?: string) => {
216
+ const body = (text ?? '').trim()
217
+
218
+ return body ? `${label}:\n${boundedLiveRenderText(body)}` : ''
219
+ }
220
+
221
+ export const buildVerboseToolTrailLine = (
222
+ name: string,
223
+ context: string,
224
+ error?: boolean,
225
+ duration?: number,
226
+ argsText?: string,
227
+ resultText?: string
228
+ ) => {
229
+ const detail = [verboseToolBlock('Args', argsText), verboseToolBlock(error ? 'Error' : 'Result', resultText)]
230
+ .filter(Boolean)
231
+ .join('\n')
232
+ const took = duration !== undefined ? ` (${duration.toFixed(1)}s)` : ''
233
+
234
+ return `${formatToolCall(name, context)}${took}${detail ? ` :: ${detail}` : ''} ${error ? '✗' : '✓'}`
235
+ }
236
+
237
+ export const isToolTrailResultLine = (line: string) => line.endsWith(' ✓') || line.endsWith(' ✗')
238
+
239
+ export const parseToolTrailResultLine = (line: string) => {
240
+ if (!isToolTrailResultLine(line)) {
241
+ return null
242
+ }
243
+
244
+ const mark = line.endsWith(' ✗') ? '✗' : '✓'
245
+ const body = line.slice(0, -2)
246
+ const sep = body.indexOf(' :: ')
247
+
248
+ if (sep >= 0) {
249
+ return { call: body.slice(0, sep), detail: body.slice(sep + 4), mark }
250
+ }
251
+
252
+ const legacy = body.indexOf(': ')
253
+
254
+ if (legacy > 0) {
255
+ return { call: body.slice(0, legacy), detail: body.slice(legacy + 2), mark }
256
+ }
257
+
258
+ return { call: body, detail: '', mark }
259
+ }
260
+
261
+ export const splitToolDuration = (call: string) => {
262
+ const match = call.match(/^(.*?)( \(\d+(?:\.\d)?s\))$/)
263
+
264
+ return match ? { label: match[1]!, duration: match[2]! } : { label: call, duration: '' }
265
+ }
266
+
267
+ export const isTransientTrailLine = (line: string) => line.startsWith('drafting ') || line === 'analyzing tool output…'
268
+
269
+ export const sameToolTrailGroup = (label: string, entry: string) =>
270
+ entry === `${label} ✓` ||
271
+ entry === `${label} ✗` ||
272
+ entry.startsWith(`${label}(`) ||
273
+ entry.startsWith(`${label} ::`) ||
274
+ entry.startsWith(`${label}:`)
275
+
276
+ export const lastCotTrailIndex = (trail: readonly string[]) => {
277
+ for (let i = trail.length - 1; i >= 0; i--) {
278
+ if (!isToolTrailResultLine(trail[i]!)) {
279
+ return i
280
+ }
281
+ }
282
+
283
+ return -1
284
+ }
285
+
286
+ export const estimateRows = (text: string, w: number, compact = false) => {
287
+ let fence: { char: '`' | '~'; len: number } | null = null
288
+ let rows = 0
289
+
290
+ for (const raw of text.split('\n')) {
291
+ const line = stripAnsi(raw)
292
+ const maybeFence = line.match(/^\s*(`{3,}|~{3,})(.*)$/)
293
+
294
+ if (maybeFence) {
295
+ const marker = maybeFence[1]!
296
+ const lang = maybeFence[2]!.trim()
297
+
298
+ if (!fence) {
299
+ fence = { char: marker[0] as '`' | '~', len: marker.length }
300
+
301
+ if (lang) {
302
+ rows += Math.ceil((`─ ${lang}`.length || 1) / w)
303
+ }
304
+ } else if (marker[0] === fence.char && marker.length >= fence.len) {
305
+ fence = null
306
+ }
307
+
308
+ continue
309
+ }
310
+
311
+ const inCode = Boolean(fence)
312
+ const trimmed = line.trim()
313
+
314
+ if (!inCode && trimmed.startsWith('|') && /^[|\s:-]+$/.test(trimmed)) {
315
+ continue
316
+ }
317
+
318
+ const rendered = inCode ? line : renderEstimateLine(line)
319
+
320
+ if (compact && !rendered.trim()) {
321
+ continue
322
+ }
323
+
324
+ rows += Math.ceil((rendered.length || 1) / w)
325
+ }
326
+
327
+ return Math.max(1, rows)
328
+ }
329
+
330
+ export const flat = (r: Record<string, string[]>) => Object.values(r).flat()
331
+
332
+ const COMPACT_NUMBER = new Intl.NumberFormat('en-US', { maximumFractionDigits: 1, notation: 'compact' })
333
+
334
+ export const fmtK = (n: number) => COMPACT_NUMBER.format(n).replace(/[KMBT]$/, s => s.toLowerCase())
335
+
336
+ export const pick = <T>(a: T[]) => a[Math.floor(Math.random() * a.length)]!
337
+
338
+ export const isPasteBackedText = (text: string) =>
339
+ /\[\[paste:\d+(?:[^\n]*?)\]\]|\[paste #\d+ (?:attached|excerpt)(?:[^\n]*?)\]/.test(text)
@@ -0,0 +1,21 @@
1
+ import { describe, expect, it } from 'vitest'
2
+
3
+ import { todoGlyph, todoTone } from './todo.js'
4
+
5
+ describe('todoGlyph', () => {
6
+ it('uses fixed-width ASCII markers so the active row does not render wide or emoji-like', () => {
7
+ expect(todoGlyph('completed')).toBe('[x]')
8
+ expect(todoGlyph('in_progress')).toBe('[>]')
9
+ expect(todoGlyph('pending')).toBe('[ ]')
10
+ expect(todoGlyph('cancelled')).toBe('[-]')
11
+ })
12
+ })
13
+
14
+ describe('todoTone', () => {
15
+ it('keeps todo status rows neutral instead of red/green', () => {
16
+ expect(todoTone('completed')).toBe('dim')
17
+ expect(todoTone('cancelled')).toBe('dim')
18
+ expect(todoTone('pending')).toBe('body')
19
+ expect(todoTone('in_progress')).toBe('active')
20
+ })
21
+ })
@@ -0,0 +1,9 @@
1
+ import type { TodoItem } from '../types.js'
2
+
3
+ export type TodoTone = 'active' | 'body' | 'dim'
4
+
5
+ export const todoGlyph = (status: TodoItem['status']) =>
6
+ status === 'completed' ? '[x]' : status === 'cancelled' ? '[-]' : status === 'in_progress' ? '[>]' : '[ ]'
7
+
8
+ export const todoTone = (status: TodoItem['status']): TodoTone =>
9
+ status === 'in_progress' ? 'active' : status === 'pending' ? 'body' : 'dim'
@@ -0,0 +1,124 @@
1
+ import type { ScrollBoxHandle } from '@nastechai/ink'
2
+ import type { RefObject } from 'react'
3
+ import { useCallback, useMemo, useSyncExternalStore } from 'react'
4
+
5
+ export interface ViewportSnapshot {
6
+ atBottom: boolean
7
+ bottom: number
8
+ pending: number
9
+ scrollHeight: number
10
+ top: number
11
+ viewportHeight: number
12
+ }
13
+
14
+ export interface ScrollbarSnapshot {
15
+ scrollHeight: number
16
+ top: number
17
+ viewportHeight: number
18
+ }
19
+
20
+ const EMPTY: ViewportSnapshot = {
21
+ atBottom: true,
22
+ bottom: 0,
23
+ pending: 0,
24
+ scrollHeight: 0,
25
+ top: 0,
26
+ viewportHeight: 0
27
+ }
28
+
29
+ const EMPTY_SCROLLBAR: ScrollbarSnapshot = {
30
+ scrollHeight: 0,
31
+ top: 0,
32
+ viewportHeight: 0
33
+ }
34
+
35
+ export function getViewportSnapshot(s?: ScrollBoxHandle | null): ViewportSnapshot {
36
+ if (!s) {
37
+ return EMPTY
38
+ }
39
+
40
+ const pending = s.getPendingDelta()
41
+ const top = Math.max(0, s.getScrollTop() + pending)
42
+ const viewportHeight = Math.max(0, s.getViewportHeight())
43
+ const cachedScrollHeight = Math.max(viewportHeight, s.getScrollHeight())
44
+ let scrollHeight = cachedScrollHeight
45
+ const bottom = top + viewportHeight
46
+ let atBottom = s.isSticky() || bottom >= scrollHeight - 2
47
+
48
+ if (!atBottom) {
49
+ scrollHeight = Math.max(viewportHeight, s.getFreshScrollHeight?.() ?? cachedScrollHeight)
50
+ atBottom = s.isSticky() || bottom >= scrollHeight - 2
51
+ }
52
+
53
+ return {
54
+ atBottom,
55
+ bottom,
56
+ pending,
57
+ scrollHeight,
58
+ top,
59
+ viewportHeight
60
+ }
61
+ }
62
+
63
+ export function viewportSnapshotKey(v: ViewportSnapshot) {
64
+ return `${v.atBottom ? 1 : 0}:${Math.ceil(v.top / 8) * 8}:${v.viewportHeight}:${Math.ceil(v.scrollHeight / 8) * 8}:${v.pending}`
65
+ }
66
+
67
+ export function getScrollbarSnapshot(s?: ScrollBoxHandle | null): ScrollbarSnapshot {
68
+ if (!s) {
69
+ return EMPTY_SCROLLBAR
70
+ }
71
+
72
+ const viewportHeight = Math.max(0, s.getViewportHeight())
73
+ const scrollHeight = Math.max(viewportHeight, s.getScrollHeight())
74
+ const maxTop = Math.max(0, scrollHeight - viewportHeight)
75
+
76
+ return {
77
+ scrollHeight,
78
+ top: Math.max(0, Math.min(maxTop, s.getScrollTop())),
79
+ viewportHeight
80
+ }
81
+ }
82
+
83
+ export function scrollbarSnapshotKey(v: ScrollbarSnapshot) {
84
+ return `${v.top}:${v.viewportHeight}:${v.scrollHeight}`
85
+ }
86
+
87
+ export function useViewportSnapshot(scrollRef: RefObject<ScrollBoxHandle | null>): ViewportSnapshot {
88
+ const key = useSyncExternalStore(
89
+ useCallback((cb: () => void) => scrollRef.current?.subscribe(cb) ?? (() => {}), [scrollRef]),
90
+ () => viewportSnapshotKey(getViewportSnapshot(scrollRef.current)),
91
+ () => viewportSnapshotKey(EMPTY)
92
+ )
93
+
94
+ return useMemo(() => {
95
+ const [atBottom = '1', top = '0', viewportHeight = '0', scrollHeight = '0', pending = '0'] = key.split(':')
96
+
97
+ return {
98
+ atBottom: atBottom === '1',
99
+ bottom: Number(top) + Number(viewportHeight),
100
+ pending: Number(pending),
101
+ scrollHeight: Number(scrollHeight),
102
+ top: Number(top),
103
+ viewportHeight: Number(viewportHeight)
104
+ }
105
+ }, [key])
106
+ }
107
+
108
+ export function useScrollbarSnapshot(scrollRef: RefObject<ScrollBoxHandle | null>): ScrollbarSnapshot {
109
+ const key = useSyncExternalStore(
110
+ useCallback((cb: () => void) => scrollRef.current?.subscribe(cb) ?? (() => {}), [scrollRef]),
111
+ () => scrollbarSnapshotKey(getScrollbarSnapshot(scrollRef.current)),
112
+ () => scrollbarSnapshotKey(EMPTY_SCROLLBAR)
113
+ )
114
+
115
+ return useMemo(() => {
116
+ const [top = '0', viewportHeight = '0', scrollHeight = '0'] = key.split(':')
117
+
118
+ return {
119
+ scrollHeight: Number(scrollHeight),
120
+ top: Number(top),
121
+ viewportHeight: Number(viewportHeight)
122
+ }
123
+ }, [key])
124
+ }
@@ -0,0 +1,145 @@
1
+ import { TERMUX_TUI_MODE } from '../config/env.js'
2
+ import type { Msg } from '../types.js'
3
+
4
+ import { transcriptBodyWidth } from './inputMetrics.js'
5
+
6
+ const hashText = (text: string) => {
7
+ let h = 5381
8
+
9
+ for (let i = 0; i < text.length; i++) {
10
+ h = ((h << 5) + h) ^ text.charCodeAt(i)
11
+ }
12
+
13
+ return (h >>> 0).toString(36)
14
+ }
15
+
16
+ export const messageHeightKey = (msg: Msg) => {
17
+ const todoSig = msg.todos?.map(t => `${t.status}:${t.content}`).join('\u0001') ?? ''
18
+
19
+ const panelSig =
20
+ msg.panelData?.sections
21
+ .map(s => `${s.title ?? ''}:${s.text?.length ?? 0}:${s.items?.length ?? 0}:${s.rows?.length ?? 0}`)
22
+ .join('\u0001') ?? ''
23
+
24
+ const introSig = msg.kind === 'intro' ? (msg.info?.version ?? '') : ''
25
+
26
+ return [
27
+ msg.role,
28
+ msg.kind ?? '',
29
+ hashText([msg.text, msg.thinking ?? '', msg.tools?.join('\n') ?? '', todoSig, panelSig, introSig].join('\0'))
30
+ ].join(':')
31
+ }
32
+
33
+ // Hard cap on rows the estimator will count. Each row above this is
34
+ // invisible to the estimator (gets clipped to MAX_ESTIMATE_LINES), but
35
+ // post-mount Yoga measurement converges to the real height on first
36
+ // render. Without this, a long assistant turn (10k+ chars) costs O(text)
37
+ // per offset rebuild × every uncached item — cold-mounting a 1000-row
38
+ // transcript becomes a multi-million-char wrap walk that blocks the UI.
39
+ //
40
+ // 800 covers any realistic assistant message (the prior history-clip
41
+ // ceiling was 16 lines, then full text — this is the sane middle).
42
+ const MAX_ESTIMATE_LINES = 800
43
+
44
+ export const wrappedLines = (text: string, width: number, maxLines: number = MAX_ESTIMATE_LINES) => {
45
+ const w = Math.max(1, width)
46
+ // Worst case: every cell is its own row at width=1, plus a small
47
+ // slack for the trailing partial line. Walking past this byte budget
48
+ // cannot increase n any further once n is already past maxLines, so
49
+ // bail. Saves O(text) walks on multi-megabyte single-line messages.
50
+ const budget = Math.min(text.length, maxLines * w + maxLines)
51
+ let n = 0
52
+ let start = 0
53
+
54
+ for (let i = 0; i <= budget; i++) {
55
+ if (i === text.length || i === budget || text.charCodeAt(i) === 10) {
56
+ const rows = Math.max(1, Math.ceil((i - start) / w))
57
+ n += rows >= maxLines - n ? maxLines - n : rows
58
+ start = i + 1
59
+
60
+ if (n >= maxLines) {
61
+ return maxLines
62
+ }
63
+ }
64
+ }
65
+
66
+ return n
67
+ }
68
+
69
+ export const estimatedMsgHeight = (
70
+ msg: Msg,
71
+ cols: number,
72
+ {
73
+ compact,
74
+ details,
75
+ thinkingVisible = details,
76
+ toolsVisible = details,
77
+ userPrompt = '',
78
+ withSeparator = false
79
+ }: {
80
+ compact: boolean
81
+ details: boolean
82
+ thinkingVisible?: boolean
83
+ toolsVisible?: boolean
84
+ userPrompt?: string
85
+ withSeparator?: boolean
86
+ }
87
+ ) => {
88
+ if (msg.kind === 'intro') {
89
+ return msg.info?.version ? 9 : 5
90
+ }
91
+
92
+ if (msg.kind === 'panel') {
93
+ return Math.max(3, (msg.panelData?.sections.length ?? 1) * 2 + 1)
94
+ }
95
+
96
+ if (msg.kind === 'trail' && msg.todos?.length) {
97
+ if (msg.todoCollapsedByDefault) {
98
+ return 2
99
+ }
100
+
101
+ return Math.max(2, msg.todos.length + 2)
102
+ }
103
+
104
+ const bodyWidth = transcriptBodyWidth(cols, msg.role, userPrompt, TERMUX_TUI_MODE)
105
+ const text = msg.text
106
+ let h = wrappedLines(text || ' ', bodyWidth)
107
+
108
+ if (!compact && msg.role === 'assistant') {
109
+ // Paragraph gaps add up to 6 extra rows of breathing room. Slice
110
+ // first so the regex never walks more than the first ~16k chars of
111
+ // a giant assistant message — post-mount Yoga measurement converges
112
+ // to the real height regardless of how the estimate undercounts.
113
+ const scan = text.length > 16_000 ? text.slice(0, 16_000) : text
114
+ h += Math.min(6, (scan.match(/\n\s*\n/g) ?? []).length)
115
+ }
116
+
117
+ if (details) {
118
+ const hasVisibleTools = toolsVisible && Boolean(msg.tools?.length)
119
+ const hasVisibleThinking = thinkingVisible && /\S/.test(msg.thinking ?? '')
120
+ const hasVisibleDetails = hasVisibleTools || hasVisibleThinking
121
+
122
+ if (hasVisibleDetails) {
123
+ h += (hasVisibleTools ? (msg.tools?.length ?? 0) : 0) + (hasVisibleThinking ? wrappedLines(msg.thinking ?? '', bodyWidth) : 0)
124
+
125
+ if (msg.role === 'assistant' && /\S/.test(msg.text)) {
126
+ h += 2
127
+ }
128
+ }
129
+ }
130
+
131
+ if (msg.role === 'user' || msg.kind === 'diff') {
132
+ h += 2
133
+ } else if (msg.kind === 'slash') {
134
+ h++
135
+ }
136
+
137
+ // Inter-turn separator above non-first user messages (1 rule row + 1
138
+ // top-margin row). The render-side gate is in appLayout.tsx; we trust
139
+ // the caller to pass `withSeparator` only when it matches that gate.
140
+ if (withSeparator) {
141
+ h += 2
142
+ }
143
+
144
+ return Math.max(1, h)
145
+ }