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,576 @@
1
+ import { forceRedraw, useInput } from '@nastechai/ink'
2
+ import { useStore } from '@nanostores/react'
3
+ import { useEffect, useRef } from 'react'
4
+
5
+ import { TYPING_IDLE_MS } from '../config/timing.js'
6
+ import type {
7
+ ApprovalRespondResponse,
8
+ ConfigSetResponse,
9
+ SecretRespondResponse,
10
+ SudoRespondResponse,
11
+ VoiceRecordResponse
12
+ } from '../gatewayTypes.js'
13
+ import { isAction, isCopyShortcut, isMac, isVoiceToggleKey } from '../lib/platform.js'
14
+ import { computePrecisionWheelStep, initPrecisionWheel } from '../lib/precisionWheel.js'
15
+ import { computeWheelStep, initWheelAccelForHost } from '../lib/wheelAccel.js'
16
+
17
+ import { getInputSelection } from './inputSelectionStore.js'
18
+ import type { InputHandlerContext, InputHandlerResult } from './interfaces.js'
19
+ import { $isBlocked, $overlayState, patchOverlayState } from './overlayStore.js'
20
+ import { turnController } from './turnController.js'
21
+ import { patchTurnState } from './turnStore.js'
22
+ import { getUiState } from './uiStore.js'
23
+
24
+ const isCtrl = (key: { ctrl: boolean }, ch: string, target: string) => key.ctrl && ch.toLowerCase() === target
25
+
26
+ /**
27
+ * Approval / clarify / confirm overlays mount their own `useInput` handlers
28
+ * for the in-prompt keys (arrows, numbers, Enter, sometimes Esc). The global
29
+ * input handler used to early-return for any other key while one of those
30
+ * overlays was up, which silently disabled transcript scrolling — the user
31
+ * couldn't read context above the prompt that the prompt itself was asking
32
+ * about. Returns true when the key is a transcript-scroll input that should
33
+ * fall through to the global scroll handlers even while a prompt is active.
34
+ *
35
+ * Modifier-held wheel (precision mode) is included — a user who wants to
36
+ * scroll a single line at a time during a prompt expects it to work.
37
+ */
38
+ export function shouldFallThroughForScroll(key: {
39
+ downArrow: boolean
40
+ pageDown: boolean
41
+ pageUp: boolean
42
+ shift: boolean
43
+ upArrow: boolean
44
+ wheelDown: boolean
45
+ wheelUp: boolean
46
+ }): boolean {
47
+ if (key.wheelUp || key.wheelDown) {
48
+ return true
49
+ }
50
+
51
+ if (key.pageUp || key.pageDown) {
52
+ return true
53
+ }
54
+
55
+ if (key.shift && (key.upArrow || key.downArrow)) {
56
+ return true
57
+ }
58
+
59
+ return false
60
+ }
61
+
62
+ export function applyVoiceRecordResponse(
63
+ response: null | VoiceRecordResponse,
64
+ starting: boolean,
65
+ voice: Pick<InputHandlerContext['voice'], 'setProcessing' | 'setRecording'>,
66
+ sys: (text: string) => void
67
+ ) {
68
+ if (!starting || response?.status === 'recording') {
69
+ return
70
+ }
71
+
72
+ voice.setRecording(false)
73
+
74
+ if (response?.status === 'busy') {
75
+ voice.setProcessing(true)
76
+ sys('voice: still transcribing; try again shortly')
77
+ } else {
78
+ voice.setProcessing(false)
79
+ }
80
+ }
81
+
82
+ export function useInputHandlers(ctx: InputHandlerContext): InputHandlerResult {
83
+ const { actions, composer, gateway, terminal, voice, wheelStep } = ctx
84
+ const { actions: cActions, refs: cRefs, state: cState } = composer
85
+
86
+ const overlay = useStore($overlayState)
87
+ const isBlocked = useStore($isBlocked)
88
+ const pagerPageSize = Math.max(5, (terminal.stdout?.rows ?? 24) - 6)
89
+ const scrollIdleTimer = useRef<null | ReturnType<typeof setTimeout>>(null)
90
+
91
+ // Wheel accel ported from claude-code: inter-event timing drives step size,
92
+ // direction flips reset. wheelStep (WHEEL_SCROLL_STEP) is the base; final
93
+ // rows = wheelStep × accelMult. State mutates in place across renders.
94
+ const wheelAccelRef = useRef(initWheelAccelForHost())
95
+
96
+ const precisionWheelRef = useRef(initPrecisionWheel())
97
+
98
+ useEffect(() => () => clearTimeout(scrollIdleTimer.current ?? undefined), [])
99
+
100
+ const scrollTranscript = (delta: number) => {
101
+ if (getUiState().busy) {
102
+ turnController.boostStreamingForScroll()
103
+ clearTimeout(scrollIdleTimer.current ?? undefined)
104
+ scrollIdleTimer.current = setTimeout(() => {
105
+ scrollIdleTimer.current = null
106
+ turnController.relaxStreaming()
107
+ }, TYPING_IDLE_MS)
108
+ }
109
+
110
+ terminal.scrollWithSelection(delta)
111
+ }
112
+
113
+ const copySelection = () => {
114
+ // ink's copySelection() already calls setClipboard() which handles
115
+ // pbcopy (macOS), wl-copy/xclip (Linux), tmux, and OSC 52 fallback.
116
+ terminal.selection.copySelection()
117
+ }
118
+
119
+ const clearSelection = () => {
120
+ terminal.selection.clearSelection()
121
+ }
122
+
123
+ const cancelOverlayFromCtrlC = () => {
124
+ if (overlay.clarify) {
125
+ return actions.answerClarify('')
126
+ }
127
+
128
+ if (overlay.approval) {
129
+ return gateway
130
+ .rpc<ApprovalRespondResponse>('approval.respond', { choice: 'deny', session_id: getUiState().sid })
131
+ .then(r => r && (patchOverlayState({ approval: null }), patchTurnState({ outcome: 'denied' })))
132
+ }
133
+
134
+ if (overlay.sudo) {
135
+ return gateway
136
+ .rpc<SudoRespondResponse>('sudo.respond', { password: '', request_id: overlay.sudo.requestId })
137
+ .then(r => r && (patchOverlayState({ sudo: null }), actions.sys('sudo cancelled')))
138
+ }
139
+
140
+ if (overlay.secret) {
141
+ return gateway
142
+ .rpc<SecretRespondResponse>('secret.respond', { request_id: overlay.secret.requestId, value: '' })
143
+ .then(r => r && (patchOverlayState({ secret: null }), actions.sys('secret entry cancelled')))
144
+ }
145
+
146
+ if (overlay.modelPicker) {
147
+ return patchOverlayState({ modelPicker: false })
148
+ }
149
+
150
+ if (overlay.skillsHub) {
151
+ return patchOverlayState({ skillsHub: false })
152
+ }
153
+
154
+ if (overlay.picker) {
155
+ return patchOverlayState({ picker: false })
156
+ }
157
+
158
+ if (overlay.agents) {
159
+ return patchOverlayState({ agents: false })
160
+ }
161
+ }
162
+
163
+ const cycleQueue = (dir: 1 | -1) => {
164
+ const len = cRefs.queueRef.current.length
165
+
166
+ if (!len) {
167
+ return false
168
+ }
169
+
170
+ const index = cState.queueEditIdx === null ? (dir > 0 ? 0 : len - 1) : (cState.queueEditIdx + dir + len) % len
171
+
172
+ cActions.setQueueEdit(index)
173
+ cActions.setHistoryIdx(null)
174
+ cActions.setInput(cRefs.queueRef.current[index] ?? '')
175
+
176
+ return true
177
+ }
178
+
179
+ const cycleHistory = (dir: 1 | -1) => {
180
+ const h = cRefs.historyRef.current
181
+ const cur = cState.historyIdx
182
+
183
+ if (dir < 0) {
184
+ if (!h.length) {
185
+ return
186
+ }
187
+
188
+ if (cur === null) {
189
+ cRefs.historyDraftRef.current = cState.input
190
+ }
191
+
192
+ const index = cur === null ? h.length - 1 : Math.max(0, cur - 1)
193
+
194
+ cActions.setHistoryIdx(index)
195
+ cActions.setQueueEdit(null)
196
+ cActions.setInput(h[index] ?? '')
197
+
198
+ return
199
+ }
200
+
201
+ if (cur === null) {
202
+ return
203
+ }
204
+
205
+ const next = cur + 1
206
+
207
+ if (next >= h.length) {
208
+ cActions.setHistoryIdx(null)
209
+ cActions.setInput(cRefs.historyDraftRef.current)
210
+ } else {
211
+ cActions.setHistoryIdx(next)
212
+ cActions.setInput(h[next] ?? '')
213
+ }
214
+ }
215
+
216
+ // CLI parity: Ctrl+B toggles a VAD-bounded push-to-talk capture
217
+ // (NOT the voice-mode umbrella bit). The mode is enabled via /voice on;
218
+ // Ctrl+B while the mode is off sys-nudges the user. While the mode is
219
+ // on, the first press starts a single VAD-bounded capture
220
+ // (gateway -> start_continuous(auto_restart=false), VAD auto-stop ->
221
+ // transcribe -> idle), a subsequent press stops and transcribes it.
222
+ // The gateway publishes voice.status + voice.transcript events that
223
+ // createGatewayEventHandler turns into UI badges and composer injection.
224
+ const voiceRecordToggle = () => {
225
+ if (!voice.enabled) {
226
+ return actions.sys('voice: mode is off — enable with /voice on')
227
+ }
228
+
229
+ const starting = !voice.recording
230
+ const action = starting ? 'start' : 'stop'
231
+
232
+ // Optimistic UI — flip the REC badge immediately so the user gets
233
+ // feedback while the RPC round-trips; the voice.status event is the
234
+ // authoritative source and may correct us.
235
+ if (starting) {
236
+ voice.setRecording(true)
237
+ } else {
238
+ voice.setRecording(false)
239
+ voice.setProcessing(false)
240
+ }
241
+
242
+ gateway
243
+ .rpc<VoiceRecordResponse>('voice.record', { action, session_id: getUiState().sid })
244
+ .then(r => applyVoiceRecordResponse(r, starting, voice, actions.sys))
245
+ .catch((e: Error) => {
246
+ // Revert optimistic UI on failure.
247
+ if (starting) {
248
+ voice.setRecording(false)
249
+ }
250
+
251
+ actions.sys(`voice error: ${e.message}`)
252
+ })
253
+ }
254
+
255
+ useInput((ch, key) => {
256
+ const live = getUiState()
257
+
258
+ if (isBlocked) {
259
+ // When approval/clarify/confirm overlays are active, their own useInput
260
+ // handlers must receive keystrokes (arrow keys, numbers, Enter). Only
261
+ // intercept Ctrl+C here so the user can deny/dismiss — all other keys
262
+ // fall through to the component-level handlers.
263
+ //
264
+ // Scroll inputs (wheel / PageUp / PageDown / Shift+↑↓) are special:
265
+ // they must reach the transcript scroll handlers below even with a
266
+ // prompt up. Long-thread context the prompt is asking about often
267
+ // lives above the visible viewport, and being unable to read it while
268
+ // answering felt like the prompt had locked the entire UI. Explicitly
269
+ // skip the prompt-overlay early-return for scroll keys so they fall
270
+ // through to the wheel / PageUp / Shift+arrow handlers below.
271
+ const promptOverlay = overlay.approval || overlay.clarify || overlay.confirm
272
+ const fallThroughForScroll = promptOverlay && shouldFallThroughForScroll(key)
273
+
274
+ if (promptOverlay && !fallThroughForScroll) {
275
+ if (isCtrl(key, ch, 'c')) {
276
+ cancelOverlayFromCtrlC()
277
+ }
278
+
279
+ return
280
+ }
281
+
282
+ if (overlay.pager) {
283
+ if (key.escape || isCtrl(key, ch, 'c') || ch === 'q') {
284
+ return patchOverlayState({ pager: null })
285
+ }
286
+
287
+ const move = (delta: number | 'top' | 'bottom') =>
288
+ patchOverlayState(prev => {
289
+ if (!prev.pager) {
290
+ return prev
291
+ }
292
+
293
+ const { lines, offset } = prev.pager
294
+ const max = Math.max(0, lines.length - pagerPageSize)
295
+ const step = delta === 'top' ? -lines.length : delta === 'bottom' ? lines.length : delta
296
+ const next = Math.max(0, Math.min(offset + step, max))
297
+
298
+ return next === offset ? prev : { ...prev, pager: { ...prev.pager, offset: next } }
299
+ })
300
+
301
+ if (key.upArrow || ch === 'k') {
302
+ return move(-1)
303
+ }
304
+
305
+ if (key.downArrow || ch === 'j') {
306
+ return move(1)
307
+ }
308
+
309
+ if (key.pageUp || ch === 'b') {
310
+ return move(-pagerPageSize)
311
+ }
312
+
313
+ if (ch === 'g') {
314
+ return move('top')
315
+ }
316
+
317
+ if (ch === 'G') {
318
+ return move('bottom')
319
+ }
320
+
321
+ if (key.return || ch === ' ' || key.pageDown) {
322
+ patchOverlayState(prev => {
323
+ if (!prev.pager) {
324
+ return prev
325
+ }
326
+
327
+ const { lines, offset } = prev.pager
328
+ const max = Math.max(0, lines.length - pagerPageSize)
329
+
330
+ // Auto-close only when already at the last page — otherwise clamp
331
+ // to `max` so the offset matches what the line/page-back handlers
332
+ // can reach (prevents a snap-back jump on the next ↑/↓/PgUp).
333
+ return offset >= max
334
+ ? { ...prev, pager: null }
335
+ : { ...prev, pager: { ...prev.pager, offset: Math.min(offset + pagerPageSize, max) } }
336
+ })
337
+ }
338
+
339
+ return
340
+ }
341
+
342
+ if (isCtrl(key, ch, 'c')) {
343
+ cancelOverlayFromCtrlC()
344
+ } else if (key.escape && overlay.picker) {
345
+ patchOverlayState({ picker: false })
346
+ }
347
+
348
+ // When a prompt overlay is up and the user pressed a scroll key, fall
349
+ // through to the global scroll handlers below instead of returning.
350
+ // Otherwise nothing above this comment matched, and there's nothing
351
+ // useful to do for an arbitrary key while blocked.
352
+ if (!fallThroughForScroll) {
353
+ return
354
+ }
355
+ }
356
+
357
+ if (cState.completions.length && cState.input && cState.historyIdx === null && (key.upArrow || key.downArrow)) {
358
+ const len = cState.completions.length
359
+
360
+ cActions.setCompIdx(i => (key.upArrow ? (i - 1 + len) % len : (i + 1) % len))
361
+
362
+ return
363
+ }
364
+
365
+ if (key.wheelUp || key.wheelDown) {
366
+ const dir: -1 | 1 = key.wheelUp ? -1 : 1
367
+ const now = Date.now()
368
+ // Modifier-held wheel = precision mode: one row per frame, no accel.
369
+ // Smooth mice / trackpads emit tiny same-frame bursts; coalesce those
370
+ // without the old 80ms throttle that made opt-scroll feel stepped.
371
+ // SGR/X10 mouse encoding only carries shift/meta/ctrl bits; Cmd on
372
+ // macOS is intercepted by the terminal, so we honor Option (meta) on
373
+ // Mac / Alt (meta) on Win+Linux / Ctrl as a portable fallback. Shift
374
+ // is reserved for selection extension.
375
+ const hasModifier = key.meta || key.ctrl
376
+ const precision = computePrecisionWheelStep(precisionWheelRef.current, dir, hasModifier, now)
377
+
378
+ if (precision.active) {
379
+ // Entering precision mode must discard any accelerated wheel state;
380
+ // otherwise the next normal wheel event inherits stale momentum.
381
+ if (precision.entered) {
382
+ wheelAccelRef.current = initWheelAccelForHost()
383
+ }
384
+
385
+ return precision.rows ? scrollTranscript(dir * wheelStep) : undefined
386
+ }
387
+
388
+ // 0 = direction-flip bounce deferred; skip the no-op scroll.
389
+ const rows = computeWheelStep(wheelAccelRef.current, dir, now)
390
+
391
+ return rows ? scrollTranscript(dir * rows * wheelStep) : undefined
392
+ }
393
+
394
+ if (key.shift && key.upArrow) {
395
+ return scrollTranscript(-1)
396
+ }
397
+
398
+ if (key.shift && key.downArrow) {
399
+ return scrollTranscript(1)
400
+ }
401
+
402
+ if (key.pageUp || key.pageDown) {
403
+ // Half-viewport keeps 50% continuity and stays under Ink's
404
+ // `delta < innerHeight` DECSTBM fast-path threshold.
405
+ const viewport = terminal.scrollRef.current?.getViewportHeight() ?? Math.max(6, (terminal.stdout?.rows ?? 24) - 8)
406
+ const step = Math.max(4, Math.floor(viewport / 2))
407
+
408
+ return scrollTranscript(key.pageUp ? -step : step)
409
+ }
410
+
411
+ // Escape-based voice bindings (ctrl/alt/super+escape) must win before the
412
+ // generic Esc handlers below; otherwise queue-edit cancel / selection-clear
413
+ // would swallow the chord and /voice would advertise a shortcut that never
414
+ // actually toggles recording in those UI states.
415
+ if (key.escape && isVoiceToggleKey(key, ch, voice.recordKey)) {
416
+ return voiceRecordToggle()
417
+ }
418
+
419
+ // Queue-edit cancel beats selection-clear for plain Esc: the queue header
420
+ // explicitly promises "Esc cancel", so honoring it takes priority over the
421
+ // implicit selection-dismissal convention. Without an active edit, fall through.
422
+ if (key.escape && cState.queueEditIdx !== null) {
423
+ return cActions.clearIn()
424
+ }
425
+
426
+ if (key.escape && terminal.hasSelection) {
427
+ return clearSelection()
428
+ }
429
+
430
+ if (key.upArrow && !cState.inputBuf.length) {
431
+ const inputSel = getInputSelection()
432
+ const cursor = inputSel && inputSel.start === inputSel.end ? inputSel.start : null
433
+
434
+ const noLineAbove =
435
+ !cState.input || (cursor !== null && cState.input.lastIndexOf('\n', Math.max(0, cursor - 1)) < 0)
436
+
437
+ if (noLineAbove) {
438
+ cycleQueue(1) || cycleHistory(-1)
439
+
440
+ return
441
+ }
442
+ }
443
+
444
+ if (key.downArrow && !cState.inputBuf.length) {
445
+ const inputSel = getInputSelection()
446
+ const cursor = inputSel && inputSel.start === inputSel.end ? inputSel.start : null
447
+ const noLineBelow = !cState.input || (cursor !== null && cState.input.indexOf('\n', cursor) < 0)
448
+
449
+ if (noLineBelow || cState.historyIdx !== null) {
450
+ cycleQueue(-1) || cycleHistory(1)
451
+
452
+ return
453
+ }
454
+ }
455
+
456
+ if (isCopyShortcut(key, ch)) {
457
+ if (terminal.hasSelection) {
458
+ return copySelection()
459
+ }
460
+
461
+ const inputSel = getInputSelection()
462
+
463
+ if (inputSel && inputSel.end > inputSel.start) {
464
+ inputSel.clear()
465
+
466
+ return
467
+ }
468
+
469
+ // On macOS, Cmd+C with no selection is a no-op (Ctrl+C below handles interrupt).
470
+ // On non-macOS, isAction uses Ctrl, so fall through to interrupt/clear/exit.
471
+ if (isMac) {
472
+ return
473
+ }
474
+ }
475
+
476
+ if (isCtrl(key, ch, 'x') && cState.queueEditIdx !== null) {
477
+ cActions.removeQueue(cState.queueEditIdx)
478
+
479
+ return cActions.clearIn()
480
+ }
481
+
482
+ if (isCtrl(key, ch, 'x')) {
483
+ return patchOverlayState({ sessions: true })
484
+ }
485
+
486
+ if (key.ctrl && ch.toLowerCase() === 'c') {
487
+ if (live.busy && live.sid) {
488
+ return turnController.interruptTurn({
489
+ appendMessage: actions.appendMessage,
490
+ gw: gateway.gw,
491
+ sid: live.sid,
492
+ sys: actions.sys
493
+ })
494
+ }
495
+
496
+ if (cState.input || cState.inputBuf.length) {
497
+ return cActions.clearIn()
498
+ }
499
+
500
+ return actions.die()
501
+ }
502
+
503
+ if (isAction(key, ch, 'd')) {
504
+ return actions.die()
505
+ }
506
+
507
+ if (isAction(key, ch, 'l')) {
508
+ clearSelection()
509
+ forceRedraw(terminal.stdout ?? process.stdout)
510
+
511
+ return
512
+ }
513
+
514
+ if (isVoiceToggleKey(key, ch, voice.recordKey)) {
515
+ return voiceRecordToggle()
516
+ }
517
+
518
+ // Cmd/Ctrl+G, plus Alt+G fallback for VSCode/Cursor (they bind the
519
+ // primary keystroke to "Find Next" before the TUI sees it; Alt+G
520
+ // arrives as meta+g across platforms).
521
+ if (ch.toLowerCase() === 'g' && (isAction(key, ch, 'g') || key.meta)) {
522
+ return void cActions.openEditor().catch((err: unknown) => {
523
+ actions.sys(err instanceof Error ? `failed to open editor: ${err.message}` : 'failed to open editor')
524
+ })
525
+ }
526
+
527
+ // shift-tab flips yolo without spending a turn (claude-code parity)
528
+ if (key.shift && key.tab && !cState.completions.length) {
529
+ if (!live.sid) {
530
+ return void actions.sys('yolo needs an active session')
531
+ }
532
+
533
+ // gateway.rpc swallows errors with its own sys() message and resolves to null,
534
+ // so we only speak when it came back with a real shape. null = rpc already spoke.
535
+ return void gateway.rpc<ConfigSetResponse>('config.set', { key: 'yolo', session_id: live.sid }).then(r => {
536
+ if (r?.value === '1') {
537
+ return actions.sys('yolo on')
538
+ }
539
+
540
+ if (r?.value === '0') {
541
+ return actions.sys('yolo off')
542
+ }
543
+
544
+ if (r) {
545
+ actions.sys('failed to toggle yolo')
546
+ }
547
+ })
548
+ }
549
+
550
+ if (key.tab && cState.completions.length) {
551
+ const row = cState.completions[cState.compIdx]
552
+
553
+ if (row?.text) {
554
+ const text =
555
+ cState.input.startsWith('/') && row.text.startsWith('/') && cState.compReplace > 0
556
+ ? row.text.slice(1)
557
+ : row.text
558
+
559
+ cActions.setInput(cState.input.slice(0, cState.compReplace) + text)
560
+ }
561
+
562
+ return
563
+ }
564
+
565
+ if (isAction(key, ch, 'k') && cRefs.queueRef.current.length && live.sid) {
566
+ const next = cActions.dequeue()
567
+
568
+ if (next) {
569
+ cActions.setQueueEdit(null)
570
+ actions.dispatchSubmission(next)
571
+ }
572
+ }
573
+ })
574
+
575
+ return { pagerPageSize }
576
+ }
@@ -0,0 +1,69 @@
1
+ import { useEffect, useRef } from 'react'
2
+
3
+ import { LONG_RUN_CHARMS } from '../content/charms.js'
4
+ import { pick, toolTrailLabel } from '../lib/text.js'
5
+
6
+ import { turnController } from './turnController.js'
7
+ import { useTurnSelector } from './turnStore.js'
8
+ import { getUiState } from './uiStore.js'
9
+
10
+ const DELAY_MS = 8_000
11
+ const INTERVAL_MS = 10_000
12
+ const MAX_CHARMS_PER_TOOL = 2
13
+
14
+ interface Slot {
15
+ count: number
16
+ lastAt: number
17
+ }
18
+
19
+ export function useLongRunToolCharms() {
20
+ const tools = useTurnSelector(state => state.tools)
21
+ const slots = useRef(new Map<string, Slot>())
22
+
23
+ useEffect(() => {
24
+ if (!getUiState().busy || !tools.length) {
25
+ slots.current.clear()
26
+
27
+ return
28
+ }
29
+
30
+ const tick = () => {
31
+ if (!getUiState().busy) {
32
+ slots.current.clear()
33
+
34
+ return
35
+ }
36
+
37
+ const now = Date.now()
38
+ const liveIds = new Set(tools.map(t => t.id))
39
+
40
+ for (const key of Array.from(slots.current.keys())) {
41
+ if (!liveIds.has(key)) {
42
+ slots.current.delete(key)
43
+ }
44
+ }
45
+
46
+ for (const tool of tools) {
47
+ if (!tool.startedAt || now - tool.startedAt < DELAY_MS) {
48
+ continue
49
+ }
50
+
51
+ const slot = slots.current.get(tool.id) ?? { count: 0, lastAt: 0 }
52
+
53
+ if (slot.count >= MAX_CHARMS_PER_TOOL || now - slot.lastAt < INTERVAL_MS) {
54
+ continue
55
+ }
56
+
57
+ slots.current.set(tool.id, { count: slot.count + 1, lastAt: now })
58
+ turnController.pushActivity(
59
+ `${pick(LONG_RUN_CHARMS)} (${toolTrailLabel(tool.name)} · ${Math.round((now - tool.startedAt) / 1000)}s)`
60
+ )
61
+ }
62
+ }
63
+
64
+ tick()
65
+ const id = setInterval(tick, 1000)
66
+
67
+ return () => clearInterval(id)
68
+ }, [tools])
69
+ }