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,845 @@
1
+ import { type AnsiCode, type StyledChar, styledCharsFromTokens, tokenize } from '@alcalzone/ansi-tokenize'
2
+
3
+ import { logForDebugging } from '../utils/debug.js'
4
+ import { getGraphemeSegmenter } from '../utils/intl.js'
5
+ import sliceAnsi from '../utils/sliceAnsi.js'
6
+
7
+ import { reorderBidi } from './bidi.js'
8
+ import { type Rectangle, unionRect } from './layout/geometry.js'
9
+ import {
10
+ blitRegion,
11
+ CellWidth,
12
+ extractHyperlinkFromStyles,
13
+ filterOutHyperlinkStyles,
14
+ markNoSelectRegion,
15
+ OSC8_PREFIX,
16
+ resetScreen,
17
+ type Screen,
18
+ setCellAt,
19
+ shiftRows,
20
+ type StylePool
21
+ } from './screen.js'
22
+ import { stringWidth } from './stringWidth.js'
23
+ import { widestLine } from './widest-line.js'
24
+
25
+ /**
26
+ * A grapheme cluster with precomputed terminal width, styleId, and hyperlink.
27
+ * Built once per unique line (cached via charCache), so the per-char hot loop
28
+ * is just property reads + setCellAt — no stringWidth, no style interning,
29
+ * no hyperlink extraction per frame.
30
+ *
31
+ * styleId is safe to cache: StylePool is session-lived (never reset).
32
+ * hyperlink is stored as a string (not interned ID) since hyperlinkPool
33
+ * resets every 5 min; setCellAt interns it per-frame (cheap Map.get).
34
+ */
35
+ type ClusteredChar = {
36
+ value: string
37
+ width: number
38
+ styleId: number
39
+ hyperlink: string | undefined
40
+ }
41
+
42
+ /**
43
+ * Collects write/blit/clear/clip operations from the render tree, then
44
+ * applies them to a Screen buffer in `get()`. The Screen is what gets
45
+ * diffed against the previous frame to produce terminal updates.
46
+ */
47
+
48
+ type Options = {
49
+ width: number
50
+ height: number
51
+ stylePool: StylePool
52
+ /**
53
+ * Screen to render into. Will be reset before use.
54
+ * For double-buffering, pass a reusable screen. Otherwise create a new one.
55
+ */
56
+ screen: Screen
57
+ }
58
+
59
+ export type Operation =
60
+ | WriteOperation
61
+ | ClipOperation
62
+ | UnclipOperation
63
+ | BlitOperation
64
+ | ClearOperation
65
+ | NoSelectOperation
66
+ | ShiftOperation
67
+
68
+ type WriteOperation = {
69
+ type: 'write'
70
+ x: number
71
+ y: number
72
+ text: string
73
+ /**
74
+ * Per-line soft-wrap flags, parallel to text.split('\n'). softWrap[i]=true
75
+ * means line i is a continuation of line i-1 (the `\n` before it was
76
+ * inserted by word-wrap, not in the source). Index 0 is always false.
77
+ * Undefined means the producer didn't track wrapping (e.g. fills,
78
+ * raw-ansi) — the screen's per-row bitmap is left untouched.
79
+ */
80
+ softWrap?: boolean[]
81
+ }
82
+
83
+ type ClipOperation = {
84
+ type: 'clip'
85
+ clip: Clip
86
+ }
87
+
88
+ export type Clip = {
89
+ x1: number | undefined
90
+ x2: number | undefined
91
+ y1: number | undefined
92
+ y2: number | undefined
93
+ }
94
+
95
+ /**
96
+ * Intersect two clips. `undefined` on an axis means unbounded; the other
97
+ * clip's bound wins. If both are bounded, take the tighter constraint
98
+ * (max of mins, min of maxes). If the resulting region is empty
99
+ * (x1 >= x2 or y1 >= y2), writes clipped by it will be dropped.
100
+ */
101
+ function intersectClip(parent: Clip | undefined, child: Clip): Clip {
102
+ if (!parent) {
103
+ return child
104
+ }
105
+
106
+ return {
107
+ x1: maxDefined(parent.x1, child.x1),
108
+ x2: minDefined(parent.x2, child.x2),
109
+ y1: maxDefined(parent.y1, child.y1),
110
+ y2: minDefined(parent.y2, child.y2)
111
+ }
112
+ }
113
+
114
+ function maxDefined(a: number | undefined, b: number | undefined): number | undefined {
115
+ if (a === undefined) {
116
+ return b
117
+ }
118
+
119
+ if (b === undefined) {
120
+ return a
121
+ }
122
+
123
+ return Math.max(a, b)
124
+ }
125
+
126
+ function minDefined(a: number | undefined, b: number | undefined): number | undefined {
127
+ if (a === undefined) {
128
+ return b
129
+ }
130
+
131
+ if (b === undefined) {
132
+ return a
133
+ }
134
+
135
+ return Math.min(a, b)
136
+ }
137
+
138
+ type UnclipOperation = {
139
+ type: 'unclip'
140
+ }
141
+
142
+ type BlitOperation = {
143
+ type: 'blit'
144
+ src: Screen
145
+ x: number
146
+ y: number
147
+ width: number
148
+ height: number
149
+ }
150
+
151
+ type ShiftOperation = {
152
+ type: 'shift'
153
+ top: number
154
+ bottom: number
155
+ n: number
156
+ }
157
+
158
+ type ClearOperation = {
159
+ type: 'clear'
160
+ region: Rectangle
161
+ /**
162
+ * Set when the clear is for an absolute-positioned node's old bounds.
163
+ * Absolute nodes overlay normal-flow siblings, so their stale paint is
164
+ * what an earlier sibling's clean-subtree blit wrongly restores from
165
+ * prevScreen. Normal-flow siblings' clears don't have this problem —
166
+ * their old position can't have been painted on top of a sibling.
167
+ */
168
+ fromAbsolute?: boolean
169
+ }
170
+
171
+ type NoSelectOperation = {
172
+ type: 'noSelect'
173
+ region: Rectangle
174
+ }
175
+
176
+ export default class Output {
177
+ width: number
178
+ height: number
179
+ private readonly stylePool: StylePool
180
+ private screen: Screen
181
+
182
+ private readonly operations: Operation[] = []
183
+
184
+ private charCache: Map<string, ClusteredChar[]> = new Map()
185
+
186
+ constructor(options: Options) {
187
+ const { width, height, stylePool, screen } = options
188
+
189
+ this.width = width
190
+ this.height = height
191
+ this.stylePool = stylePool
192
+ this.screen = screen
193
+
194
+ resetScreen(screen, width, height)
195
+ }
196
+
197
+ /**
198
+ * Reuse this Output for a new frame. Zeroes the screen buffer, clears
199
+ * the operation list (backing storage is retained), and caps charCache
200
+ * growth. Preserving charCache across frames is the main win — most
201
+ * lines don't change between renders, so tokenize + grapheme clustering
202
+ * becomes a cache hit.
203
+ */
204
+ reset(width: number, height: number, screen: Screen): void {
205
+ this.width = width
206
+ this.height = height
207
+ this.screen = screen
208
+ this.operations.length = 0
209
+ resetScreen(screen, width, height)
210
+
211
+ if (this.charCache.size > 16384) {
212
+ this.charCache.clear()
213
+ }
214
+ }
215
+
216
+ /**
217
+ * Copy cells from a source screen region (blit = block image transfer).
218
+ */
219
+ blit(src: Screen, x: number, y: number, width: number, height: number): void {
220
+ this.operations.push({ type: 'blit', src, x, y, width, height })
221
+ }
222
+
223
+ /**
224
+ * Shift full-width rows within [top, bottom] by n. n > 0 = up. Mirrors
225
+ * what DECSTBM + SU/SD does to the terminal. Paired with blit() to reuse
226
+ * prevScreen content during pure scroll, avoiding full child re-render.
227
+ */
228
+ shift(top: number, bottom: number, n: number): void {
229
+ this.operations.push({ type: 'shift', top, bottom, n })
230
+ }
231
+
232
+ /**
233
+ * Clear a region by writing empty cells. Used when a node shrinks to
234
+ * ensure stale content from the previous frame is removed.
235
+ */
236
+ clear(region: Rectangle, fromAbsolute?: boolean): void {
237
+ this.operations.push({ type: 'clear', region, fromAbsolute })
238
+ }
239
+
240
+ /**
241
+ * Mark a region as non-selectable (excluded from fullscreen text
242
+ * selection copy + highlight). Used by <NoSelect> to fence off
243
+ * gutters (line numbers, diff sigils). Applied AFTER blit/write so
244
+ * the mark wins regardless of what's blitted into the region.
245
+ */
246
+ noSelect(region: Rectangle): void {
247
+ this.operations.push({ type: 'noSelect', region })
248
+ }
249
+
250
+ write(x: number, y: number, text: string, softWrap?: boolean[]): void {
251
+ if (!text) {
252
+ return
253
+ }
254
+
255
+ this.operations.push({
256
+ type: 'write',
257
+ x,
258
+ y,
259
+ text,
260
+ softWrap
261
+ })
262
+ }
263
+
264
+ clip(clip: Clip) {
265
+ this.operations.push({
266
+ type: 'clip',
267
+ clip
268
+ })
269
+ }
270
+
271
+ unclip() {
272
+ this.operations.push({
273
+ type: 'unclip'
274
+ })
275
+ }
276
+
277
+ get(): Screen {
278
+ const screen = this.screen
279
+ const screenWidth = this.width
280
+ const screenHeight = this.height
281
+
282
+ // Track blit vs write cell counts for debugging
283
+ let blitCells = 0
284
+ let writeCells = 0
285
+
286
+ // Pass 1: expand damage to cover clear regions. The buffer is freshly
287
+ // zeroed by resetScreen, so this pass only marks damage so diff()
288
+ // checks these regions against the previous frame.
289
+ //
290
+ // Also collect clears from absolute-positioned nodes. An absolute
291
+ // node overlays normal-flow siblings; when it shrinks, its clear is
292
+ // pushed AFTER those siblings' clean-subtree blits (DOM order). The
293
+ // blit copies the absolute node's own stale paint from prevScreen,
294
+ // and since clear is damage-only, the ghost survives diff. Normal-
295
+ // flow clears don't need this — a normal-flow node's old position
296
+ // can't have been painted on top of a sibling's current position.
297
+ const absoluteClears: Rectangle[] = []
298
+
299
+ for (const operation of this.operations) {
300
+ if (operation.type !== 'clear') {
301
+ continue
302
+ }
303
+
304
+ const { x, y, width, height } = operation.region
305
+ const startX = Math.max(0, x)
306
+ const startY = Math.max(0, y)
307
+ const maxX = Math.min(x + width, screenWidth)
308
+ const maxY = Math.min(y + height, screenHeight)
309
+
310
+ if (startX >= maxX || startY >= maxY) {
311
+ continue
312
+ }
313
+
314
+ const rect = {
315
+ x: startX,
316
+ y: startY,
317
+ width: maxX - startX,
318
+ height: maxY - startY
319
+ }
320
+
321
+ screen.damage = screen.damage ? unionRect(screen.damage, rect) : rect
322
+
323
+ if (operation.fromAbsolute) {
324
+ absoluteClears.push(rect)
325
+ }
326
+ }
327
+
328
+ const clips: Clip[] = []
329
+
330
+ for (const operation of this.operations) {
331
+ switch (operation.type) {
332
+ case 'clear':
333
+ // handled in pass 1
334
+ continue
335
+
336
+ case 'clip':
337
+ // Intersect with the parent clip (if any) so nested
338
+ // overflow:hidden boxes can't write outside their ancestor's
339
+ // clip region. Without this, a message with overflow:hidden at
340
+ // the bottom of a scrollbox pushes its OWN clip (based on its
341
+ // layout bounds, already translated by -scrollTop) which can
342
+ // extend below the scrollbox viewport — writes escape into
343
+ // the sibling bottom section's rows.
344
+ clips.push(intersectClip(clips.at(-1), operation.clip))
345
+
346
+ continue
347
+
348
+ case 'unclip':
349
+ clips.pop()
350
+
351
+ continue
352
+ case 'blit': {
353
+ // Bulk-copy cells from source screen region using TypedArray.set().
354
+ // Tracking damage ensures diff() checks blitted cells for stale content
355
+ // when a parent blits an area that previously contained child content.
356
+ const { src, x: regionX, y: regionY, width: regionWidth, height: regionHeight } = operation
357
+
358
+ // Intersect with active clip — a child's clean-blit passes its full
359
+ // cached rect, but the parent ScrollBox may have shrunk (pill mount).
360
+ // Without this, the blit writes past the ScrollBox's new bottom edge
361
+ // into the pill's row.
362
+ const clip = clips.at(-1)
363
+ const startX = Math.max(regionX, clip?.x1 ?? 0)
364
+ const startY = Math.max(regionY, clip?.y1 ?? 0)
365
+
366
+ const maxY = Math.min(regionY + regionHeight, screenHeight, src.height, clip?.y2 ?? Infinity)
367
+
368
+ const maxX = Math.min(regionX + regionWidth, screenWidth, src.width, clip?.x2 ?? Infinity)
369
+
370
+ if (startX >= maxX || startY >= maxY) {
371
+ continue
372
+ }
373
+
374
+ // Exclude cells covered by an absolute-positioned node's clear.
375
+ // Absolute nodes overlay normal-flow siblings, so prevScreen in
376
+ // that region holds stale overlay paint. If we blit those cells
377
+ // back, removed/moved overlays ghost as a duplicate.
378
+ if (absoluteClears.length === 0) {
379
+ blitRegion(screen, src, startX, startY, maxX, maxY)
380
+ blitCells += (maxY - startY) * (maxX - startX)
381
+
382
+ continue
383
+ }
384
+
385
+ for (let row = startY; row < maxY; row++) {
386
+ let spans: [number, number][] = [[startX, maxX]]
387
+
388
+ for (const r of absoluteClears) {
389
+ if (row < r.y || row >= r.y + r.height || !spans.length) {
390
+ break
391
+ }
392
+
393
+ const cs = Math.max(startX, r.x)
394
+ const ce = Math.min(maxX, r.x + r.width)
395
+
396
+ if (cs >= ce) {
397
+ continue
398
+ }
399
+
400
+ const next: [number, number][] = []
401
+
402
+ for (const [sx, ex] of spans) {
403
+ if (ce <= sx || cs >= ex) {
404
+ next.push([sx, ex])
405
+
406
+ continue
407
+ }
408
+
409
+ if (sx < cs) {
410
+ next.push([sx, cs])
411
+ }
412
+
413
+ if (ce < ex) {
414
+ next.push([ce, ex])
415
+ }
416
+ }
417
+
418
+ spans = next
419
+ }
420
+
421
+ for (const [sx, ex] of spans) {
422
+ blitRegion(screen, src, sx, row, ex, row + 1)
423
+ blitCells += ex - sx
424
+ }
425
+ }
426
+
427
+ continue
428
+ }
429
+
430
+ case 'shift': {
431
+ shiftRows(screen, operation.top, operation.bottom, operation.n)
432
+
433
+ continue
434
+ }
435
+
436
+ case 'write': {
437
+ const { text, softWrap } = operation
438
+ let { x, y } = operation
439
+ let lines = text.split('\n')
440
+ let swFrom = 0
441
+ let prevContentEnd = 0
442
+
443
+ const clip = clips.at(-1)
444
+
445
+ if (clip) {
446
+ const clipHorizontally = typeof clip?.x1 === 'number' && typeof clip?.x2 === 'number'
447
+
448
+ const clipVertically = typeof clip?.y1 === 'number' && typeof clip?.y2 === 'number'
449
+
450
+ // If text is positioned outside of clipping area altogether,
451
+ // skip to the next operation to avoid unnecessary calculations
452
+ if (clipHorizontally) {
453
+ const width = widestLine(text)
454
+
455
+ if (x + width <= clip.x1! || x >= clip.x2!) {
456
+ continue
457
+ }
458
+ }
459
+
460
+ if (clipVertically) {
461
+ const height = lines.length
462
+
463
+ if (y + height <= clip.y1! || y >= clip.y2!) {
464
+ continue
465
+ }
466
+ }
467
+
468
+ if (clipHorizontally) {
469
+ lines = lines.map(line => {
470
+ const width = stringWidth(line)
471
+ const startsBefore = x < clip.x1!
472
+ const endsAfter = x + width > clip.x2!
473
+
474
+ // Fast path: line fits entirely within the clip box — skip
475
+ // tokenize/slice. Common case for transcript text where
476
+ // containers are wider than rendered content. CPU profile
477
+ // (Apr 2026): sliceAnsi at 18% total during scroll, mostly
478
+ // no-op (line, 0, width) slices.
479
+ if (!startsBefore && !endsAfter) {
480
+ return line
481
+ }
482
+
483
+ const from = startsBefore ? clip.x1! - x : 0
484
+ const to = endsAfter ? clip.x2! - x : width
485
+ let sliced = sliceAnsi(line, from, to)
486
+
487
+ // Wide chars (CJK, emoji) occupy 2 cells. When `to` lands
488
+ // on the first cell of a wide char, sliceAnsi includes the
489
+ // entire glyph and the result overflows clip.x2 by one cell,
490
+ // writing a SpacerTail into the adjacent sibling. Re-slice
491
+ // one cell earlier; wide chars are exactly 2 cells, so a
492
+ // single retry always fits.
493
+ if (stringWidth(sliced) > to - from) {
494
+ sliced = sliceAnsi(line, from, to - 1)
495
+ }
496
+
497
+ return sliced
498
+ })
499
+
500
+ if (x < clip.x1!) {
501
+ x = clip.x1!
502
+ }
503
+ }
504
+
505
+ if (clipVertically) {
506
+ const from = y < clip.y1! ? clip.y1! - y : 0
507
+ const height = lines.length
508
+ const to = y + height > clip.y2! ? clip.y2! - y : height
509
+
510
+ // If the first visible line is a soft-wrap continuation, we
511
+ // need the clipped previous line's content end so
512
+ // screen.softWrap[lineY] correctly records the join point
513
+ // even though that line's cells were never written.
514
+ if (softWrap && from > 0 && softWrap[from] === true) {
515
+ prevContentEnd = x + stringWidth(lines[from - 1]!)
516
+ }
517
+
518
+ lines = lines.slice(from, to)
519
+ swFrom = from
520
+
521
+ if (y < clip.y1!) {
522
+ y = clip.y1!
523
+ }
524
+ }
525
+ }
526
+
527
+ const swBits = screen.softWrap
528
+ let offsetY = 0
529
+
530
+ for (const line of lines) {
531
+ const lineY = y + offsetY
532
+
533
+ // Line can be outside screen if `text` is taller than screen height
534
+ if (lineY >= screenHeight) {
535
+ break
536
+ }
537
+
538
+ const contentEnd = writeLineToScreen(screen, line, x, lineY, screenWidth, this.stylePool, this.charCache)
539
+
540
+ writeCells += contentEnd - x
541
+
542
+ // See Screen.softWrap docstring for the encoding. contentEnd
543
+ // from writeLineToScreen is tab-expansion-aware, unlike
544
+ // x+stringWidth(line) which treats tabs as width 0.
545
+ if (softWrap) {
546
+ const isSW = softWrap[swFrom + offsetY] === true
547
+ swBits[lineY] = isSW ? prevContentEnd : 0
548
+ prevContentEnd = contentEnd
549
+ }
550
+
551
+ offsetY++
552
+ }
553
+
554
+ continue
555
+ }
556
+ }
557
+ }
558
+
559
+ // noSelect ops go LAST so they win over blits (which copy noSelect
560
+ // from prevScreen) and writes (which don't touch noSelect). This way
561
+ // a <NoSelect> box correctly fences its region even when the parent
562
+ // blits, and moving a <NoSelect> between frames correctly clears the
563
+ // old region (resetScreen already zeroed the bitmap).
564
+ for (const operation of this.operations) {
565
+ if (operation.type === 'noSelect') {
566
+ const { x, y, width, height } = operation.region
567
+ markNoSelectRegion(screen, x, y, width, height)
568
+ }
569
+ }
570
+
571
+ // Log blit/write ratio for debugging - high write count suggests blitting isn't working
572
+ const totalCells = blitCells + writeCells
573
+
574
+ if (totalCells > 1000 && writeCells > blitCells) {
575
+ logForDebugging(
576
+ `High write ratio: blit=${blitCells}, write=${writeCells} (${((writeCells / totalCells) * 100).toFixed(1)}% writes), screen=${screenHeight}x${screenWidth}`
577
+ )
578
+ }
579
+
580
+ return screen
581
+ }
582
+ }
583
+
584
+ function stylesEqual(a: AnsiCode[], b: AnsiCode[]): boolean {
585
+ if (a === b) {
586
+ return true
587
+ } // Reference equality fast path
588
+
589
+ const len = a.length
590
+
591
+ if (len !== b.length) {
592
+ return false
593
+ }
594
+
595
+ if (len === 0) {
596
+ return true
597
+ } // Both empty
598
+
599
+ for (let i = 0; i < len; i++) {
600
+ if (a[i]!.code !== b[i]!.code) {
601
+ return false
602
+ }
603
+ }
604
+
605
+ return true
606
+ }
607
+
608
+ /**
609
+ * Convert a string with ANSI codes into styled characters with proper grapheme
610
+ * clustering. Fixes ansi-tokenize splitting grapheme clusters (like family
611
+ * emojis) into individual code points.
612
+ *
613
+ * Also precomputes styleId + hyperlink per style run (not per char) — an
614
+ * 80-char line with 3 style runs does 3 intern calls instead of 80.
615
+ */
616
+ function styledCharsWithGraphemeClustering(chars: StyledChar[], stylePool: StylePool): ClusteredChar[] {
617
+ const charCount = chars.length
618
+
619
+ if (charCount === 0) {
620
+ return []
621
+ }
622
+
623
+ const result: ClusteredChar[] = []
624
+ const bufferChars: string[] = []
625
+ let bufferStyles: AnsiCode[] = chars[0]!.styles
626
+
627
+ for (let i = 0; i < charCount; i++) {
628
+ const char = chars[i]!
629
+ const styles = char.styles
630
+
631
+ // Different styles means we need to flush and start new buffer
632
+ if (bufferChars.length > 0 && !stylesEqual(styles, bufferStyles)) {
633
+ flushBuffer(bufferChars.join(''), bufferStyles, stylePool, result)
634
+ bufferChars.length = 0
635
+ }
636
+
637
+ bufferChars.push(char.value)
638
+ bufferStyles = styles
639
+ }
640
+
641
+ // Final flush
642
+ if (bufferChars.length > 0) {
643
+ flushBuffer(bufferChars.join(''), bufferStyles, stylePool, result)
644
+ }
645
+
646
+ return result
647
+ }
648
+
649
+ function flushBuffer(buffer: string, styles: AnsiCode[], stylePool: StylePool, out: ClusteredChar[]): void {
650
+ // Compute styleId + hyperlink ONCE for the whole style run.
651
+ // Every grapheme in this buffer shares the same styles.
652
+ //
653
+ // Extract and track hyperlinks separately, filter from styles.
654
+ // Always check for OSC 8 codes to filter, not just when a URL is
655
+ // extracted. The tokenizer treats OSC 8 close codes (empty URL) as
656
+ // active styles, so they must be filtered even when no hyperlink
657
+ // URL is present.
658
+ const hyperlink = extractHyperlinkFromStyles(styles) ?? undefined
659
+
660
+ const hasOsc8Styles =
661
+ hyperlink !== undefined || styles.some(s => s.code.length >= OSC8_PREFIX.length && s.code.startsWith(OSC8_PREFIX))
662
+
663
+ const filteredStyles = hasOsc8Styles ? filterOutHyperlinkStyles(styles) : styles
664
+
665
+ const styleId = stylePool.intern(filteredStyles)
666
+
667
+ for (const { segment: grapheme } of getGraphemeSegmenter().segment(buffer)) {
668
+ out.push({
669
+ value: grapheme,
670
+ width: stringWidth(grapheme),
671
+ styleId,
672
+ hyperlink
673
+ })
674
+ }
675
+ }
676
+
677
+ /**
678
+ * Write a single line's characters into the screen buffer.
679
+ * Extracted from Output.get() so JSC can optimize this tight,
680
+ * monomorphic loop independently — better register allocation,
681
+ * setCellAt inlining, and type feedback than when buried inside
682
+ * a 300-line dispatch function.
683
+ *
684
+ * Returns the end column (x + visual width, including tab expansion) so
685
+ * the caller can record it in screen.softWrap without re-walking the
686
+ * line via stringWidth(). Caller computes the debug cell-count as end-x.
687
+ */
688
+ function writeLineToScreen(
689
+ screen: Screen,
690
+ line: string,
691
+ x: number,
692
+ y: number,
693
+ screenWidth: number,
694
+ stylePool: StylePool,
695
+ charCache: Map<string, ClusteredChar[]>
696
+ ): number {
697
+ let characters = charCache.get(line)
698
+
699
+ if (!characters) {
700
+ characters = reorderBidi(styledCharsWithGraphemeClustering(styledCharsFromTokens(tokenize(line)), stylePool))
701
+ charCache.set(line, characters)
702
+ }
703
+
704
+ let offsetX = x
705
+
706
+ for (let charIdx = 0; charIdx < characters.length; charIdx++) {
707
+ const character = characters[charIdx]!
708
+ const codePoint = character.value.codePointAt(0)
709
+
710
+ // Handle C0 control characters (0x00-0x1F) that cause cursor movement
711
+ // mismatches. stringWidth treats these as width 0, but terminals may
712
+ // move the cursor differently.
713
+ if (codePoint !== undefined && codePoint <= 0x1f) {
714
+ // Tab (0x09): expand to spaces to reach next tab stop
715
+ if (codePoint === 0x09) {
716
+ const tabWidth = 8
717
+ const spacesToNextStop = tabWidth - (offsetX % tabWidth)
718
+
719
+ for (let i = 0; i < spacesToNextStop && offsetX < screenWidth; i++) {
720
+ setCellAt(screen, offsetX, y, {
721
+ char: ' ',
722
+ styleId: stylePool.none,
723
+ width: CellWidth.Narrow,
724
+ hyperlink: undefined
725
+ })
726
+ offsetX++
727
+ }
728
+ }
729
+ // ESC (0x1B): skip incomplete escape sequences that ansi-tokenize
730
+ // didn't recognize. ansi-tokenize only parses SGR sequences (ESC[...m)
731
+ // and OSC 8 hyperlinks (ESC]8;;url BEL). Other sequences like cursor
732
+ // movement, screen clearing, or terminal title become individual char
733
+ // tokens that we need to skip here.
734
+ else if (codePoint === 0x1b) {
735
+ const nextChar = characters[charIdx + 1]?.value
736
+ const nextCode = nextChar?.codePointAt(0)
737
+
738
+ if (nextChar === '(' || nextChar === ')' || nextChar === '*' || nextChar === '+') {
739
+ // Charset selection: ESC ( X, ESC ) X, etc.
740
+ // Skip the intermediate char and the charset designator
741
+ charIdx += 2
742
+ } else if (nextChar === '[') {
743
+ // CSI sequence: ESC [ ... final-byte
744
+ // Final byte is in range 0x40-0x7E (@, A-Z, [\]^_`, a-z, {|}~)
745
+ // Examples: ESC[2J (clear), ESC[?25l (cursor hide), ESC[H (home)
746
+ charIdx++ // skip the [
747
+
748
+ while (charIdx < characters.length - 1) {
749
+ charIdx++
750
+ const c = characters[charIdx]?.value.codePointAt(0)
751
+
752
+ // Final byte terminates the sequence
753
+ if (c !== undefined && c >= 0x40 && c <= 0x7e) {
754
+ break
755
+ }
756
+ }
757
+ } else if (nextChar === ']' || nextChar === 'P' || nextChar === '_' || nextChar === '^' || nextChar === 'X') {
758
+ // String-based sequences terminated by BEL (0x07) or ST (ESC \):
759
+ // - OSC: ESC ] ... (Operating System Command)
760
+ // - DCS: ESC P ... (Device Control String)
761
+ // - APC: ESC _ ... (Application Program Command)
762
+ // - PM: ESC ^ ... (Privacy Message)
763
+ // - SOS: ESC X ... (Start of String)
764
+ charIdx++ // skip the introducer char
765
+
766
+ while (charIdx < characters.length - 1) {
767
+ charIdx++
768
+ const c = characters[charIdx]?.value
769
+
770
+ // BEL (0x07) terminates the sequence
771
+ if (c === '\x07') {
772
+ break
773
+ }
774
+
775
+ // ST (String Terminator) is ESC \
776
+ // When we see ESC, check if next char is backslash
777
+ if (c === '\x1b') {
778
+ const nextC = characters[charIdx + 1]?.value
779
+
780
+ if (nextC === '\\') {
781
+ charIdx++ // skip the backslash too
782
+
783
+ break
784
+ }
785
+ }
786
+ }
787
+ } else if (nextCode !== undefined && nextCode >= 0x30 && nextCode <= 0x7e) {
788
+ // Single-character escape sequences: ESC followed by 0x30-0x7E
789
+ // (excluding the multi-char introducers already handled above)
790
+ // - Fp range (0x30-0x3F): ESC 7 (save cursor), ESC 8 (restore)
791
+ // - Fe range (0x40-0x5F): ESC D (index), ESC M (reverse index)
792
+ // - Fs range (0x60-0x7E): ESC c (reset)
793
+ charIdx++ // skip the command char
794
+ }
795
+ }
796
+
797
+ // Carriage return (0x0D): would move cursor to column 0, skip it
798
+ // Backspace (0x08): would move cursor left, skip it
799
+ // Bell (0x07), vertical tab (0x0B), form feed (0x0C): skip
800
+ // All other control chars (0x00-0x06, 0x0E-0x1F): skip
801
+ // Note: newline (0x0A) is already handled by line splitting
802
+ continue
803
+ }
804
+
805
+ // Zero-width characters (combining marks, ZWNJ, ZWS, etc.)
806
+ // don't occupy terminal cells — storing them as Narrow cells
807
+ // desyncs the virtual cursor from the real terminal cursor.
808
+ // Width was computed once during clustering (cached via charCache).
809
+ const charWidth = character.width
810
+
811
+ if (charWidth === 0) {
812
+ continue
813
+ }
814
+
815
+ const isWideCharacter = charWidth >= 2
816
+
817
+ // Wide char at last column can't fit — terminal would wrap it to
818
+ // the next line, desyncing our cursor model. Place a SpacerHead
819
+ // to mark the blank column, matching terminal behavior.
820
+ if (isWideCharacter && offsetX + 2 > screenWidth) {
821
+ setCellAt(screen, offsetX, y, {
822
+ char: ' ',
823
+ styleId: stylePool.none,
824
+ width: CellWidth.SpacerHead,
825
+ hyperlink: undefined
826
+ })
827
+ offsetX++
828
+
829
+ continue
830
+ }
831
+
832
+ // styleId + hyperlink were precomputed during clustering (once per
833
+ // style run, cached via charCache). Hot loop is now just property
834
+ // reads — no intern, no extract, no filter per frame.
835
+ setCellAt(screen, offsetX, y, {
836
+ char: character.value,
837
+ styleId: character.styleId,
838
+ width: isWideCharacter ? CellWidth.Wide : CellWidth.Narrow,
839
+ hyperlink: character.hyperlink
840
+ })
841
+ offsetX += isWideCharacter ? 2 : 1
842
+ }
843
+
844
+ return offsetX
845
+ }