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,106 @@
1
+ import { type AnsiCode, ansiCodesToString, reduceAnsiCodes, tokenize, undoAnsiCodes } from '@alcalzone/ansi-tokenize'
2
+
3
+ import { lruEvict } from '../ink/lru.js'
4
+ import { stringWidth } from '../ink/stringWidth.js'
5
+
6
+ function isEndCode(code: AnsiCode): boolean {
7
+ return code.code === code.endCode
8
+ }
9
+
10
+ function filterStartCodes(codes: AnsiCode[]): AnsiCode[] {
11
+ return codes.filter(c => !isEndCode(c))
12
+ }
13
+
14
+ // LRU cache: same (string, start, end) → same output. Output.get() re-emits
15
+ // identical writes every frame for stable transcript content; this avoids
16
+ // re-tokenizing them. CPU profile (Apr 2026) showed sliceAnsi at 18% total
17
+ // time during scroll. Bounded at 4096 entries — entries are short clipped
18
+ // lines so memory cost is small.
19
+ const sliceCache = new Map<string, string>()
20
+ const SLICE_CACHE_LIMIT = 4096
21
+
22
+ export default function sliceAnsi(str: string, start: number, end?: number): string {
23
+ if (!str) {
24
+ return ''
25
+ }
26
+
27
+ // Hot-path: only cache when end is defined (the Output.get() use-case).
28
+ if (end !== undefined) {
29
+ const key = `${start}|${end}|${str}`
30
+ const cached = sliceCache.get(key)
31
+
32
+ if (cached !== undefined) {
33
+ sliceCache.delete(key)
34
+ sliceCache.set(key, cached)
35
+
36
+ return cached
37
+ }
38
+
39
+ const result = computeSlice(str, start, end)
40
+
41
+ if (sliceCache.size >= SLICE_CACHE_LIMIT) {
42
+ sliceCache.delete(sliceCache.keys().next().value!)
43
+ }
44
+
45
+ sliceCache.set(key, result)
46
+
47
+ return result
48
+ }
49
+
50
+ return computeSlice(str, start, end)
51
+ }
52
+
53
+ export function sliceCacheSize(): number {
54
+ return sliceCache.size
55
+ }
56
+
57
+ export function evictSliceCache(keepRatio = 0): void {
58
+ lruEvict(sliceCache, keepRatio)
59
+ }
60
+
61
+ function computeSlice(str: string, start: number, end?: number): string {
62
+ const tokens = tokenize(str)
63
+ let activeCodes: AnsiCode[] = []
64
+ let position = 0
65
+ let result = ''
66
+ let include = false
67
+
68
+ for (const token of tokens) {
69
+ const width = token.type === 'ansi' ? 0 : token.fullWidth ? 2 : stringWidth(token.value)
70
+
71
+ if (end !== undefined && position >= end) {
72
+ if (token.type === 'ansi' || width > 0 || !include) {
73
+ break
74
+ }
75
+ }
76
+
77
+ if (token.type === 'ansi') {
78
+ activeCodes.push(token)
79
+
80
+ if (include) {
81
+ result += token.code
82
+ }
83
+ } else {
84
+ if (!include && position >= start) {
85
+ if (start > 0 && width === 0) {
86
+ continue
87
+ }
88
+
89
+ include = true
90
+ activeCodes = filterStartCodes(reduceAnsiCodes(activeCodes))
91
+ result = ansiCodesToString(activeCodes)
92
+ }
93
+
94
+ if (include) {
95
+ result += token.value
96
+ }
97
+
98
+ position += width
99
+ }
100
+ }
101
+
102
+ const activeStartCodes = filterStartCodes(reduceAnsiCodes(activeCodes))
103
+ result += ansiCodesToString(undoAnsiCodes(activeStartCodes))
104
+
105
+ return result
106
+ }
@@ -0,0 +1,2 @@
1
+ export { default, UncontrolledTextInput } from 'ink-text-input'
2
+ export type { Props } from 'ink-text-input'
@@ -0,0 +1 @@
1
+ export { default, UncontrolledTextInput } from 'ink-text-input'
@@ -0,0 +1,61 @@
1
+ #!/usr/bin/env node
2
+ // Bundles src/entry.tsx into a single self-contained dist/entry.js.
3
+ // No runtime node_modules needed.
4
+ import { build } from 'esbuild'
5
+ import { readFileSync, writeFileSync } from 'node:fs'
6
+ import { fileURLToPath } from 'node:url'
7
+ import { dirname, resolve } from 'node:path'
8
+
9
+ const here = dirname(fileURLToPath(import.meta.url))
10
+ const root = resolve(here, '..')
11
+ const out = resolve(root, 'dist/entry.js')
12
+
13
+ // `react-devtools-core` is only imported when DEV=true at runtime (Ink dev
14
+ // mode). Stub it out so the bundle doesn't carry the dep.
15
+ const stubDevtools = {
16
+ name: 'stub-react-devtools-core',
17
+ setup(b) {
18
+ b.onResolve({ filter: /^react-devtools-core$/ }, args => ({
19
+ path: args.path,
20
+ namespace: 'stub-devtools'
21
+ }))
22
+ b.onLoad({ filter: /.*/, namespace: 'stub-devtools' }, () => ({
23
+ contents: 'export default { initialize() {}, connectToDevTools() {} }',
24
+ loader: 'js'
25
+ }))
26
+ }
27
+ }
28
+
29
+ await build({
30
+ entryPoints: [resolve(root, 'src/entry.tsx')],
31
+ bundle: true,
32
+ platform: 'node',
33
+ format: 'esm',
34
+ target: 'node20',
35
+ outfile: out,
36
+ jsx: 'automatic',
37
+ jsxImportSource: 'react',
38
+ // Skip the prebuilt @nastech/ink bundle — esbuild's __esm helper doesn't
39
+ // await nested async init, which breaks lazy-initialized exports like
40
+ // `render`. Bundling from source sidesteps that.
41
+ alias: { '@nastech/ink': resolve(root, 'packages/nastech-ink/src/entry-exports.ts') },
42
+ plugins: [stubDevtools],
43
+ // Some transitive deps use CommonJS `require(...)` at runtime. ESM bundles
44
+ // don't get a `require` binding automatically, so we inject one.
45
+ banner: {
46
+ js: "import { createRequire as __cr } from 'node:module'; const require = __cr(import.meta.url);"
47
+ },
48
+ logLevel: 'info'
49
+ })
50
+
51
+ // esbuild preserves the shebang from src/entry.tsx into the bundle, but Nix's
52
+ // patchShebangs phase mangles `/usr/bin/env -S node --foo --bar` (it strips
53
+ // the `node` token, leaving a broken interpreter). The nastech_cli launcher
54
+ // always invokes this file as `node dist/entry.js` anyway, so the shebang is
55
+ // redundant — strip it.
56
+ const body = readFileSync(out, 'utf8')
57
+ if (body.startsWith('#!')) {
58
+ writeFileSync(out, body.slice(body.indexOf('\n') + 1))
59
+ }
60
+
61
+ console.log(`built ${out}`)
@@ -0,0 +1,121 @@
1
+ #!/usr/bin/env node
2
+ /* global Buffer, console, process, setImmediate */
3
+ import inspector from 'node:inspector'
4
+ import { performance } from 'node:perf_hooks'
5
+
6
+ import React from 'react'
7
+ import { render } from '@nastech/ink'
8
+ import { AppLayout } from '../src/components/appLayout.tsx'
9
+ import { resetOverlayState } from '../src/app/overlayStore.ts'
10
+ import { resetTurnState } from '../src/app/turnStore.ts'
11
+ import { resetUiState } from '../src/app/uiStore.ts'
12
+
13
+ const session = new inspector.Session()
14
+ session.connect()
15
+ const post = (method, params = {}) => new Promise((resolve, reject) => {
16
+ session.post(method, params, (err, result) => err ? reject(err) : resolve(result))
17
+ })
18
+
19
+ const historySize = Number(process.env.HISTORY || 500)
20
+ const mountedRows = Number(process.env.MOUNTED || 120)
21
+
22
+ class Sink {
23
+ columns = Number(process.env.COLS || 120)
24
+ rows = Number(process.env.ROWS || 42)
25
+ isTTY = true
26
+ bytes = 0
27
+ writes = 0
28
+ listeners = new Map()
29
+ write(chunk) {
30
+ this.bytes += Buffer.byteLength(String(chunk ?? ''))
31
+ this.writes++
32
+ return true
33
+ }
34
+ on(event, fn) { this.listeners.set(event, fn); return this }
35
+ off(event) { this.listeners.delete(event); return this }
36
+ once(event, fn) { this.listeners.set(event, fn); return this }
37
+ removeListener(event) { this.listeners.delete(event); return this }
38
+ }
39
+
40
+ const theme = {
41
+ brand: { prompt: '›' },
42
+ color: {
43
+ amber: '#d19a66', bronze: '#8b6f47', dim: '#6b7280', error: '#ff5555', gold: '#ffd166', label: '#61afef',
44
+ ok: '#98c379', warn: '#e5c07b', cornsilk: '#fff8dc', prompt: '#c678dd', shellDollar: '#98c379',
45
+ statusCritical: '#ff5555', statusBad: '#e06c75', statusWarn: '#e5c07b', statusGood: '#98c379',
46
+ selectionBg: '#44475a'
47
+ }
48
+ }
49
+
50
+ const noop = () => {}
51
+ const historyItems = [
52
+ { kind: 'intro', role: 'system', text: '', info: { model: 'test', tools: {}, skills: {}, version: 'test' } },
53
+ ...Array.from({ length: historySize }, (_, i) => ({
54
+ role: i % 5 === 0 ? 'user' : 'assistant',
55
+ text: `message ${i}\n${'lorem ipsum '.repeat(80)}`
56
+ }))
57
+ ]
58
+ const scrollRef = { current: {
59
+ getScrollTop: () => 0,
60
+ getPendingDelta: () => 0,
61
+ getScrollHeight: () => historySize * 4,
62
+ getViewportHeight: () => 30,
63
+ getViewportTop: () => 0,
64
+ isSticky: () => true,
65
+ subscribe: () => () => {},
66
+ scrollBy: noop,
67
+ scrollTo: noop,
68
+ scrollToBottom: noop,
69
+ setClampBounds: noop,
70
+ getLastManualScrollAt: () => 0
71
+ } }
72
+
73
+ const baseProps = streamingText => ({
74
+ actions: { answerApproval: noop, answerClarify: noop, answerSecret: noop, answerSudo: noop, onModelSelect: noop, resumeById: noop, setStickyPrompt: noop },
75
+ composer: { cols: 120, compIdx: 0, completions: [], empty: false, handleTextPaste: () => null, input: '', inputBuf: [], pagerPageSize: 10, queueEditIdx: null, queuedDisplay: [], submit: noop, updateInput: noop },
76
+ mouseTracking: false,
77
+ progress: {
78
+ activity: [], outcome: '', reasoning: streamingText, reasoningActive: true, reasoningStreaming: true,
79
+ reasoningTokens: Math.ceil(streamingText.length / 4), showProgressArea: true, showStreamingArea: true,
80
+ streamPendingTools: [], streamSegments: [], streaming: streamingText, subagents: [], toolTokens: 0, tools: [], turnTrail: [], todos: []
81
+ },
82
+ status: { cwdLabel: '~/repo', goodVibesTick: 0, sessionStartedAt: Date.now(), showStickyPrompt: false, statusColor: theme.color.ok, stickyPrompt: '', turnStartedAt: Date.now(), voiceLabel: 'voice off' },
83
+ transcript: {
84
+ historyItems,
85
+ scrollRef,
86
+ virtualHistory: { bottomSpacer: 0, end: historyItems.length, measureRef: () => noop, offsets: historyItems.map((_, i) => i * 4), start: Math.max(0, historyItems.length - mountedRows), topSpacer: 0 },
87
+ virtualRows: historyItems.map((msg, index) => ({ index, key: `m${index}`, msg }))
88
+ }
89
+ })
90
+
91
+ async function main() {
92
+ resetUiState()
93
+ resetTurnState()
94
+ resetOverlayState()
95
+ const stdout = new Sink()
96
+ const stdin = { isTTY: true, setRawMode: noop, on: noop, off: noop, resume: noop, pause: noop }
97
+ const text = Array.from({ length: Number(process.env.LINES || 1200) }, (_, i) => `stream line ${i} ${'x'.repeat(90)}`).join('\n')
98
+ const inst = render(React.createElement(AppLayout, baseProps('')), { stdout, stdin, stderr: stdout, debug: false, exitOnCtrlC: false })
99
+
100
+ await post('Profiler.enable')
101
+ await post('HeapProfiler.enable')
102
+ await post('Profiler.start')
103
+ const startMem = process.memoryUsage()
104
+ const t0 = performance.now()
105
+ const iterations = Number(process.env.ITERS || 40)
106
+ for (let i = 1; i <= iterations; i++) {
107
+ const prefix = text.slice(0, Math.floor(text.length * i / iterations))
108
+ inst.rerender(React.createElement(AppLayout, baseProps(prefix)))
109
+ await new Promise(r => setImmediate(r))
110
+ }
111
+ const elapsed = performance.now() - t0
112
+ const prof = await post('Profiler.stop')
113
+ const endMem = process.memoryUsage()
114
+ await post('HeapProfiler.collectGarbage')
115
+ const afterGc = process.memoryUsage()
116
+ inst.unmount()
117
+ session.disconnect()
118
+ console.log(JSON.stringify({ elapsedMs: Math.round(elapsed), stdoutBytes: stdout.bytes, stdoutWrites: stdout.writes, startMem, endMem, afterGc, profileNodes: prof.profile.nodes.length }, null, 2))
119
+ }
120
+
121
+ main().catch(err => { console.error(err); process.exit(1) })
@@ -0,0 +1,157 @@
1
+ import { describe, expect, it } from 'vitest'
2
+
3
+ import { DEFAULT_THEME } from '../theme.js'
4
+ import type { SessionActiveItem } from '../gatewayTypes.js'
5
+ import {
6
+ activeSessionCountLabel,
7
+ canTypeOrchestratorPrompt,
8
+ currentSessionSelectionIndex,
9
+ orchestratorContextHint,
10
+ orchestratorContextHintSegments,
11
+ orchestratorGlobalHotkeyHint,
12
+ orchestratorGlobalHotkeyHintSegments,
13
+ orchestratorHintSegmentColor,
14
+ clampOrchestratorSelection,
15
+ closeFallbackAfterClose,
16
+ draftModelArgFromPickerValue,
17
+ draftModelDisplayLabel,
18
+ fixedSessionColumnStyle,
19
+ draftTitleFromPrompt,
20
+ isNewSessionRow,
21
+ newSessionMarkerColor,
22
+ newSessionRowIndex,
23
+ orchestratorRowClickAction,
24
+ orchestratorVisibleRowIndexes,
25
+ selectedSessionRowStyle
26
+ } from '../components/activeSessionSwitcher.js'
27
+
28
+ describe('session orchestrator helpers', () => {
29
+ it('labels live sessions compactly for tight overlays', () => {
30
+ expect(activeSessionCountLabel(0)).toBe('0 live sessions')
31
+ expect(activeSessionCountLabel(1)).toBe('1 live session')
32
+ expect(activeSessionCountLabel(3)).toBe('3 live sessions')
33
+ expect(activeSessionCountLabel(1)).not.toContain('in this TUI')
34
+ })
35
+
36
+ it('keeps session orchestrator hotkey hints short and contextual', () => {
37
+ expect(orchestratorContextHint(false)).toBe('Session row: Enter switch · Ctrl+D close')
38
+ expect(orchestratorContextHint(true)).toBe('New row: type prompt · Enter start · Tab model')
39
+ expect(orchestratorGlobalHotkeyHint).toBe('↑↓ move · Ctrl+N new · Ctrl+R refresh · Esc close')
40
+ expect(orchestratorGlobalHotkeyHint.length).toBeLessThanOrEqual(56)
41
+ })
42
+
43
+ it('assigns themed colors consistently to orchestrator labels and hotkeys', () => {
44
+ expect(orchestratorContextHintSegments(false)).toEqual([
45
+ { role: 'label', text: 'Session row:' },
46
+ { role: 'text', text: ' ' },
47
+ { role: 'hotkey', text: 'Enter' },
48
+ { role: 'text', text: ' switch · ' },
49
+ { role: 'hotkey', text: 'Ctrl+D' },
50
+ { role: 'text', text: ' close' }
51
+ ])
52
+ expect(orchestratorContextHintSegments(true)).toEqual([
53
+ { role: 'label', text: 'New row:' },
54
+ { role: 'text', text: ' type prompt · ' },
55
+ { role: 'hotkey', text: 'Enter' },
56
+ { role: 'text', text: ' start · ' },
57
+ { role: 'hotkey', text: 'Tab' },
58
+ { role: 'text', text: ' model' }
59
+ ])
60
+ expect(orchestratorGlobalHotkeyHintSegments.filter(s => s.role === 'hotkey').map(s => s.text)).toEqual([
61
+ '↑↓',
62
+ 'Ctrl+N',
63
+ 'Ctrl+R',
64
+ 'Esc'
65
+ ])
66
+ expect(orchestratorHintSegmentColor(DEFAULT_THEME, 'hotkey')).toBe(DEFAULT_THEME.color.accent)
67
+ expect(orchestratorHintSegmentColor(DEFAULT_THEME, 'label')).toBe(DEFAULT_THEME.color.label)
68
+ expect(orchestratorHintSegmentColor(DEFAULT_THEME, 'text')).toBe(DEFAULT_THEME.color.muted)
69
+ expect(newSessionMarkerColor(DEFAULT_THEME, false)).toBe(DEFAULT_THEME.color.label)
70
+ expect(newSessionMarkerColor(DEFAULT_THEME, true)).toBe(DEFAULT_THEME.color.text)
71
+ })
72
+
73
+ it('uses a readable selected row style instead of accent-on-accent inverse text', () => {
74
+ const style = selectedSessionRowStyle(DEFAULT_THEME)
75
+
76
+ expect(style.backgroundColor).toBe(DEFAULT_THEME.color.selectionBg)
77
+ expect(style.color).toBe(DEFAULT_THEME.color.text)
78
+ expect(style.backgroundColor).not.toBe(DEFAULT_THEME.color.accent)
79
+ expect(style.color).not.toBe(DEFAULT_THEME.color.accent)
80
+ })
81
+
82
+ it('turns model picker values into session-scoped draft model args', () => {
83
+ expect(draftModelArgFromPickerValue('kimi-k2.6 --provider ollama-cloud --tui-session')).toBe(
84
+ 'kimi-k2.6 --provider ollama-cloud'
85
+ )
86
+ expect(draftModelArgFromPickerValue('openai/gpt-5.5 --provider openai-codex --global')).toBe(
87
+ 'openai/gpt-5.5 --provider openai-codex'
88
+ )
89
+ })
90
+
91
+ it('highlights the current live session when the picker opens', () => {
92
+ const sessions = [
93
+ { id: 'first', status: 'idle' },
94
+ { id: 'second', status: 'working', current: true },
95
+ { id: 'third', status: 'idle' }
96
+ ] satisfies SessionActiveItem[]
97
+
98
+ expect(currentSessionSelectionIndex(sessions, 'second')).toBe(1)
99
+ expect(
100
+ currentSessionSelectionIndex([{ id: 'first', status: 'idle' }, { id: 'third', status: 'idle' }], 'third')
101
+ ).toBe(1)
102
+ expect(currentSessionSelectionIndex(sessions, 'missing')).toBe(1)
103
+ expect(currentSessionSelectionIndex([], 'missing')).toBe(0)
104
+ })
105
+
106
+ it('adds a selectable New row after the live sessions and gates prompt typing to it', () => {
107
+ expect(newSessionRowIndex(0)).toBe(0)
108
+ expect(newSessionRowIndex(3)).toBe(3)
109
+ expect(clampOrchestratorSelection(-5, 2)).toBe(0)
110
+ expect(clampOrchestratorSelection(99, 2)).toBe(2)
111
+ expect(isNewSessionRow(0, 0)).toBe(true)
112
+ expect(isNewSessionRow(1, 2)).toBe(false)
113
+ expect(isNewSessionRow(2, 2)).toBe(true)
114
+ expect(canTypeOrchestratorPrompt(1, 2)).toBe(false)
115
+ expect(canTypeOrchestratorPrompt(2, 2)).toBe(true)
116
+ expect(orchestratorVisibleRowIndexes(3, 3, 12)).toEqual([0, 1, 2, 3])
117
+ expect(orchestratorVisibleRowIndexes(13, 13, 12)).toContain(13)
118
+ })
119
+
120
+ it('selects a safe fallback after closing the current live session', () => {
121
+ const remaining = [
122
+ { id: 'next', status: 'idle' },
123
+ { id: 'other', status: 'working' }
124
+ ] satisfies SessionActiveItem[]
125
+
126
+ expect(closeFallbackAfterClose('other', 'current', remaining)).toEqual({ action: 'stay' })
127
+ expect(closeFallbackAfterClose('current', 'current', remaining)).toEqual({ action: 'activate', sessionId: 'next' })
128
+ expect(closeFallbackAfterClose('current', 'current', [])).toEqual({ action: 'new' })
129
+ })
130
+
131
+ it('shows clean draft model labels without picker flags or provider params', () => {
132
+ expect(draftModelDisplayLabel('kimi-k2.6 --provider ollama-cloud --tui-session')).toBe('kimi-k2.6')
133
+ expect(draftModelDisplayLabel('openai/gpt-5.5 --provider openai-codex --global')).toBe('gpt-5.5')
134
+ expect(draftModelDisplayLabel('')).toBe('current/default')
135
+ })
136
+
137
+ it('maps row clicks to existing-session activation or New-row focus', () => {
138
+ const sessions = [
139
+ { id: 'a', status: 'idle' },
140
+ { id: 'b', status: 'idle' }
141
+ ] satisfies SessionActiveItem[]
142
+
143
+ expect(orchestratorRowClickAction(1, sessions)).toEqual({ action: 'activate', sessionId: 'b' })
144
+ expect(orchestratorRowClickAction(2, sessions)).toEqual({ action: 'select-new' })
145
+ expect(orchestratorRowClickAction(99, sessions)).toEqual({ action: 'select-new' })
146
+ })
147
+
148
+ it('keeps fixed table columns from shrinking into adjacent columns', () => {
149
+ expect(fixedSessionColumnStyle().flexShrink).toBe(0)
150
+ })
151
+
152
+ it('builds a compact title from the orchestrator prompt', () => {
153
+ expect(draftTitleFromPrompt(' Build the websocket orchestrator panel and make it robust. ', 24)).toBe(
154
+ 'Build the websocket orc…'
155
+ )
156
+ })
157
+ })
@@ -0,0 +1,84 @@
1
+ import React from 'react'
2
+ import { describe, expect, it, vi } from 'vitest'
3
+
4
+ import { StatusRule } from '../components/appChrome.js'
5
+ import { DEFAULT_THEME } from '../theme.js'
6
+
7
+ type ReactNodeLike = React.ReactNode
8
+
9
+ const textContent = (node: ReactNodeLike): string => {
10
+ if (node === null || node === undefined || typeof node === 'boolean') {
11
+ return ''
12
+ }
13
+
14
+ if (typeof node === 'string' || typeof node === 'number') {
15
+ return String(node)
16
+ }
17
+
18
+ if (Array.isArray(node)) {
19
+ return node.map(textContent).join('')
20
+ }
21
+
22
+ if (React.isValidElement(node)) {
23
+ return textContent(node.props.children)
24
+ }
25
+
26
+ return ''
27
+ }
28
+
29
+ const findClickableWithText = (node: ReactNodeLike, needle: string): React.ReactElement | null => {
30
+ if (node === null || node === undefined || typeof node === 'boolean') {
31
+ return null
32
+ }
33
+
34
+ if (Array.isArray(node)) {
35
+ for (const child of node) {
36
+ const found = findClickableWithText(child, needle)
37
+
38
+ if (found) {
39
+ return found
40
+ }
41
+ }
42
+
43
+ return null
44
+ }
45
+
46
+ if (!React.isValidElement(node)) {
47
+ return null
48
+ }
49
+
50
+ if (typeof node.props.onClick === 'function' && textContent(node).includes(needle)) {
51
+ return node
52
+ }
53
+
54
+ return findClickableWithText(node.props.children, needle)
55
+ }
56
+
57
+ describe('StatusRule session count click target', () => {
58
+ it('makes the live session count itself clickable', () => {
59
+ const openSwitcher = vi.fn()
60
+ const element = StatusRule({
61
+ bgCount: 0,
62
+ busy: false,
63
+ cols: 100,
64
+ cwdLabel: '~/repo',
65
+ liveSessionCount: 1,
66
+ model: 'kimi-k2.6',
67
+ onSessionCountClick: openSwitcher,
68
+ sessionStartedAt: null,
69
+ showCost: false,
70
+ status: 'ready',
71
+ statusColor: DEFAULT_THEME.color.ok,
72
+ t: DEFAULT_THEME,
73
+ turnStartedAt: null,
74
+ usage: { total: 0 },
75
+ voiceLabel: ''
76
+ })
77
+
78
+ const clickableSessionCount = findClickableWithText(element, '1 session')
79
+
80
+ expect(clickableSessionCount).not.toBeNull()
81
+ clickableSessionCount!.props.onClick({ stopImmediatePropagation: vi.fn() })
82
+ expect(openSwitcher).toHaveBeenCalledOnce()
83
+ })
84
+ })
@@ -0,0 +1,73 @@
1
+ import React from 'react'
2
+ import { describe, expect, it, vi } from 'vitest'
3
+
4
+ import { StatusRule } from '../components/appChrome.js'
5
+ import { DEFAULT_THEME } from '../theme.js'
6
+
7
+ // DEV_CREDITS_MODE is a module-load-time constant (config/env.ts reads
8
+ // process.env.NASTECH_DEV_CREDITS exactly once, at import). Mutating process.env
9
+ // inside a test can't flip it after the module is loaded — so mock the module to
10
+ // the dev-on value for this file. vitest hoists vi.mock above the imports, so
11
+ // appChrome picks up the mocked flag. Lives in its own file so the override
12
+ // stays scoped (the other StatusRule tests run with the real, dev-off value).
13
+ vi.mock('../config/env.js', async (importOriginal) => {
14
+ const actual = await importOriginal<typeof import('../config/env.js')>()
15
+ return { ...actual, DEV_CREDITS_MODE: true }
16
+ })
17
+
18
+ type ReactNodeLike = React.ReactNode
19
+
20
+ const textContent = (node: ReactNodeLike): string => {
21
+ if (node === null || node === undefined || typeof node === 'boolean') {
22
+ return ''
23
+ }
24
+
25
+ if (typeof node === 'string' || typeof node === 'number') {
26
+ return String(node)
27
+ }
28
+
29
+ if (Array.isArray(node)) {
30
+ return node.map(textContent).join('')
31
+ }
32
+
33
+ if (React.isValidElement(node)) {
34
+ return textContent(node.props.children)
35
+ }
36
+
37
+ return ''
38
+ }
39
+
40
+ const baseProps = {
41
+ bgCount: 0,
42
+ busy: false,
43
+ cols: 100,
44
+ cwdLabel: '~/repo',
45
+ liveSessionCount: 0,
46
+ model: 'opus-4.8',
47
+ sessionStartedAt: null,
48
+ showCost: false,
49
+ status: 'ready',
50
+ statusColor: DEFAULT_THEME.color.ok,
51
+ t: DEFAULT_THEME,
52
+ turnStartedAt: null,
53
+ usage: { context_max: 200_000, context_percent: 25, context_used: 50_000, total: 50_000 },
54
+ voiceLabel: ''
55
+ }
56
+
57
+ describe('StatusRule dev-credits banner (NASTECH_DEV_CREDITS on)', () => {
58
+ it('keeps the dev-credits banner visible alongside a notice', () => {
59
+ const element = StatusRule({
60
+ ...baseProps,
61
+ notice: { key: 'credits.90', kind: 'sticky', level: 'warn', text: '⚠ 90% used' },
62
+ usage: { ...baseProps.usage, dev_credits_spent_micros: 12_345 }
63
+ })
64
+
65
+ const rendered = textContent(element)
66
+
67
+ // The notice and the dev banner coexist …
68
+ expect(rendered).toContain('⚠ 90% used')
69
+ expect(rendered).toContain('(dev credits)')
70
+ // … and the Δ spend segment renders (12345 micros → 1.2¢).
71
+ expect(rendered).toContain('Δ')
72
+ })
73
+ })
@@ -0,0 +1,50 @@
1
+ import { describe, expect, it } from 'vitest'
2
+
3
+ import { approvalAction } from '../components/prompts.js'
4
+
5
+ describe('approvalAction — pure key dispatch for ApprovalPrompt', () => {
6
+ it('maps Esc to deny — parity with global Ctrl+C cancellation', () => {
7
+ expect(approvalAction('', { escape: true }, 0)).toEqual({ kind: 'choose', choice: 'deny' })
8
+ expect(approvalAction('', { escape: true }, 2)).toEqual({ kind: 'choose', choice: 'deny' })
9
+ })
10
+
11
+ it('maps number keys 1..4 to once/session/always/deny in registration order', () => {
12
+ expect(approvalAction('1', {}, 0)).toEqual({ kind: 'choose', choice: 'once' })
13
+ expect(approvalAction('2', {}, 0)).toEqual({ kind: 'choose', choice: 'session' })
14
+ expect(approvalAction('3', {}, 0)).toEqual({ kind: 'choose', choice: 'always' })
15
+ expect(approvalAction('4', {}, 0)).toEqual({ kind: 'choose', choice: 'deny' })
16
+ })
17
+
18
+ it('ignores out-of-range numbers', () => {
19
+ expect(approvalAction('0', {}, 1)).toEqual({ kind: 'noop' })
20
+ expect(approvalAction('5', {}, 1)).toEqual({ kind: 'noop' })
21
+ expect(approvalAction('9', {}, 1)).toEqual({ kind: 'noop' })
22
+ })
23
+
24
+ it('confirms the current selection on Enter', () => {
25
+ expect(approvalAction('', { return: true }, 0)).toEqual({ kind: 'choose', choice: 'once' })
26
+ expect(approvalAction('', { return: true }, 3)).toEqual({ kind: 'choose', choice: 'deny' })
27
+ })
28
+
29
+ it('moves selection up/down within bounds', () => {
30
+ expect(approvalAction('', { upArrow: true }, 2)).toEqual({ kind: 'move', delta: -1 })
31
+ expect(approvalAction('', { downArrow: true }, 1)).toEqual({ kind: 'move', delta: 1 })
32
+ })
33
+
34
+ it('clamps selection movement at the edges', () => {
35
+ expect(approvalAction('', { upArrow: true }, 0)).toEqual({ kind: 'noop' })
36
+ expect(approvalAction('', { downArrow: true }, 3)).toEqual({ kind: 'noop' })
37
+ })
38
+
39
+ it('Esc beats numeric/return — denying is always the first interpretation', () => {
40
+ // If a terminal somehow delivers Esc + a digit in the same event, deny
41
+ // wins. Documents the precedence so a future refactor doesn't flip it.
42
+ expect(approvalAction('1', { escape: true }, 0)).toEqual({ kind: 'choose', choice: 'deny' })
43
+ expect(approvalAction('', { escape: true, return: true }, 1)).toEqual({ kind: 'choose', choice: 'deny' })
44
+ })
45
+
46
+ it('returns noop for unrelated keystrokes (printable letters etc.)', () => {
47
+ expect(approvalAction('a', {}, 0)).toEqual({ kind: 'noop' })
48
+ expect(approvalAction(' ', {}, 0)).toEqual({ kind: 'noop' })
49
+ })
50
+ })