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,65 @@
1
+ import { describe, expect, it } from 'vitest'
2
+
3
+ import { providerDisplayNames } from '../domain/providers.js'
4
+
5
+ describe('providerDisplayNames', () => {
6
+ it('returns bare names when all are unique', () => {
7
+ expect(
8
+ providerDisplayNames([
9
+ { name: 'Anthropic', slug: 'anthropic' },
10
+ { name: 'OpenAI', slug: 'openai' }
11
+ ])
12
+ ).toEqual(['Anthropic', 'OpenAI'])
13
+ })
14
+
15
+ it('appends slug to every collision so the disambiguation is symmetric', () => {
16
+ expect(
17
+ providerDisplayNames([
18
+ { name: 'Kimi For Coding', slug: 'kimi-coding' },
19
+ { name: 'Kimi For Coding', slug: 'kimi-coding-cn' }
20
+ ])
21
+ ).toEqual(['Kimi For Coding (kimi-coding)', 'Kimi For Coding (kimi-coding-cn)'])
22
+ })
23
+
24
+ it('only disambiguates the colliding group', () => {
25
+ expect(
26
+ providerDisplayNames([
27
+ { name: 'Anthropic', slug: 'anthropic' },
28
+ { name: 'Foo', slug: 'foo-a' },
29
+ { name: 'Foo', slug: 'foo-b' }
30
+ ])
31
+ ).toEqual(['Anthropic', 'Foo (foo-a)', 'Foo (foo-b)'])
32
+ })
33
+
34
+ it('falls back to plain name if slug is empty', () => {
35
+ expect(
36
+ providerDisplayNames([
37
+ { name: 'Foo', slug: '' },
38
+ { name: 'Foo', slug: '' }
39
+ ])
40
+ ).toEqual(['Foo', 'Foo'])
41
+ })
42
+
43
+ it('skips disambiguation when slug equals name', () => {
44
+ expect(
45
+ providerDisplayNames([
46
+ { name: 'foo', slug: 'foo' },
47
+ { name: 'foo', slug: 'foo' }
48
+ ])
49
+ ).toEqual(['foo', 'foo'])
50
+ })
51
+
52
+ it('handles empty input', () => {
53
+ expect(providerDisplayNames([])).toEqual([])
54
+ })
55
+
56
+ it('preserves order', () => {
57
+ const input = [
58
+ { name: 'Z', slug: 'z' },
59
+ { name: 'A', slug: 'a1' },
60
+ { name: 'A', slug: 'a2' }
61
+ ]
62
+
63
+ expect(providerDisplayNames(input)).toEqual(['Z', 'A (a1)', 'A (a2)'])
64
+ })
65
+ })
@@ -0,0 +1,76 @@
1
+ import { describe, expect, it } from 'vitest'
2
+
3
+ import { hasReasoningTag, splitReasoning } from '../lib/reasoning.js'
4
+ import { cleanThinkingText } from '../lib/text.js'
5
+
6
+ describe('splitReasoning', () => {
7
+ it('extracts <think>…</think> and strips it from text', () => {
8
+ const { reasoning, text } = splitReasoning('<think>plotting</think>\n\nhere is the answer')
9
+
10
+ expect(reasoning).toBe('plotting')
11
+ expect(text).toBe('here is the answer')
12
+ })
13
+
14
+ it('handles multiple tag shapes', () => {
15
+ const input = '<reasoning>a</reasoning> <THINKING>b</THINKING> <thought>c</thought> body'
16
+ const { reasoning, text } = splitReasoning(input)
17
+
18
+ expect(reasoning).toContain('a')
19
+ expect(reasoning).toContain('b')
20
+ expect(reasoning).toContain('c')
21
+ expect(text).toBe('body')
22
+ })
23
+
24
+ it('treats unclosed leading <think>… as reasoning (real reasoning-model stream)', () => {
25
+ const { reasoning, text } = splitReasoning('<think>still deciding')
26
+
27
+ expect(reasoning).toBe('still deciding')
28
+ expect(text).toBe('')
29
+ })
30
+
31
+ it('does not strip trailing prose after a stray mid-text <think> mention', () => {
32
+ // Regression for "TUI eats last paragraph of output": when the model
33
+ // emits a literal `<think>` somewhere in prose (quoted explanation, code
34
+ // example, partial stream-mid-tag), the trailing greedy unclosed-tag
35
+ // regex used to consume every paragraph after it. Real unclosed
36
+ // reasoning blocks always lead the message — anchor to ^ so prose
37
+ // mentions are preserved.
38
+ const { reasoning, text } = splitReasoning(
39
+ 'final answer paragraph one.\n\n<think>internal note never closed\n\nfinal answer paragraph two.'
40
+ )
41
+
42
+ expect(reasoning).toBe('')
43
+ expect(text).toBe('final answer paragraph one.\n\n<think>internal note never closed\n\nfinal answer paragraph two.')
44
+ })
45
+
46
+ it('returns empty reasoning and untouched text when no tags present', () => {
47
+ const { reasoning, text } = splitReasoning('plain body with no tags')
48
+
49
+ expect(reasoning).toBe('')
50
+ expect(text).toBe('plain body with no tags')
51
+ })
52
+
53
+ it('preserves text when reasoning block is empty', () => {
54
+ const { reasoning, text } = splitReasoning('<think></think>only body')
55
+
56
+ expect(reasoning).toBe('')
57
+ expect(text).toBe('only body')
58
+ })
59
+
60
+ it('detects presence of any supported tag', () => {
61
+ expect(hasReasoningTag('pre <think>x</think> post')).toBe(true)
62
+ expect(hasReasoningTag('pre <reasoning>x</reasoning>')).toBe(true)
63
+ expect(hasReasoningTag('<REASONING_SCRATCHPAD>x</REASONING_SCRATCHPAD>')).toBe(true)
64
+ expect(hasReasoningTag('no tags at all')).toBe(false)
65
+ })
66
+ })
67
+
68
+ describe('cleanThinkingText', () => {
69
+ it('removes face/status ticker fragments while preserving real reasoning', () => {
70
+ expect(
71
+ cleanThinkingText(
72
+ '(¬_¬) synthesizing...**Resolving comments on GitHub**\n( ͡° ͜ʖ ͡°) musing...\nActual step\n٩(๑❛ᴗ❛๑)۶ contemplating...next step'
73
+ )
74
+ ).toBe('**Resolving comments on GitHub**\nActual step\nnext step')
75
+ })
76
+ })
@@ -0,0 +1,27 @@
1
+ import { describe, expect, it } from 'vitest'
2
+
3
+ import { asRpcResult, rpcErrorMessage } from '../lib/rpc.js'
4
+
5
+ describe('asRpcResult', () => {
6
+ it('keeps plain object payloads', () => {
7
+ expect(asRpcResult({ ok: true, value: 'x' })).toEqual({ ok: true, value: 'x' })
8
+ })
9
+
10
+ it('rejects missing or non-object payloads', () => {
11
+ expect(asRpcResult(undefined)).toBeNull()
12
+ expect(asRpcResult(null)).toBeNull()
13
+ expect(asRpcResult('oops')).toBeNull()
14
+ expect(asRpcResult(['bad'])).toBeNull()
15
+ })
16
+ })
17
+
18
+ describe('rpcErrorMessage', () => {
19
+ it('prefers Error messages', () => {
20
+ expect(rpcErrorMessage(new Error('boom'))).toBe('boom')
21
+ })
22
+
23
+ it('falls back for unknown errors', () => {
24
+ expect(rpcErrorMessage('broken')).toBe('broken')
25
+ expect(rpcErrorMessage({ code: 500 })).toBe('request failed')
26
+ })
27
+ })
@@ -0,0 +1,99 @@
1
+ import { describe, expect, it, vi } from 'vitest'
2
+
3
+ import { scrollWithSelectionBy } from '../app/scroll.js'
4
+
5
+ function makeScroll(overrides: Partial<Record<string, unknown>> = {}) {
6
+ const getScrollHeight = (overrides.getScrollHeight as (() => number) | undefined) ?? vi.fn(() => 100)
7
+
8
+ return {
9
+ getFreshScrollHeight: vi.fn(() => getScrollHeight()),
10
+ getPendingDelta: vi.fn(() => 0),
11
+ getScrollHeight,
12
+ getScrollTop: vi.fn(() => 10),
13
+ getViewportHeight: vi.fn(() => 20),
14
+ getViewportTop: vi.fn(() => 0),
15
+ scrollBy: vi.fn(),
16
+ ...overrides
17
+ }
18
+ }
19
+
20
+ describe('scrollWithSelectionBy', () => {
21
+ it('clamps to the actual remaining scroll distance before calling scrollBy', () => {
22
+ const s = makeScroll({
23
+ getScrollHeight: vi.fn(() => 30),
24
+ getScrollTop: vi.fn(() => 9),
25
+ getViewportHeight: vi.fn(() => 20)
26
+ })
27
+
28
+ const selection = {
29
+ captureScrolledRows: vi.fn(),
30
+ getState: vi.fn(() => null),
31
+ shiftAnchor: vi.fn(),
32
+ shiftSelection: vi.fn()
33
+ }
34
+
35
+ scrollWithSelectionBy(10, { scrollRef: { current: s as never }, selection })
36
+
37
+ expect(s.scrollBy).toHaveBeenCalledWith(1)
38
+ })
39
+
40
+ it('uses fresh scroll height when cached height would swallow a down-scroll at a fake bottom', () => {
41
+ const s = makeScroll({
42
+ getFreshScrollHeight: vi.fn(() => 34),
43
+ getScrollHeight: vi.fn(() => 30),
44
+ getScrollTop: vi.fn(() => 10),
45
+ getViewportHeight: vi.fn(() => 20)
46
+ })
47
+
48
+ const selection = {
49
+ captureScrolledRows: vi.fn(),
50
+ getState: vi.fn(() => null),
51
+ shiftAnchor: vi.fn(),
52
+ shiftSelection: vi.fn()
53
+ }
54
+
55
+ scrollWithSelectionBy(10, { scrollRef: { current: s as never }, selection })
56
+
57
+ expect(s.scrollBy).toHaveBeenCalledWith(4)
58
+ })
59
+
60
+ it('uses fresh height when pending down-scroll reaches the cached fake bottom', () => {
61
+ const s = makeScroll({
62
+ getFreshScrollHeight: vi.fn(() => 38),
63
+ getPendingDelta: vi.fn(() => 2),
64
+ getScrollHeight: vi.fn(() => 32),
65
+ getScrollTop: vi.fn(() => 10),
66
+ getViewportHeight: vi.fn(() => 20)
67
+ })
68
+
69
+ const selection = {
70
+ captureScrolledRows: vi.fn(),
71
+ getState: vi.fn(() => null),
72
+ shiftAnchor: vi.fn(),
73
+ shiftSelection: vi.fn()
74
+ }
75
+
76
+ scrollWithSelectionBy(10, { scrollRef: { current: s as never }, selection })
77
+
78
+ expect(s.scrollBy).toHaveBeenCalledWith(6)
79
+ })
80
+
81
+ it('does nothing at the edge instead of queueing dead pending deltas', () => {
82
+ const s = makeScroll({
83
+ getScrollHeight: vi.fn(() => 30),
84
+ getScrollTop: vi.fn(() => 10),
85
+ getViewportHeight: vi.fn(() => 20)
86
+ })
87
+
88
+ const selection = {
89
+ captureScrolledRows: vi.fn(),
90
+ getState: vi.fn(() => null),
91
+ shiftAnchor: vi.fn(),
92
+ shiftSelection: vi.fn()
93
+ }
94
+
95
+ scrollWithSelectionBy(10, { scrollRef: { current: s as never }, selection })
96
+
97
+ expect(s.scrollBy).not.toHaveBeenCalled()
98
+ })
99
+ })
@@ -0,0 +1,123 @@
1
+ import { execFileSync } from 'node:child_process'
2
+ import { dirname, resolve } from 'node:path'
3
+ import { fileURLToPath } from 'node:url'
4
+
5
+ import { describe, expect, it } from 'vitest'
6
+
7
+ import { findSlashCommand, SLASH_COMMANDS } from '../app/slash/registry.js'
8
+
9
+ type CommandRoute = 'fallback' | 'local' | 'native'
10
+
11
+ interface CommandRegistryLoad {
12
+ error?: string
13
+ names: string[]
14
+ }
15
+
16
+ const NATIVE_MUTATING_COMMANDS = new Set(['browser', 'busy', 'fast', 'reload-mcp', 'rollback', 'stop'])
17
+
18
+ const MUTATING_COMMANDS = [
19
+ 'background',
20
+ 'branch',
21
+ 'browser',
22
+ 'busy',
23
+ 'clear',
24
+ 'compress',
25
+ 'fast',
26
+ 'model',
27
+ 'new',
28
+ 'personality',
29
+ 'queue',
30
+ 'reasoning',
31
+ 'reload-mcp',
32
+ 'retry',
33
+ 'rollback',
34
+ 'steer',
35
+ 'stop',
36
+ 'title',
37
+ 'tools',
38
+ 'undo',
39
+ 'verbose',
40
+ 'voice',
41
+ 'yolo'
42
+ ] as const
43
+
44
+ const loadCommandRegistryNames = (): CommandRegistryLoad => {
45
+ const here = dirname(fileURLToPath(import.meta.url))
46
+
47
+ try {
48
+ const names = JSON.parse(
49
+ execFileSync(
50
+ process.env.PYTHON ?? 'python3',
51
+ [
52
+ '-c',
53
+ 'import json; from nastech_cli.commands import COMMAND_REGISTRY; print(json.dumps([c.name for c in COMMAND_REGISTRY]))'
54
+ ],
55
+ { cwd: resolve(here, '../../..'), encoding: 'utf8' }
56
+ )
57
+ ) as string[]
58
+
59
+ return { names: [...new Set(names)] }
60
+ } catch (error) {
61
+ return {
62
+ error: error instanceof Error ? error.message : String(error),
63
+ names: []
64
+ }
65
+ }
66
+ }
67
+
68
+ const commandRegistry = loadCommandRegistryNames()
69
+ const registryIt = commandRegistry.error ? it.skip : it
70
+ const skipReason = commandRegistry.error ? commandRegistry.error.split('\n')[0] : ''
71
+
72
+ const LOCAL_COMMAND_NAMES = new Set(
73
+ SLASH_COMMANDS.flatMap(command => [command.name, ...(command.aliases ?? [])].map(name => name.toLowerCase()))
74
+ )
75
+
76
+ const classifyRoute = (name: string): CommandRoute => {
77
+ const normalized = name.toLowerCase()
78
+
79
+ if (NATIVE_MUTATING_COMMANDS.has(normalized)) {
80
+ return 'native'
81
+ }
82
+
83
+ if (LOCAL_COMMAND_NAMES.has(normalized)) {
84
+ return 'local'
85
+ }
86
+
87
+ return 'fallback'
88
+ }
89
+
90
+ describe('slash parity matrix', () => {
91
+ if (commandRegistry.error) {
92
+ it.skip(`Python command registry unavailable: ${skipReason}`, () => {})
93
+ }
94
+
95
+ registryIt('classifies each command registry command as local/native/fallback', () => {
96
+ const routes = Object.fromEntries(commandRegistry.names.map(name => [name, classifyRoute(name)]))
97
+
98
+ expect(routes['model']).toBe('local')
99
+ expect(routes['browser']).toBe('native')
100
+ expect(routes['reload-mcp']).toBe('native')
101
+ expect(routes['rollback']).toBe('native')
102
+ expect(routes['stop']).toBe('native')
103
+ })
104
+
105
+ registryIt('keeps every mutating command off slash-worker fallback', () => {
106
+ const routes = Object.fromEntries(commandRegistry.names.map(name => [name, classifyRoute(name)]))
107
+
108
+ for (const name of MUTATING_COMMANDS) {
109
+ expect(routes[name], `missing command in registry: ${name}`).toBeDefined()
110
+ expect(routes[name], `mutating command must not fallback: ${name}`).not.toBe('fallback')
111
+ }
112
+ })
113
+
114
+ it('/q alias resolves to queue, not quit (#31983)', () => {
115
+ // Regression for #31983: the TUI `quit` command used to carry alias `q`,
116
+ // which collided with the Python-side `/queue` alias. TUI-local commands
117
+ // dispatch before the backend, so `/q` resolved to /quit (session.die)
118
+ // instead of queueing a prompt.
119
+ const cmd = findSlashCommand('q')
120
+ expect(cmd, '/q must resolve to a command').toBeDefined()
121
+ expect(cmd!.name).toBe('queue')
122
+ })
123
+ })
@@ -0,0 +1,46 @@
1
+ import { beforeEach, describe, expect, it } from 'vitest'
2
+
3
+ import { clearSpawnHistory, getSpawnHistory, pushDiskSnapshot } from '../app/spawnHistoryStore.js'
4
+
5
+ describe('spawnHistoryStore status normalization', () => {
6
+ beforeEach(() => {
7
+ clearSpawnHistory()
8
+ })
9
+
10
+ it('keeps timeout/error statuses from disk snapshots', () => {
11
+ pushDiskSnapshot(
12
+ {
13
+ finished_at: 1_700_000_001,
14
+ label: 'status test',
15
+ session_id: 'sess-1',
16
+ started_at: 1_700_000_000,
17
+ subagents: [
18
+ { goal: 'timeout child', id: 'sa-timeout', index: 0, status: 'timeout' },
19
+ { goal: 'error child', id: 'sa-error', index: 1, status: 'error' }
20
+ ]
21
+ },
22
+ '/tmp/snap-timeout-error.json'
23
+ )
24
+
25
+ const statuses = getSpawnHistory()[0]?.subagents.map(s => s.status)
26
+
27
+ expect(statuses).toEqual(['timeout', 'error'])
28
+ })
29
+
30
+ it('falls back unknown disk statuses to completed', () => {
31
+ pushDiskSnapshot(
32
+ {
33
+ finished_at: 1_700_000_011,
34
+ label: 'unknown status test',
35
+ session_id: 'sess-2',
36
+ started_at: 1_700_000_010,
37
+ subagents: [{ goal: 'mystery child', id: 'sa-unknown', index: 0, status: 'mystery_status' }]
38
+ },
39
+ '/tmp/snap-unknown.json'
40
+ )
41
+
42
+ const status = getSpawnHistory()[0]?.subagents[0]?.status
43
+
44
+ expect(status).toBe('completed')
45
+ })
46
+ })
@@ -0,0 +1,46 @@
1
+ import { beforeEach, describe, expect, it } from 'vitest'
2
+
3
+ import { patchTurnState, resetTurnState } from '../app/turnStore.js'
4
+ import { $uiState, resetUiState } from '../app/uiStore.js'
5
+
6
+ const shallowEqual = <T extends Record<string, unknown>>(a: T, b: T) =>
7
+ Object.keys(a).length === Object.keys(b).length && Object.keys(a).every(key => Object.is(a[key], b[key]))
8
+
9
+ const subscribeSelected = <T extends Record<string, unknown>>(selector: () => T) => {
10
+ let current = selector()
11
+ let calls = 0
12
+
13
+ const unsubscribe = $uiState.listen(() => {
14
+ const next = selector()
15
+
16
+ if (shallowEqual(next, current)) {
17
+ return
18
+ }
19
+
20
+ current = next
21
+ calls++
22
+ })
23
+
24
+ return { calls: () => calls, unsubscribe }
25
+ }
26
+
27
+ describe('TUI state isolation', () => {
28
+ beforeEach(() => {
29
+ resetUiState()
30
+ resetTurnState()
31
+ })
32
+
33
+ it('does not notify ui/composer subscribers for high-frequency turn updates', () => {
34
+ const composerRelevant = subscribeSelected(() => ({ busy: $uiState.get().busy, sid: $uiState.get().sid }))
35
+
36
+ try {
37
+ for (let i = 0; i < 50; i++) {
38
+ patchTurnState({ streaming: `token ${i}` })
39
+ }
40
+ } finally {
41
+ composerRelevant.unsubscribe()
42
+ }
43
+
44
+ expect(composerRelevant.calls()).toBe(0)
45
+ })
46
+ })
@@ -0,0 +1,18 @@
1
+ import { describe, expect, it } from 'vitest'
2
+
3
+ import { padVerb, VERB_PAD_LEN } from '../components/appChrome.js'
4
+ import { VERBS } from '../content/verbs.js'
5
+
6
+ describe('FaceTicker verb padding', () => {
7
+ it('pads every verb to the same width', () => {
8
+ for (const verb of VERBS) {
9
+ expect(padVerb(verb)).toHaveLength(VERB_PAD_LEN)
10
+ }
11
+ })
12
+
13
+ it('keeps trailing ellipsis attached', () => {
14
+ for (const verb of VERBS) {
15
+ expect(padVerb(verb).startsWith(`${verb}…`)).toBe(true)
16
+ }
17
+ })
18
+ })
@@ -0,0 +1,32 @@
1
+ import { describe, expect, it } from 'vitest'
2
+
3
+ import { statusRuleWidths } from '../components/appChrome.js'
4
+
5
+ describe('statusRuleWidths', () => {
6
+ it('keeps the status rule within the terminal width', () => {
7
+ for (const cols of [8, 12, 20, 40, 100]) {
8
+ const widths = statusRuleWidths(cols, '~/src/nastech-agent/main (some-long-branch-name)')
9
+
10
+ expect(widths.leftWidth + widths.separatorWidth + widths.rightWidth).toBeLessThanOrEqual(cols)
11
+ expect(widths.leftWidth).toBeGreaterThan(0)
12
+ }
13
+ })
14
+
15
+ it('truncates the cwd segment before it can wrap in skinny terminals', () => {
16
+ const widths = statusRuleWidths(24, '~/src/nastech-agent/main (bb/some-extremely-long-branch)')
17
+
18
+ expect(widths.rightWidth).toBeLessThan('~/src/nastech-agent/main (bb/some-extremely-long-branch)'.length)
19
+ expect(widths.leftWidth).toBeGreaterThanOrEqual(8)
20
+ })
21
+
22
+ it('omits the cwd segment when there is no room for it', () => {
23
+ expect(statusRuleWidths(2, 'abcdef')).toEqual({ leftWidth: 2, rightWidth: 0, separatorWidth: 0 })
24
+ })
25
+
26
+ it('budgets the cwd segment by display width, not utf-16 length', () => {
27
+ const widths = statusRuleWidths(30, '目录/分支')
28
+
29
+ expect(widths.leftWidth + widths.separatorWidth + widths.rightWidth).toBeLessThanOrEqual(30)
30
+ expect(widths.rightWidth).toBeGreaterThan('目录/分支'.length)
31
+ })
32
+ })
@@ -0,0 +1,121 @@
1
+ import { describe, expect, it } from 'vitest'
2
+
3
+ import { findStableBoundary } from '../components/streamingMarkdown.js'
4
+ // We test the pure boundary logic by rendering the component's ref
5
+ // behaviour through repeated calls. Since React isn't being rendered here,
6
+ // we reach into the module to test findStableBoundary via its exported
7
+ // behaviour — but the pure helper isn't exported. So test the component's
8
+ // observable output: pass sequential text values and verify the stable
9
+ // prefix never retreats.
10
+ //
11
+ // Strategy: mount StreamingMd in isolation and observe which <Md>
12
+ // instances it renders (by text prop). Without a DOM renderer that's
13
+ // heavy, so we validate the helper behaviour by directly invoking the
14
+ // fence/boundary logic via a re-exported surface.
15
+ import { DEFAULT_THEME } from '../theme.js'
16
+
17
+ describe('findStableBoundary', () => {
18
+ it('returns -1 when no blank line exists yet', () => {
19
+ expect(findStableBoundary('partial line with no newline yet')).toBe(-1)
20
+ })
21
+
22
+ it('returns -1 when only single newlines exist', () => {
23
+ expect(findStableBoundary('line one\nline two\nline three')).toBe(-1)
24
+ })
25
+
26
+ it('splits after the last blank line separator', () => {
27
+ // 'first\n\nsecond\n\nthird' → last blank = before 'third'
28
+ const text = 'first paragraph\n\nsecond paragraph\n\nthird'
29
+ const idx = findStableBoundary(text)
30
+
31
+ expect(text.slice(0, idx)).toBe('first paragraph\n\nsecond paragraph\n\n')
32
+ expect(text.slice(idx)).toBe('third')
33
+ })
34
+
35
+ it('refuses to split inside an open fenced block', () => {
36
+ // Fence opens, contains a blank line inside the code, no close yet.
37
+ const text = '```ts\nfn();\n\nmore code here'
38
+
39
+ expect(findStableBoundary(text)).toBe(-1)
40
+ })
41
+
42
+ it('splits before an open fenced block but not inside', () => {
43
+ const text = 'intro paragraph\n\n```ts\nfn();\n\nmore code'
44
+ const idx = findStableBoundary(text)
45
+
46
+ expect(text.slice(0, idx)).toBe('intro paragraph\n\n')
47
+ expect(text.slice(idx).startsWith('```ts')).toBe(true)
48
+ })
49
+
50
+ it('allows splitting after a fenced block closes', () => {
51
+ const text = '```ts\nfn();\n```\n\nnarration continues'
52
+ const idx = findStableBoundary(text)
53
+
54
+ expect(text.slice(0, idx)).toBe('```ts\nfn();\n```\n\n')
55
+ expect(text.slice(idx)).toBe('narration continues')
56
+ })
57
+
58
+ it('walks backwards through nested fence boundaries safely', () => {
59
+ // Two closed fences + narration + one new open fence. The only legal
60
+ // split is before the open fence, not between the closed ones.
61
+ const text = '```js\na\n```\n\nmid text\n\n```python\nstill open'
62
+ const idx = findStableBoundary(text)
63
+
64
+ expect(text.slice(0, idx)).toBe('```js\na\n```\n\nmid text\n\n')
65
+ })
66
+
67
+ it('handles empty input', () => {
68
+ expect(findStableBoundary('')).toBe(-1)
69
+ })
70
+
71
+ it('refuses to split inside an open $$ math block', () => {
72
+ // Display math has been opened but not closed; the only blank line
73
+ // sits inside the open block, so there's no safe boundary yet.
74
+ const text = '$$\nx + y\n\nmore math'
75
+
76
+ expect(findStableBoundary(text)).toBe(-1)
77
+ })
78
+
79
+ it('allows splitting after a $$ math block closes', () => {
80
+ const text = '$$\nx + y = z\n$$\n\nnarration continues'
81
+ const idx = findStableBoundary(text)
82
+
83
+ expect(text.slice(0, idx)).toBe('$$\nx + y = z\n$$\n\n')
84
+ expect(text.slice(idx)).toBe('narration continues')
85
+ })
86
+
87
+ it('splits before an open $$ block but not inside', () => {
88
+ // Mirror of the existing fenced-code test: prose, then an unclosed
89
+ // math block. The only safe boundary is the blank line BEFORE `$$`.
90
+ const text = 'intro paragraph\n\n$$\nx + y\n\nmore'
91
+ const idx = findStableBoundary(text)
92
+
93
+ expect(text.slice(0, idx)).toBe('intro paragraph\n\n')
94
+ expect(text.slice(idx).startsWith('$$')).toBe(true)
95
+ })
96
+
97
+ it('treats single-line $$x$$ as zero net toggle', () => {
98
+ // `$$x = y$$` opens AND closes on one line, so the stable boundary
99
+ // after it is allowed.
100
+ const text = 'intro\n\n$$x = y$$\n\nnarration'
101
+ const idx = findStableBoundary(text)
102
+
103
+ expect(text.slice(0, idx)).toBe('intro\n\n$$x = y$$\n\n')
104
+ expect(text.slice(idx)).toBe('narration')
105
+ })
106
+
107
+ it('refuses to split inside an open \\[ math block', () => {
108
+ const text = '\\[\nx + y\n\nmore'
109
+
110
+ expect(findStableBoundary(text)).toBe(-1)
111
+ })
112
+ })
113
+
114
+ describe('streaming theme assumption', () => {
115
+ it('theme is exportable (component import sanity check)', () => {
116
+ // Sanity that the theme we pass doesn't change shape. Component import
117
+ // already happens above — this is a smoke test that the module graph
118
+ // for streamingMarkdown wires up without cycles.
119
+ expect(DEFAULT_THEME.color.accent).toBeTruthy()
120
+ })
121
+ })