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,101 @@
1
+ import { useContext, useMemo, useSyncExternalStore } from 'react'
2
+
3
+ import StdinContext from '../components/StdinContext.js'
4
+ import instances from '../instances.js'
5
+ import { type FocusMove, type SelectionState, shiftAnchor } from '../selection.js'
6
+
7
+ /**
8
+ * Access to text selection operations on the Ink instance (fullscreen only).
9
+ * Returns no-op functions when fullscreen mode is disabled.
10
+ */
11
+ export function useSelection(): {
12
+ copySelection: () => Promise<string>
13
+ /** Copy without clearing the highlight (for copy-on-select). */
14
+ copySelectionNoClear: () => Promise<string>
15
+ clearSelection: () => void
16
+ hasSelection: () => boolean
17
+ /** Read the raw mutable selection state (for drag-to-scroll). */
18
+ getState: () => SelectionState | null
19
+ /** Subscribe to selection mutations (start/update/finish/clear). */
20
+ subscribe: (cb: () => void) => () => void
21
+ /** Shift the anchor row by dRow, clamped to [minRow, maxRow]. */
22
+ shiftAnchor: (dRow: number, minRow: number, maxRow: number) => void
23
+ /** Shift anchor AND focus by dRow (keyboard scroll: whole selection
24
+ * tracks content). Clamped points get col reset to the full-width edge
25
+ * since their content was captured by captureScrolledRows. Reads
26
+ * screen.width from the ink instance for the col-reset boundary. */
27
+ shiftSelection: (dRow: number, minRow: number, maxRow: number) => void
28
+ /** Keyboard selection extension (shift+arrow): move focus, anchor fixed.
29
+ * Left/right wrap across rows; up/down clamp at viewport edges. */
30
+ moveFocus: (move: FocusMove) => void
31
+ /** Capture text from rows about to scroll out of the viewport (call
32
+ * BEFORE scrollBy so the screen buffer still has the outgoing rows). */
33
+ captureScrolledRows: (firstRow: number, lastRow: number, side: 'above' | 'below') => void
34
+ /** Set the selection highlight bg color (theme-piping; solid bg
35
+ * replaces the old SGR-7 inverse so syntax highlighting stays readable
36
+ * under selection). Call once on mount + whenever theme changes. */
37
+ setSelectionBgColor: (color: string) => void
38
+ /** Monotonic counter incremented on every selection mutation. */
39
+ version: () => number
40
+ } {
41
+ // Look up the Ink instance via stdout — same pattern as instances map.
42
+ // StdinContext is available (it's always provided), and the Ink instance
43
+ // is keyed by stdout which we can get from process.stdout since there's
44
+ // only one Ink instance per process in practice.
45
+ useContext(StdinContext) // anchor to App subtree for hook rules
46
+ const ink = instances.get(process.stdout)
47
+
48
+ // Memoize so callers can safely use the return value in dependency arrays.
49
+ // ink is a singleton per stdout — stable across renders.
50
+ return useMemo(() => {
51
+ if (!ink) {
52
+ return {
53
+ copySelection: async () => '',
54
+ copySelectionNoClear: async () => '',
55
+ clearSelection: () => {},
56
+ hasSelection: () => false,
57
+ getState: () => null,
58
+ subscribe: () => () => {},
59
+ shiftAnchor: () => {},
60
+ shiftSelection: () => {},
61
+ moveFocus: () => {},
62
+ captureScrolledRows: () => {},
63
+ setSelectionBgColor: () => {},
64
+ version: () => 0
65
+ }
66
+ }
67
+
68
+ return {
69
+ copySelection: () => ink.copySelection(),
70
+ copySelectionNoClear: () => ink.copySelectionNoClear(),
71
+ clearSelection: () => ink.clearTextSelection(),
72
+ hasSelection: () => ink.hasTextSelection(),
73
+ getState: () => ink.selection,
74
+ subscribe: (cb: () => void) => ink.subscribeToSelectionChange(cb),
75
+ shiftAnchor: (dRow: number, minRow: number, maxRow: number) => shiftAnchor(ink.selection, dRow, minRow, maxRow),
76
+ shiftSelection: (dRow, minRow, maxRow) => ink.shiftSelectionForScroll(dRow, minRow, maxRow),
77
+ moveFocus: (move: FocusMove) => ink.moveSelectionFocus(move),
78
+ captureScrolledRows: (firstRow, lastRow, side) => ink.captureScrolledRows(firstRow, lastRow, side),
79
+ setSelectionBgColor: (color: string) => ink.setSelectionBgColor(color),
80
+ version: () => ink.getSelectionVersion()
81
+ }
82
+ }, [ink])
83
+ }
84
+
85
+ const NO_SUBSCRIBE = () => () => {}
86
+ const ALWAYS_FALSE = () => false
87
+
88
+ /**
89
+ * Reactive selection-exists state. Re-renders the caller when a text
90
+ * selection is created or cleared. Always returns false outside
91
+ * fullscreen mode (selection is only available in alt-screen).
92
+ */
93
+ export function useHasSelection(): boolean {
94
+ useContext(StdinContext)
95
+ const ink = instances.get(process.stdout)
96
+
97
+ return useSyncExternalStore(
98
+ ink ? ink.subscribeToSelectionChange : NO_SUBSCRIBE,
99
+ ink ? ink.hasTextSelection : ALWAYS_FALSE
100
+ )
101
+ }
@@ -0,0 +1,9 @@
1
+ import { useContext } from 'react'
2
+
3
+ import StdinContext from '../components/StdinContext.js'
4
+
5
+ /**
6
+ * `useStdin` is a React hook, which exposes stdin stream.
7
+ */
8
+ const useStdin = () => useContext(StdinContext)
9
+ export default useStdin
@@ -0,0 +1,71 @@
1
+ import { useContext, useEffect, useRef } from 'react'
2
+
3
+ import { CLEAR_TAB_STATUS, supportsTabStatus, tabStatus, wrapForMultiplexer } from '../termio/osc.js'
4
+ import type { Color } from '../termio/types.js'
5
+ import { TerminalWriteContext } from '../useTerminalNotification.js'
6
+
7
+ export type TabStatusKind = 'idle' | 'busy' | 'waiting'
8
+
9
+ const rgb = (r: number, g: number, b: number): Color => ({
10
+ type: 'rgb',
11
+ r,
12
+ g,
13
+ b
14
+ })
15
+
16
+ // Per the OSC 21337 usage guide's suggested mapping.
17
+ const TAB_STATUS_PRESETS: Record<TabStatusKind, { indicator: Color; status: string; statusColor: Color }> = {
18
+ idle: {
19
+ indicator: rgb(0, 215, 95),
20
+ status: 'Idle',
21
+ statusColor: rgb(136, 136, 136)
22
+ },
23
+ busy: {
24
+ indicator: rgb(255, 149, 0),
25
+ status: 'Working…',
26
+ statusColor: rgb(255, 149, 0)
27
+ },
28
+ waiting: {
29
+ indicator: rgb(95, 135, 255),
30
+ status: 'Waiting',
31
+ statusColor: rgb(95, 135, 255)
32
+ }
33
+ }
34
+
35
+ /**
36
+ * Declaratively set the tab-status indicator (OSC 21337).
37
+ *
38
+ * Emits a colored dot + short status text to the tab sidebar. Terminals
39
+ * that don't support OSC 21337 discard the sequence silently, so this is
40
+ * safe to call unconditionally. Wrapped for tmux/screen passthrough.
41
+ *
42
+ * Pass `null` to opt out. If a status was previously set, transitioning to
43
+ * `null` emits CLEAR_TAB_STATUS so toggling off mid-session doesn't leave
44
+ * a stale dot. Process-exit cleanup is handled by ink.tsx's unmount path.
45
+ */
46
+ export function useTabStatus(kind: TabStatusKind | null): void {
47
+ const writeRaw = useContext(TerminalWriteContext)
48
+ const prevKindRef = useRef<TabStatusKind | null>(null)
49
+
50
+ useEffect(() => {
51
+ // When kind transitions from non-null to null (e.g. user toggles off
52
+ // showStatusInTerminalTab mid-session), clear the stale dot.
53
+ if (kind === null) {
54
+ if (prevKindRef.current !== null && writeRaw && supportsTabStatus()) {
55
+ writeRaw(wrapForMultiplexer(CLEAR_TAB_STATUS))
56
+ }
57
+
58
+ prevKindRef.current = null
59
+
60
+ return
61
+ }
62
+
63
+ prevKindRef.current = kind
64
+
65
+ if (!writeRaw || !supportsTabStatus()) {
66
+ return
67
+ }
68
+
69
+ writeRaw(wrapForMultiplexer(tabStatus(TAB_STATUS_PRESETS[kind])))
70
+ }, [kind, writeRaw])
71
+ }
@@ -0,0 +1,18 @@
1
+ import { useContext } from 'react'
2
+
3
+ import TerminalFocusContext from '../components/TerminalFocusContext.js'
4
+
5
+ /**
6
+ * Hook to check if the terminal has focus.
7
+ *
8
+ * Uses DECSET 1004 focus reporting - the terminal sends escape sequences
9
+ * when it gains or loses focus. These are handled automatically
10
+ * by Ink and filtered from useInput.
11
+ *
12
+ * @returns true if the terminal is focused (or focus state is unknown)
13
+ */
14
+ export function useTerminalFocus(): boolean {
15
+ const { isTerminalFocused } = useContext(TerminalFocusContext)
16
+
17
+ return isTerminalFocused
18
+ }
@@ -0,0 +1,34 @@
1
+ import { useContext, useEffect } from 'react'
2
+ import stripAnsi from 'strip-ansi'
3
+
4
+ import { OSC, osc } from '../termio/osc.js'
5
+ import { TerminalWriteContext } from '../useTerminalNotification.js'
6
+
7
+ /**
8
+ * Declaratively set the terminal tab/window title.
9
+ *
10
+ * Pass a string to set the title. ANSI escape sequences are stripped
11
+ * automatically so callers don't need to know about terminal encoding.
12
+ * Pass `null` to opt out — the hook becomes a no-op and leaves the
13
+ * terminal title untouched.
14
+ *
15
+ * On Windows, uses `process.title` (classic conhost doesn't support OSC).
16
+ * Elsewhere, writes OSC 0 (set title+icon) via Ink's stdout.
17
+ */
18
+ export function useTerminalTitle(title: string | null): void {
19
+ const writeRaw = useContext(TerminalWriteContext)
20
+
21
+ useEffect(() => {
22
+ if (title === null || !writeRaw) {
23
+ return
24
+ }
25
+
26
+ const clean = stripAnsi(title)
27
+
28
+ if (process.platform === 'win32') {
29
+ process.title = clean
30
+ } else {
31
+ writeRaw(osc(OSC.SET_TITLE_AND_ICON, clean))
32
+ }
33
+ }, [title, writeRaw])
34
+ }
@@ -0,0 +1,100 @@
1
+ import { useCallback, useContext, useLayoutEffect, useRef } from 'react'
2
+
3
+ import { TerminalSizeContext } from '../components/TerminalSizeContext.js'
4
+ import type { DOMElement } from '../dom.js'
5
+
6
+ type ViewportEntry = {
7
+ /**
8
+ * Whether the element is currently within the terminal viewport
9
+ */
10
+ isVisible: boolean
11
+ }
12
+
13
+ /**
14
+ * Hook to detect if a component is within the terminal viewport.
15
+ *
16
+ * Returns a callback ref and a viewport entry object.
17
+ * Attach the ref to the component you want to track.
18
+ *
19
+ * The entry is updated during the layout phase (useLayoutEffect) so callers
20
+ * always read fresh values during render. Visibility changes do NOT trigger
21
+ * re-renders on their own — callers that re-render for other reasons (e.g.
22
+ * animation ticks, state changes) will pick up the latest value naturally.
23
+ * This avoids infinite update loops when combined with other layout effects
24
+ * that also call setState.
25
+ *
26
+ * @example
27
+ * const [ref, entry] = useTerminalViewport()
28
+ * return <Box ref={ref}><Animation enabled={entry.isVisible}>...</Animation></Box>
29
+ */
30
+ export function useTerminalViewport(): [ref: (element: DOMElement | null) => void, entry: ViewportEntry] {
31
+ const terminalSize = useContext(TerminalSizeContext)
32
+ const elementRef = useRef<DOMElement | null>(null)
33
+ const entryRef = useRef<ViewportEntry>({ isVisible: true })
34
+
35
+ const setElement = useCallback((el: DOMElement | null) => {
36
+ elementRef.current = el
37
+ }, [])
38
+
39
+ // Runs on every render because yoga layout values can change
40
+ // without React being aware. Only updates the ref — no setState
41
+ // to avoid cascading re-renders during the commit phase.
42
+ // Walks the DOM ancestor chain fresh each time to avoid holding stale
43
+ // references after yoga tree rebuilds.
44
+ useLayoutEffect(() => {
45
+ const element = elementRef.current
46
+
47
+ if (!element?.yogaNode || !terminalSize) {
48
+ return
49
+ }
50
+
51
+ const height = element.yogaNode.getComputedHeight()
52
+ const rows = terminalSize.rows
53
+
54
+ // Walk the DOM parent chain (not yoga.getParent()) so we can detect
55
+ // scroll containers and subtract their scrollTop. Yoga computes layout
56
+ // positions without scroll offset — scrollTop is applied at render time.
57
+ // Without this, an element inside a ScrollBox whose yoga position exceeds
58
+ // terminalRows would be considered offscreen even when scrolled into view
59
+ // (e.g., the spinner in fullscreen mode after enough messages accumulate).
60
+ let absoluteTop = element.yogaNode.getComputedTop()
61
+ let parent: DOMElement | undefined = element.parentNode
62
+ let root = element.yogaNode
63
+
64
+ while (parent) {
65
+ if (parent.yogaNode) {
66
+ absoluteTop += parent.yogaNode.getComputedTop()
67
+ root = parent.yogaNode
68
+ }
69
+
70
+ // scrollTop is only ever set on scroll containers (by ScrollBox + renderer).
71
+ // Non-scroll nodes have undefined scrollTop → falsy fast-path.
72
+ if (parent.scrollTop) {
73
+ absoluteTop -= parent.scrollTop
74
+ }
75
+
76
+ parent = parent.parentNode
77
+ }
78
+
79
+ // Only the root's height matters
80
+ const screenHeight = root.getComputedHeight()
81
+
82
+ const bottom = absoluteTop + height
83
+ // When content overflows the viewport (screenHeight > rows), the
84
+ // cursor-restore at frame end scrolls one extra row into scrollback.
85
+ // log-update.ts accounts for this with scrollbackRows = viewportY + 1.
86
+ // We must match, otherwise an element at the boundary is considered
87
+ // "visible" here (animation keeps ticking) but its row is treated as
88
+ // scrollback by log-update (content change → full reset → flicker).
89
+ const cursorRestoreScroll = screenHeight > rows ? 1 : 0
90
+ const viewportY = Math.max(0, screenHeight - rows) + cursorRestoreScroll
91
+ const viewportBottom = viewportY + rows
92
+ const visible = bottom > viewportY && absoluteTop < viewportBottom
93
+
94
+ if (visible !== entryRef.current.isVisible) {
95
+ entryRef.current = { isVisible: visible }
96
+ }
97
+ })
98
+
99
+ return [setElement, entryRef.current]
100
+ }
@@ -0,0 +1,52 @@
1
+ import { cellAtIndex, CellWidth, type Screen, setCellStyleId, type StylePool } from './screen.js'
2
+
3
+ /**
4
+ * Highlight every cell whose OSC 8 hyperlink matches `hoveredUrl` by inverting
5
+ * its style. This is the cursor-hover affordance for clickable links: terminal
6
+ * applications can't change the system mouse cursor, so we light up the link
7
+ * itself when the pointer is over it. Same overlay machinery as
8
+ * applySearchHighlight — post-layout, pure SGR, picked up by the diff.
9
+ *
10
+ * Returns true if any cell was highlighted. The caller decides whether to
11
+ * promote that into a full-frame damage request — for hover specifically,
12
+ * full damage is only useful on enter/leave/change transitions (so the
13
+ * previous frame's inverted cells get re-emitted), not on every steady-state
14
+ * frame the pointer sits on the link.
15
+ */
16
+ export function applyHyperlinkHoverHighlight(
17
+ screen: Screen,
18
+ hoveredUrl: string | undefined,
19
+ stylePool: StylePool
20
+ ): boolean {
21
+ if (!hoveredUrl) {
22
+ return false
23
+ }
24
+
25
+ const w = screen.width
26
+ const height = screen.height
27
+ let applied = false
28
+
29
+ for (let row = 0; row < height; row++) {
30
+ const rowOff = row * w
31
+
32
+ for (let col = 0; col < w; col++) {
33
+ const cell = cellAtIndex(screen, rowOff + col)
34
+
35
+ // Skip SpacerTail — the head cell at col-1 owns the hyperlink, and
36
+ // setCellStyleId on the tail would split the styling of a wide-char
37
+ // glyph mid-cell. The head's restyle covers both halves.
38
+ if (cell.width === CellWidth.SpacerTail) {
39
+ continue
40
+ }
41
+
42
+ if (cell.hyperlink !== hoveredUrl) {
43
+ continue
44
+ }
45
+
46
+ applied = true
47
+ setCellStyleId(screen, col, row, stylePool.withInverse(cell.styleId))
48
+ }
49
+ }
50
+
51
+ return applied
52
+ }
@@ -0,0 +1,234 @@
1
+ import { EventEmitter } from 'events'
2
+
3
+ import React from 'react'
4
+ import { describe, expect, it } from 'vitest'
5
+
6
+ import Text from './components/Text.js'
7
+ import Ink from './ink.js'
8
+
9
+ class FakeTty extends EventEmitter {
10
+ chunks: string[] = []
11
+ columns = 40
12
+ rows = 8
13
+ isTTY = true
14
+
15
+ write(chunk: string | Uint8Array, cb?: (err?: Error | null) => void): boolean {
16
+ this.chunks.push(typeof chunk === 'string' ? chunk : Buffer.from(chunk).toString('utf8'))
17
+ cb?.()
18
+
19
+ return true
20
+ }
21
+ }
22
+
23
+ function makeInk() {
24
+ const stdout = new FakeTty()
25
+ const stdin = new FakeTty()
26
+ const stderr = new FakeTty()
27
+
28
+ const ink = new Ink({
29
+ exitOnCtrlC: false,
30
+ patchConsole: false,
31
+ stderr: stderr as unknown as NodeJS.WriteStream,
32
+ stdin: stdin as unknown as NodeJS.ReadStream,
33
+ stdout: stdout as unknown as NodeJS.WriteStream
34
+ })
35
+
36
+ return { ink, stdout, stdin, stderr }
37
+ }
38
+
39
+ // Cast helper instead of exposing __get*ForTest methods on production Ink —
40
+ // these are internal frame/cursor caches we only inspect from tests.
41
+ type InkPrivate = {
42
+ displayCursor: { x: number; y: number } | null
43
+ cursorDeclaration: { node: unknown; relativeX: number; relativeY: number } | null
44
+ frontFrame: { cursor: { x: number; y: number } }
45
+ }
46
+ const peek = (ink: Ink): InkPrivate => ink as unknown as InkPrivate
47
+
48
+ // Closes the cursor-drift bug: when TextInput's fast-echo path writes a
49
+ // printable character directly to stdout, the hardware cursor advances by
50
+ // one cell BUT Ink's `displayCursor` cache (used as the basis for the
51
+ // next frame's relative cursor preamble) wasn't being updated. On long
52
+ // sessions an unrelated re-render (status bar timer, streaming
53
+ // reasoning, etc.) would then park the hardware cursor N cells offset
54
+ // from the actual caret — visible as "extra whitespace between my last
55
+ // typed character and the cursor block".
56
+ describe('Ink.noteExternalCursorAdvance', () => {
57
+ it('bumps an already-tracked displayCursor by the given delta', () => {
58
+ const { ink } = makeInk()
59
+
60
+ ink.render(React.createElement(Text, null, 'hi'))
61
+ ink.onRender()
62
+
63
+ // Seed a known parked position directly. In production this is set by
64
+ // the cursor-park branch in onRender when a useDeclaredCursor caller
65
+ // commits a declaration; this test bypasses React for hermeticity.
66
+ peek(ink).displayCursor = { x: 5, y: 0 }
67
+
68
+ ink.noteExternalCursorAdvance(3)
69
+ expect(peek(ink).displayCursor).toEqual({ x: 8, y: 0 })
70
+
71
+ ink.noteExternalCursorAdvance(-1)
72
+ expect(peek(ink).displayCursor).toEqual({ x: 7, y: 0 })
73
+
74
+ ink.noteExternalCursorAdvance(0, 2)
75
+ expect(peek(ink).displayCursor).toEqual({ x: 7, y: 2 })
76
+
77
+ ink.unmount()
78
+ })
79
+
80
+ it('seeds displayCursor from frontFrame.cursor when nothing was parked', () => {
81
+ const { ink } = makeInk()
82
+
83
+ ink.render(React.createElement(Text, null, 'hello'))
84
+ ink.onRender()
85
+
86
+ expect(peek(ink).displayCursor).toBeNull()
87
+ const base = { x: peek(ink).frontFrame.cursor.x, y: peek(ink).frontFrame.cursor.y }
88
+
89
+ ink.noteExternalCursorAdvance(4)
90
+ expect(peek(ink).displayCursor).toEqual({ x: base.x + 4, y: base.y })
91
+
92
+ ink.unmount()
93
+ })
94
+
95
+ it('is a no-op when the delta is zero', () => {
96
+ const { ink } = makeInk()
97
+
98
+ ink.render(React.createElement(Text, null, 'hi'))
99
+ ink.onRender()
100
+
101
+ ink.noteExternalCursorAdvance(0)
102
+ expect(peek(ink).displayCursor).toBeNull()
103
+
104
+ ink.noteExternalCursorAdvance(0, 0)
105
+ expect(peek(ink).displayCursor).toBeNull()
106
+
107
+ ink.unmount()
108
+ })
109
+
110
+ it('skips displayCursor on alt-screen — CSI H resets every frame', () => {
111
+ const { ink } = makeInk()
112
+
113
+ ink.setAltScreenActive(true)
114
+ ink.render(React.createElement(Text, null, 'hi'))
115
+ ink.onRender()
116
+ peek(ink).displayCursor = { x: 5, y: 0 }
117
+
118
+ ink.noteExternalCursorAdvance(3)
119
+
120
+ expect(peek(ink).displayCursor).toEqual({ x: 5, y: 0 })
121
+
122
+ ink.unmount()
123
+ })
124
+
125
+ // Closes Copilot follow-up on PR #26717: the default TUI wraps the
126
+ // composer in <AlternateScreen>, so alt-screen is the production
127
+ // path. CSI H only resets the log-update relative-move basis — the
128
+ // declared cursor target is still consulted by onRender's alt-screen
129
+ // park branch (`cursorPosition(row, col)` using rect + decl). So
130
+ // cursorDeclaration MUST advance on alt-screen too, even though
131
+ // displayCursor doesn't need to.
132
+ it('still advances cursorDeclaration on alt-screen', () => {
133
+ const { ink } = makeInk()
134
+
135
+ ink.setAltScreenActive(true)
136
+ ink.render(React.createElement(Text, null, 'hi'))
137
+ ink.onRender()
138
+
139
+ const fakeNode = {} as unknown as Record<string, unknown>
140
+
141
+ peek(ink).cursorDeclaration = { node: fakeNode, relativeX: 7, relativeY: 0 }
142
+ peek(ink).displayCursor = { x: 12, y: 0 }
143
+
144
+ ink.noteExternalCursorAdvance(3)
145
+
146
+ // displayCursor untouched on alt-screen
147
+ expect(peek(ink).displayCursor).toEqual({ x: 12, y: 0 })
148
+ // declaration still advanced — onRender's alt-screen park reads this
149
+ expect(peek(ink).cursorDeclaration).toEqual({ node: fakeNode, relativeX: 10, relativeY: 0 })
150
+
151
+ ink.unmount()
152
+ })
153
+
154
+ // Closes Copilot review feedback on PR #26717: even after the
155
+ // TextInput-level fix where layout reads `curRef.current` directly,
156
+ // there's still a window where a fast-echo wrote to stdout but the
157
+ // current cursor declaration on Ink (set by an earlier render's
158
+ // useDeclaredCursor commit) points at the PRE-keystroke caret
159
+ // column. If we advanced only `displayCursor`, an unrelated re-render
160
+ // in that window would re-run onRender's cursor-park branch with the
161
+ // stale declaration and visually undo the fast-echo's advance. We
162
+ // must bump BOTH so the cursor stays anchored to the physical caret
163
+ // until the next React commit publishes a fresh declaration
164
+ // (computed from `curRef.current` via the cursorLayout call in
165
+ // textInput.tsx) that supersedes the bump.
166
+ it('advances the active cursorDeclaration in lock-step with displayCursor', () => {
167
+ const { ink } = makeInk()
168
+
169
+ ink.render(React.createElement(Text, null, 'hi'))
170
+ ink.onRender()
171
+
172
+ const fakeNode = {} as unknown as Record<string, unknown>
173
+
174
+ peek(ink).cursorDeclaration = { node: fakeNode, relativeX: 7, relativeY: 0 }
175
+ peek(ink).displayCursor = { x: 12, y: 0 }
176
+
177
+ ink.noteExternalCursorAdvance(3)
178
+
179
+ expect(peek(ink).displayCursor).toEqual({ x: 15, y: 0 })
180
+ expect(peek(ink).cursorDeclaration).toEqual({ node: fakeNode, relativeX: 10, relativeY: 0 })
181
+
182
+ ink.noteExternalCursorAdvance(-1)
183
+ expect(peek(ink).displayCursor).toEqual({ x: 14, y: 0 })
184
+ expect(peek(ink).cursorDeclaration).toEqual({ node: fakeNode, relativeX: 9, relativeY: 0 })
185
+
186
+ ink.unmount()
187
+ })
188
+
189
+ // Closes Copilot follow-up on PR #26717: the dy half of the notifier
190
+ // contract was tested for `displayCursor` but not for
191
+ // `cursorDeclaration.relativeY`. Newlines in fast-echoed text never
192
+ // hit the bypass today (canFastAppendShape rejects '\n'), but `dy`
193
+ // is part of the public API and must propagate symmetrically with
194
+ // dx so future callers (e.g. multi-line paste shortcuts) don't get
195
+ // a half-implemented contract.
196
+ it('advances cursorDeclaration.relativeY when dy is non-zero', () => {
197
+ const { ink } = makeInk()
198
+
199
+ ink.render(React.createElement(Text, null, 'hi'))
200
+ ink.onRender()
201
+
202
+ const fakeNode = {} as unknown as Record<string, unknown>
203
+
204
+ peek(ink).cursorDeclaration = { node: fakeNode, relativeX: 2, relativeY: 1 }
205
+ peek(ink).displayCursor = { x: 4, y: 2 }
206
+
207
+ ink.noteExternalCursorAdvance(1, 3)
208
+
209
+ expect(peek(ink).displayCursor).toEqual({ x: 5, y: 5 })
210
+ expect(peek(ink).cursorDeclaration).toEqual({ node: fakeNode, relativeX: 3, relativeY: 4 })
211
+
212
+ // Negative dy too — cursor moving up across visual rows.
213
+ ink.noteExternalCursorAdvance(0, -2)
214
+ expect(peek(ink).displayCursor).toEqual({ x: 5, y: 3 })
215
+ expect(peek(ink).cursorDeclaration).toEqual({ node: fakeNode, relativeX: 3, relativeY: 2 })
216
+
217
+ ink.unmount()
218
+ })
219
+
220
+ it('leaves cursorDeclaration unchanged when no declaration is active', () => {
221
+ const { ink } = makeInk()
222
+
223
+ ink.render(React.createElement(Text, null, 'hi'))
224
+ ink.onRender()
225
+
226
+ expect(peek(ink).cursorDeclaration).toBeNull()
227
+
228
+ ink.noteExternalCursorAdvance(3)
229
+
230
+ expect(peek(ink).cursorDeclaration).toBeNull()
231
+
232
+ ink.unmount()
233
+ })
234
+ })
@@ -0,0 +1,50 @@
1
+ import { EventEmitter } from 'events'
2
+ import React from 'react'
3
+ import { describe, expect, it } from 'vitest'
4
+
5
+ import Text from './components/Text.js'
6
+ import Ink from './ink.js'
7
+ import { CURSOR_HOME, ERASE_SCREEN } from './termio/csi.js'
8
+
9
+ class FakeTty extends EventEmitter {
10
+ chunks: string[] = []
11
+ columns = 20
12
+ rows = 5
13
+ isTTY = true
14
+
15
+ write(chunk: string | Uint8Array, cb?: (err?: Error | null) => void): boolean {
16
+ this.chunks.push(typeof chunk === 'string' ? chunk : Buffer.from(chunk).toString('utf8'))
17
+ cb?.()
18
+ return true
19
+ }
20
+ }
21
+
22
+ const tick = () => new Promise<void>(resolve => queueMicrotask(resolve))
23
+
24
+ describe('Ink resize healing', () => {
25
+ it('heals same-dimension alt-screen resize events with an erase before repaint', async () => {
26
+ const stdout = new FakeTty()
27
+ const stdin = new FakeTty()
28
+ const stderr = new FakeTty()
29
+ const ink = new Ink({
30
+ exitOnCtrlC: false,
31
+ patchConsole: false,
32
+ stderr: stderr as unknown as NodeJS.WriteStream,
33
+ stdin: stdin as unknown as NodeJS.ReadStream,
34
+ stdout: stdout as unknown as NodeJS.WriteStream
35
+ })
36
+
37
+ ink.setAltScreenActive(true)
38
+ ink.render(React.createElement(Text, null, 'hello'))
39
+ ink.onRender()
40
+ stdout.chunks = []
41
+
42
+ stdout.emit('resize')
43
+ ink.onRender()
44
+ await tick()
45
+
46
+ expect(stdout.chunks.join('')).toContain(ERASE_SCREEN + CURSOR_HOME)
47
+
48
+ ink.unmount()
49
+ })
50
+ })