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,177 @@
1
+ // Lightweight fuzzy subsequence scorer for picker filtering.
2
+ //
3
+ // Matches a query as an ordered subsequence of the target (so `g4o` matches
4
+ // `gpt-4o`) and scores by match quality so callers can rank results. Higher
5
+ // score is a better match. Returns the matched character indices so callers
6
+ // can highlight them.
7
+ //
8
+ // The scoring favours, in rough order: exact full match, prefix match, matches
9
+ // that start on a word boundary (after `-`, `_`, `/`, `.`, space, or a
10
+ // lower→upper case transition), contiguous runs, and earlier matches. This is
11
+ // intentionally simple — no external dependency — but good enough to make
12
+ // `son4` rank `claude-sonnet-4` above an incidental scattered hit.
13
+ //
14
+ // The WebUI ships a logically identical copy of this module at
15
+ // web/src/lib/fuzzy.ts (only prettier formatting differs); keep the two in
16
+ // sync. The TUI copy carries the vitest suite (the web package has no test
17
+ // runner), so changes should be validated here.
18
+
19
+ export interface FuzzyMatch {
20
+ /** Total score; higher is better. */
21
+ score: number
22
+ /** Indices into the original (non-lowercased) target that were matched. */
23
+ positions: number[]
24
+ }
25
+
26
+ const WORD_BOUNDARY = /[-_/.\s]/
27
+
28
+ function isBoundary(target: string, index: number): boolean {
29
+ if (index === 0) {
30
+ return true
31
+ }
32
+
33
+ const prev = target[index - 1]
34
+
35
+ if (WORD_BOUNDARY.test(prev)) {
36
+ return true
37
+ }
38
+
39
+ // camelCase / lower→upper transition (e.g. the `O` in `gptO`).
40
+ const cur = target[index]
41
+
42
+ return prev === prev.toLowerCase() && cur !== cur.toLowerCase() && cur === cur.toUpperCase()
43
+ }
44
+
45
+ /**
46
+ * Score a single query token against a target. Returns null when the token is
47
+ * not a subsequence of the target. An empty query scores 0 with no positions.
48
+ */
49
+ export function fuzzyScore(target: string, query: string): FuzzyMatch | null {
50
+ if (!query) {
51
+ return { score: 0, positions: [] }
52
+ }
53
+
54
+ const lowerTarget = target.toLowerCase()
55
+ const lowerQuery = query.toLowerCase()
56
+
57
+ const positions: number[] = []
58
+ let score = 0
59
+ let prevIndex = -1
60
+ let searchFrom = 0
61
+
62
+ for (const ch of lowerQuery) {
63
+ const idx = lowerTarget.indexOf(ch, searchFrom)
64
+
65
+ if (idx < 0) {
66
+ return null
67
+ }
68
+
69
+ positions.push(idx)
70
+
71
+ // Base point for the matched character.
72
+ score += 1
73
+
74
+ // Contiguous with the previous match → strong bonus.
75
+ if (prevIndex >= 0 && idx === prevIndex + 1) {
76
+ score += 5
77
+ } else if (prevIndex >= 0) {
78
+ // Penalise the gap we had to skip (capped), so contiguous beats scattered.
79
+ score -= Math.min(idx - prevIndex - 1, 3)
80
+ }
81
+
82
+ // Word-boundary / start-of-string matches are meaningful.
83
+ if (isBoundary(target, idx)) {
84
+ score += 3
85
+ }
86
+
87
+ // Matching the very first character of the target is the strongest signal.
88
+ if (idx === 0) {
89
+ score += 5
90
+ }
91
+
92
+ prevIndex = idx
93
+ searchFrom = idx + 1
94
+ }
95
+
96
+ // Prefix bonus: the query matched a contiguous prefix of the target.
97
+ if (positions.length && positions[0] === 0 && positions[positions.length - 1] === positions.length - 1) {
98
+ score += 8
99
+ }
100
+
101
+ // Exact full match dominates everything else.
102
+ if (lowerTarget === lowerQuery) {
103
+ score += 20
104
+ }
105
+
106
+ // Slightly prefer shorter targets when scores are otherwise close, so a
107
+ // query that fully prefixes a short id beats the same prefix on a long one.
108
+ score -= lowerTarget.length * 0.01
109
+
110
+ return { score, positions }
111
+ }
112
+
113
+ /**
114
+ * Score a target against a whitespace-separated, multi-token query. Every token
115
+ * must match (AND semantics); the result aggregates per-token scores and the
116
+ * union of matched positions. Returns null if any token fails to match.
117
+ */
118
+ export function fuzzyScoreMulti(target: string, query: string): FuzzyMatch | null {
119
+ const tokens = query.trim().toLowerCase().split(/\s+/).filter(Boolean)
120
+
121
+ if (!tokens.length) {
122
+ return { score: 0, positions: [] }
123
+ }
124
+
125
+ let score = 0
126
+ const positionSet = new Set<number>()
127
+
128
+ for (const token of tokens) {
129
+ const match = fuzzyScore(target, token)
130
+
131
+ if (!match) {
132
+ return null
133
+ }
134
+
135
+ score += match.score
136
+
137
+ for (const pos of match.positions) {
138
+ positionSet.add(pos)
139
+ }
140
+ }
141
+
142
+ return { score, positions: [...positionSet].sort((a, b) => a - b) }
143
+ }
144
+
145
+ export interface RankedItem<T> {
146
+ item: T
147
+ score: number
148
+ positions: number[]
149
+ }
150
+
151
+ /**
152
+ * Filter + rank a list by a fuzzy query against a derived text key. Non-matching
153
+ * items are dropped; matches are sorted by score (descending), ties broken by
154
+ * the original index so ordering is stable for equal scores. An empty query
155
+ * returns every item in original order with no positions.
156
+ */
157
+ export function fuzzyRank<T>(items: readonly T[], query: string, toText: (item: T) => string): RankedItem<T>[] {
158
+ const trimmed = query.trim()
159
+
160
+ if (!trimmed) {
161
+ return items.map(item => ({ item, score: 0, positions: [] }))
162
+ }
163
+
164
+ const ranked: Array<RankedItem<T> & { index: number }> = []
165
+
166
+ items.forEach((item, index) => {
167
+ const match = fuzzyScoreMulti(toText(item), trimmed)
168
+
169
+ if (match) {
170
+ ranked.push({ item, score: match.score, positions: match.positions, index })
171
+ }
172
+ })
173
+
174
+ ranked.sort((a, b) => b.score - a.score || a.index - b.index)
175
+
176
+ return ranked.map(({ item, score, positions }) => ({ item, score, positions }))
177
+ }
@@ -0,0 +1,47 @@
1
+ interface SetupOptions {
2
+ cleanups?: (() => Promise<void> | void)[]
3
+ failsafeMs?: number
4
+ onError?: (scope: 'uncaughtException' | 'unhandledRejection', err: unknown) => void
5
+ onSignal?: (signal: NodeJS.Signals) => void
6
+ }
7
+
8
+ const SIGNAL_EXIT_CODE: Record<'SIGHUP' | 'SIGINT' | 'SIGTERM', number> = {
9
+ SIGHUP: 129,
10
+ SIGINT: 130,
11
+ SIGTERM: 143
12
+ }
13
+
14
+ let wired = false
15
+
16
+ export function setupGracefulExit({ cleanups = [], failsafeMs = 4000, onError, onSignal }: SetupOptions = {}) {
17
+ if (wired) {
18
+ return
19
+ }
20
+
21
+ wired = true
22
+
23
+ let shuttingDown = false
24
+
25
+ const exit = (code: number, signal?: NodeJS.Signals) => {
26
+ if (shuttingDown) {
27
+ return
28
+ }
29
+
30
+ shuttingDown = true
31
+
32
+ if (signal) {
33
+ onSignal?.(signal)
34
+ }
35
+
36
+ setTimeout(() => process.exit(code), failsafeMs).unref?.()
37
+
38
+ void Promise.allSettled(cleanups.map(fn => Promise.resolve().then(fn))).finally(() => process.exit(code))
39
+ }
40
+
41
+ for (const sig of ['SIGINT', 'SIGTERM', 'SIGHUP'] as const) {
42
+ process.on(sig, () => exit(SIGNAL_EXIT_CODE[sig], sig))
43
+ }
44
+
45
+ process.on('uncaughtException', err => onError?.('uncaughtException', err))
46
+ process.on('unhandledRejection', reason => onError?.('unhandledRejection', reason))
47
+ }
@@ -0,0 +1,82 @@
1
+ import { appendFileSync, existsSync, mkdirSync, readFileSync } from 'node:fs'
2
+ import { homedir } from 'node:os'
3
+ import { join } from 'node:path'
4
+
5
+ const MAX = 1000
6
+ const dir = process.env.NASTECH_HOME ?? join(homedir(), '.nastech')
7
+ const file = join(dir, '.nastech_history')
8
+
9
+ let cache: string[] | null = null
10
+
11
+ export function load() {
12
+ if (cache) {
13
+ return cache
14
+ }
15
+
16
+ try {
17
+ if (!existsSync(file)) {
18
+ cache = []
19
+
20
+ return cache
21
+ }
22
+
23
+ const entries: string[] = []
24
+ let current: string[] = []
25
+
26
+ for (const line of readFileSync(file, 'utf8').split('\n')) {
27
+ if (line.startsWith('+')) {
28
+ current.push(line.slice(1))
29
+ } else if (current.length) {
30
+ entries.push(current.join('\n'))
31
+ current = []
32
+ }
33
+ }
34
+
35
+ if (current.length) {
36
+ entries.push(current.join('\n'))
37
+ }
38
+
39
+ cache = entries.slice(-MAX)
40
+ } catch {
41
+ cache = []
42
+ }
43
+
44
+ return cache
45
+ }
46
+
47
+ export function append(line: string) {
48
+ const trimmed = line.trim()
49
+
50
+ if (!trimmed) {
51
+ return
52
+ }
53
+
54
+ const items = load()
55
+
56
+ if (items.at(-1) === trimmed) {
57
+ return
58
+ }
59
+
60
+ items.push(trimmed)
61
+
62
+ if (items.length > MAX) {
63
+ items.splice(0, items.length - MAX)
64
+ }
65
+
66
+ try {
67
+ if (!existsSync(dir)) {
68
+ mkdirSync(dir, { recursive: true })
69
+ }
70
+
71
+ const ts = new Date().toISOString().replace('T', ' ').replace('Z', '')
72
+
73
+ const encoded = trimmed
74
+ .split('\n')
75
+ .map(l => `+${l}`)
76
+ .join('\n')
77
+
78
+ appendFileSync(file, `\n# ${ts}\n${encoded}\n`)
79
+ } catch {
80
+ void 0
81
+ }
82
+ }
@@ -0,0 +1,203 @@
1
+ import { stringWidth, wrapAnsi } from '@nastechai/ink'
2
+
3
+ import type { Role } from '../types.js'
4
+
5
+ export const COMPOSER_PROMPT_GAP_WIDTH = 1
6
+
7
+ let _seg: Intl.Segmenter | null = null
8
+ const seg = () => (_seg ??= new Intl.Segmenter(undefined, { granularity: 'grapheme' }))
9
+
10
+ interface VisualLine {
11
+ end: number
12
+ start: number
13
+ }
14
+
15
+ const graphemes = (value: string) =>
16
+ [...seg().segment(value)].map(({ segment, index }) => ({
17
+ end: index + segment.length,
18
+ index,
19
+ segment,
20
+ width: Math.max(1, stringWidth(segment))
21
+ }))
22
+
23
+ // Build VisualLines from wrap-ansi's output by mapping each emitted character
24
+ // back to its original offset in `value`. wrap-ansi only INSERTS '\n' at wrap
25
+ // boundaries — it never drops, reorders, or substitutes existing characters —
26
+ // so a parallel walk uniquely identifies each line's source range.
27
+ //
28
+ // This used to be a hand-rolled word-wrap whose break points disagreed with
29
+ // wrap-ansi in subtle but visible ways: exact-fill rows pushed the cursor to
30
+ // a phantom next line, mid-word breaks landed one grapheme off, etc. The
31
+ // composer's TextInput renders text via Ink's <Text wrap="wrap">, which
32
+ // delegates to wrap-ansi — so any drift between the two algorithms parks the
33
+ // hardware cursor several cells away from the last rendered character.
34
+ // Sourcing both from wrap-ansi guarantees agreement.
35
+ function visualLines(value: string, cols: number): VisualLine[] {
36
+ if (!value.length) {
37
+ return [{ start: 0, end: 0 }]
38
+ }
39
+
40
+ const width = Math.max(1, cols)
41
+ const wrapped = wrapAnsi(value, width, { hard: true, trim: false })
42
+ const lines: VisualLine[] = []
43
+
44
+ let originalIdx = 0
45
+ let lineStart = 0
46
+
47
+ for (let i = 0; i < wrapped.length; i += 1) {
48
+ const ch = wrapped[i]!
49
+
50
+ if (ch === '\n') {
51
+ // wrap-ansi inserts '\n' to mark a soft-wrap boundary OR copies a
52
+ // literal '\n' from the input. Either way the next char in `wrapped`
53
+ // begins a new visual line. If the source character is a hard '\n',
54
+ // consume it (it doesn't appear in either line). Otherwise the '\n'
55
+ // is purely a wrap marker and originalIdx stays put.
56
+ lines.push({ start: lineStart, end: originalIdx })
57
+ const isHardNewline = originalIdx < value.length && value[originalIdx] === '\n'
58
+
59
+ if (isHardNewline) {
60
+ originalIdx += 1
61
+ }
62
+
63
+ lineStart = originalIdx
64
+
65
+ continue
66
+ }
67
+
68
+ // Defensive sync check. wrap-ansi (with `hard: true, trim: false`, no
69
+ // styled input) is documented to only insert '\n' at break points and
70
+ // never substitute, drop, or reorder source characters — so under those
71
+ // options `wrapped[i]` should always equal `value[originalIdx]`. But
72
+ // future option changes, library upgrades, or callers that start passing
73
+ // styled input (ANSI escapes) could violate that invariant silently. If
74
+ // they do, we'd slide `originalIdx` past the end of `value` and emit
75
+ // garbage line ranges with no diagnostic. Realign by scanning forward
76
+ // for the matching character; bail out (return whatever we have) if the
77
+ // sync is unrecoverable rather than producing wrong-but-plausible output.
78
+ if (originalIdx >= value.length) {
79
+ break
80
+ }
81
+
82
+ if (value[originalIdx] !== ch) {
83
+ const reSync = value.indexOf(ch, originalIdx)
84
+
85
+ if (reSync === -1) {
86
+ break
87
+ }
88
+
89
+ originalIdx = reSync
90
+ }
91
+
92
+ originalIdx += 1
93
+ }
94
+
95
+ lines.push({ start: lineStart, end: originalIdx })
96
+
97
+ // wrap-ansi collapses an empty input into [""] which we already handled
98
+ // above; preserve the invariant that lines is never empty for any input.
99
+ return lines.length ? lines : [{ start: 0, end: 0 }]
100
+ }
101
+
102
+ function widthBetween(value: string, start: number, end: number) {
103
+ let width = 0
104
+
105
+ for (const part of graphemes(value.slice(start, end))) {
106
+ width += part.width
107
+ }
108
+
109
+ return width
110
+ }
111
+
112
+ /**
113
+ * Mirrors the word-wrap behavior used by the composer TextInput.
114
+ * Returns the zero-based visual line and column of the cursor cell.
115
+ *
116
+ * IMPORTANT: this MUST stay in lock-step with how Ink's `<Text wrap="wrap">`
117
+ * lays the value out (which uses `wrap-ansi`). Any divergence parks the
118
+ * hardware cursor several cells off the last rendered character — see the
119
+ * "cursor drift past blank cells" bug. `visualLines` is sourced directly
120
+ * from wrap-ansi to enforce that invariant.
121
+ */
122
+ export function cursorLayout(value: string, cursor: number, cols: number) {
123
+ const pos = Math.max(0, Math.min(cursor, value.length))
124
+ const w = Math.max(1, cols)
125
+ const lines = visualLines(value, w)
126
+ let lineIndex = 0
127
+
128
+ for (let i = 0; i < lines.length; i += 1) {
129
+ if (lines[i]!.start <= pos) {
130
+ lineIndex = i
131
+ } else {
132
+ break
133
+ }
134
+ }
135
+
136
+ const line = lines[lineIndex]!
137
+ const column = widthBetween(value, line.start, Math.min(pos, line.end))
138
+
139
+ // NOTE: the previous implementation forced an extra line break when
140
+ // `column >= w` (the "trailing cursor-cell overflows" rule). With
141
+ // `visualLines` sourcing breaks from wrap-ansi, the line wrapping
142
+ // above already matches what Ink will actually render. Pushing the
143
+ // cursor onto a phantom next line here would re-introduce the same
144
+ // drift we're fixing, so we don't.
145
+ return { column, line: lineIndex }
146
+ }
147
+
148
+ export function offsetFromPosition(value: string, row: number, col: number, cols: number) {
149
+ if (!value.length) {
150
+ return 0
151
+ }
152
+
153
+ const lines = visualLines(value, cols)
154
+ const target = lines[Math.max(0, Math.min(lines.length - 1, Math.floor(row)))]!
155
+ const targetCol = Math.max(0, Math.floor(col))
156
+ let column = 0
157
+
158
+ for (const part of graphemes(value.slice(target.start, target.end))) {
159
+ if (targetCol <= column + Math.max(0, part.width - 1)) {
160
+ return target.start + part.index
161
+ }
162
+
163
+ column += part.width
164
+ }
165
+
166
+ return target.end
167
+ }
168
+
169
+ export function inputVisualHeight(value: string, columns: number) {
170
+ return cursorLayout(value, value.length, columns).line + 1
171
+ }
172
+
173
+ export function composerPromptWidth(promptText: string) {
174
+ return Math.max(1, stringWidth(promptText)) + COMPOSER_PROMPT_GAP_WIDTH
175
+ }
176
+
177
+ export function transcriptGutterWidth(role: Role, userPrompt: string) {
178
+ return role === 'user' ? composerPromptWidth(userPrompt) : 3
179
+ }
180
+
181
+ export function transcriptBodyWidth(totalCols: number, role: Role, userPrompt: string, termuxMode = false) {
182
+ const horizontalReserve = termuxMode ? 2 : 4
183
+ const available = Math.max(1, totalCols - transcriptGutterWidth(role, userPrompt) - horizontalReserve)
184
+
185
+ if (termuxMode) {
186
+ // On narrow / unusual aspect-ratio mobile panes, forcing a wide minimum
187
+ // width causes right-edge clipping and chopped words.
188
+ return available
189
+ }
190
+
191
+ return Math.max(20, available)
192
+ }
193
+
194
+ export function stableComposerColumns(totalCols: number, promptWidth: number, termuxMode = false) {
195
+ // Physical render/wrap width. Always reserve outer composer padding and
196
+ // prompt prefix. Only reserve the transcript scrollbar gutter when the
197
+ // terminal is wide enough; on narrow panes, preserving input columns beats
198
+ // keeping gutters visually aligned.
199
+ const afterPrompt = totalCols - promptWidth
200
+ const reserveScrollbar = afterPrompt >= (termuxMode ? 36 : 24) ? 2 : 0
201
+
202
+ return Math.max(1, totalCols - promptWidth - 2 - reserveScrollbar)
203
+ }
@@ -0,0 +1,116 @@
1
+ import { describe, expect, it } from 'vitest'
2
+
3
+ import type { Msg } from '../types.js'
4
+
5
+ import { appendToolShelfMessage, canHoldToolShelf, isTodoDone, mergeToolShelfInto } from './liveProgress.js'
6
+
7
+ describe('isTodoDone', () => {
8
+ it('only treats non-empty all-completed/cancelled lists as done', () => {
9
+ expect(isTodoDone([])).toBe(false)
10
+ expect(isTodoDone([{ content: 'x', id: 'x', status: 'completed' }])).toBe(true)
11
+ expect(isTodoDone([{ content: 'x', id: 'x', status: 'in_progress' }])).toBe(false)
12
+ expect(
13
+ isTodoDone([
14
+ { content: 'x', id: 'x', status: 'completed' },
15
+ { content: 'y', id: 'y', status: 'cancelled' }
16
+ ])
17
+ ).toBe(true)
18
+ })
19
+ })
20
+
21
+ describe('tool shelf helpers', () => {
22
+ it('recognizes contextual thinking shelves as holders', () => {
23
+ expect(canHoldToolShelf({ kind: 'trail', role: 'system', text: '', thinking: 'plan' })).toBe(true)
24
+ expect(canHoldToolShelf({ kind: 'trail', role: 'system', text: '', tools: ['one ✓'] })).toBe(true)
25
+ expect(canHoldToolShelf({ role: 'assistant', text: 'done' })).toBe(false)
26
+ })
27
+
28
+ it('merges source rows into an existing shelf', () => {
29
+ expect(
30
+ mergeToolShelfInto(
31
+ { kind: 'trail', role: 'system', text: '', thinking: 'plan', tools: ['one ✓'] },
32
+ { kind: 'trail', role: 'system', text: '', tools: ['two ✓'] }
33
+ )
34
+ ).toEqual({ kind: 'trail', role: 'system', text: '', thinking: 'plan', tools: ['one ✓', 'two ✓'] })
35
+ })
36
+ })
37
+
38
+ describe('appendToolShelfMessage', () => {
39
+ it('merges adjacent tool shelves into one contextual shelf', () => {
40
+ const merged = appendToolShelfMessage([{ kind: 'trail', role: 'system', text: '', tools: ['one ✓'] }], {
41
+ kind: 'trail',
42
+ role: 'system',
43
+ text: '',
44
+ tools: ['two ✓']
45
+ })
46
+
47
+ expect(merged).toEqual([{ kind: 'trail', role: 'system', text: '', tools: ['one ✓', 'two ✓'] }])
48
+ })
49
+
50
+ it('adds tools to the nearest contextual thinking shelf', () => {
51
+ const merged = appendToolShelfMessage(
52
+ [{ kind: 'trail', role: 'system', text: '', thinking: 'plan', tools: ['one ✓'] }],
53
+ { kind: 'trail', role: 'system', text: '', tools: ['two ✓'] }
54
+ )
55
+
56
+ expect(merged).toEqual([{ kind: 'trail', role: 'system', text: '', thinking: 'plan', tools: ['one ✓', 'two ✓'] }])
57
+ })
58
+
59
+ it('merges through intervening thinking-only rows back into the nearest holder', () => {
60
+ const prev: Msg[] = [
61
+ { kind: 'trail', role: 'system', text: '', thinking: 'plan', tools: ['one ✓'] },
62
+ { kind: 'trail', role: 'system', text: '', thinking: 'more plan' }
63
+ ]
64
+
65
+ const merged = appendToolShelfMessage(prev, {
66
+ kind: 'trail',
67
+ role: 'system',
68
+ text: '',
69
+ tools: ['two ✓']
70
+ })
71
+
72
+ expect(merged).toHaveLength(2)
73
+ expect(merged[0]).toEqual({
74
+ kind: 'trail',
75
+ role: 'system',
76
+ text: '',
77
+ thinking: 'plan',
78
+ tools: ['one ✓', 'two ✓']
79
+ })
80
+ expect(merged[1]).toEqual({ kind: 'trail', role: 'system', text: '', thinking: 'more plan' })
81
+ })
82
+
83
+ it('collapses a chronological thinking/tool/thinking/tool stream into one shelf', () => {
84
+ const events: Msg[] = [
85
+ { kind: 'trail', role: 'system', text: '', thinking: 'plan' },
86
+ { kind: 'trail', role: 'system', text: '', tools: ['one ✓'] },
87
+ { kind: 'trail', role: 'system', text: '', thinking: 'more plan' },
88
+ { kind: 'trail', role: 'system', text: '', tools: ['two ✓'] },
89
+ { kind: 'trail', role: 'system', text: '', tools: ['three ✓'] }
90
+ ]
91
+
92
+ const reduced = events.reduce<Msg[]>((acc, msg) => appendToolShelfMessage(acc, msg), [])
93
+
94
+ expect(reduced).toHaveLength(2)
95
+ expect(reduced[0]).toEqual({
96
+ kind: 'trail',
97
+ role: 'system',
98
+ text: '',
99
+ thinking: 'plan',
100
+ tools: ['one ✓', 'two ✓', 'three ✓']
101
+ })
102
+ expect(reduced[1]).toEqual({ kind: 'trail', role: 'system', text: '', thinking: 'more plan' })
103
+ })
104
+
105
+ it('starts a new shelf across assistant text boundaries', () => {
106
+ const merged = appendToolShelfMessage(
107
+ [
108
+ { kind: 'trail', role: 'system', text: '', tools: ['one ✓'] },
109
+ { role: 'assistant', text: 'done' }
110
+ ],
111
+ { kind: 'trail', role: 'system', text: '', tools: ['two ✓'] }
112
+ )
113
+
114
+ expect(merged).toHaveLength(3)
115
+ })
116
+ })
@@ -0,0 +1,79 @@
1
+ import type { Msg, TodoItem } from '../types.js'
2
+
3
+ export const countPendingTodos = (todos: readonly TodoItem[]) =>
4
+ todos.filter(todo => todo.status === 'in_progress' || todo.status === 'pending').length
5
+
6
+ export const isTodoDone = (todos: readonly TodoItem[]) =>
7
+ todos.length > 0 && todos.every(todo => todo.status === 'completed' || todo.status === 'cancelled')
8
+
9
+ export const isToolShelfMessage = (msg: Msg | undefined) =>
10
+ Boolean(msg?.kind === 'trail' && !msg.text && !msg.thinking?.trim() && msg.tools?.length)
11
+
12
+ export const canHoldToolShelf = (msg: Msg | undefined) =>
13
+ Boolean(msg?.kind === 'trail' && !msg.text && (msg.thinking?.trim() || msg.tools?.length))
14
+
15
+ export const mergeToolShelfInto = (target: Msg, source: Msg): Msg => ({
16
+ ...target,
17
+ tools: [...(target.tools ?? []), ...(source.tools ?? [])]
18
+ })
19
+
20
+ const isBarrierMessage = (msg: Msg | undefined) => {
21
+ if (!msg) {
22
+ return true
23
+ }
24
+
25
+ // Assistant text, user input, intro/panel rows all terminate the shelf.
26
+ if (msg.kind === 'intro' || msg.kind === 'panel' || msg.kind === 'diff') {
27
+ return true
28
+ }
29
+
30
+ if (msg.role && msg.role !== 'system') {
31
+ return true
32
+ }
33
+
34
+ if (msg.text) {
35
+ return true
36
+ }
37
+
38
+ return false
39
+ }
40
+
41
+ const isToolCarryingTrail = (msg: Msg | undefined) => Boolean(msg?.kind === 'trail' && !msg.text && msg.tools?.length)
42
+
43
+ export const appendToolShelfMessage = (prev: readonly Msg[], msg: Msg): Msg[] => {
44
+ if (!isToolShelfMessage(msg)) {
45
+ return [...prev, msg]
46
+ }
47
+
48
+ let fallbackHolder: number | null = null
49
+
50
+ for (let index = prev.length - 1; index >= 0; index--) {
51
+ const candidate = prev[index]
52
+
53
+ if (isToolCarryingTrail(candidate)) {
54
+ const next = [...prev]
55
+
56
+ next[index] = mergeToolShelfInto(candidate!, msg)
57
+
58
+ return next
59
+ }
60
+
61
+ if (fallbackHolder === null && canHoldToolShelf(candidate)) {
62
+ fallbackHolder = index
63
+ }
64
+
65
+ if (isBarrierMessage(candidate)) {
66
+ break
67
+ }
68
+ }
69
+
70
+ if (fallbackHolder !== null) {
71
+ const next = [...prev]
72
+
73
+ next[fallbackHolder] = mergeToolShelfInto(prev[fallbackHolder]!, msg)
74
+
75
+ return next
76
+ }
77
+
78
+ return [...prev, msg]
79
+ }