@linxiraos/pi-tui 1.1.26 → 1.2.0

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 (423) hide show
  1. package/CHANGELOG.md +51 -491
  2. package/README.md +13 -7
  3. package/dist/types/active-terminal.d.ts +30 -0
  4. package/dist/types/app-keybindings.d.ts +7 -2
  5. package/dist/types/apps/cleanse-board.d.ts +1 -1
  6. package/dist/types/apps/debug/log-viewer.d.ts +4 -0
  7. package/dist/types/apps/debug/raw-sse-buffer.d.ts +2 -0
  8. package/dist/types/apps/git/git-tui.d.ts +5 -4
  9. package/dist/types/apps/git/help.d.ts +3 -1
  10. package/dist/types/apps/if-bench-board.d.ts +2 -2
  11. package/dist/types/apps/setup-model-picker.d.ts +1 -1
  12. package/dist/types/autocomplete.d.ts +5 -2
  13. package/dist/types/charts/chart-plan.d.ts +674 -0
  14. package/dist/types/charts/chart-svg.d.ts +59 -0
  15. package/dist/types/charts/table-data.d.ts +175 -0
  16. package/dist/types/chat/assistant-message.d.ts +11 -1
  17. package/dist/types/chat/chat-transcript-builder.d.ts +3 -0
  18. package/dist/types/chat/compaction-summary-message.d.ts +1 -0
  19. package/dist/types/chat/execution-shared.d.ts +6 -5
  20. package/dist/types/chat/fence-figure.d.ts +42 -0
  21. package/dist/types/chat/figure-markdown.d.ts +42 -0
  22. package/dist/types/chat/messages.d.ts +1 -1
  23. package/dist/types/chat/svg-figure.d.ts +33 -0
  24. package/dist/types/chat/svg-source.d.ts +49 -0
  25. package/dist/types/chat/table-chart.d.ts +89 -0
  26. package/dist/types/chat/tool-execution.d.ts +2 -0
  27. package/dist/types/chat/user-message.d.ts +17 -4
  28. package/dist/types/chrome/shared.d.ts +9 -0
  29. package/dist/types/chrome/status-notice.d.ts +15 -3
  30. package/dist/types/chrome/transcript-container.d.ts +6 -0
  31. package/dist/types/components/box.d.ts +1 -0
  32. package/dist/types/components/disclosure.d.ts +1 -0
  33. package/dist/types/components/editor.d.ts +23 -5
  34. package/dist/types/components/form.d.ts +1 -0
  35. package/dist/types/components/image.d.ts +26 -44
  36. package/dist/types/components/input.d.ts +2 -1
  37. package/dist/types/components/key-value-list.d.ts +1 -0
  38. package/dist/types/components/layout/row.d.ts +1 -0
  39. package/dist/types/components/layout/stack.d.ts +1 -0
  40. package/dist/types/components/loader.d.ts +5 -6
  41. package/dist/types/components/markdown.d.ts +47 -5
  42. package/dist/types/components/scroll-view.d.ts +1 -0
  43. package/dist/types/components/section.d.ts +1 -0
  44. package/dist/types/components/table.d.ts +1 -0
  45. package/dist/types/components/text.d.ts +1 -0
  46. package/dist/types/components/tree-view.d.ts +7 -0
  47. package/dist/types/components/truncated-text.d.ts +1 -0
  48. package/dist/types/fuzzy.d.ts +29 -1
  49. package/dist/types/key-hint-format.d.ts +9 -1
  50. package/dist/types/keybinding-matchers.d.ts +3 -0
  51. package/dist/types/keybindings.d.ts +1 -1
  52. package/dist/types/kitty-graphics.d.ts +2 -2
  53. package/dist/types/loop-watchdog.d.ts +8 -7
  54. package/dist/types/multiplexers/cmux.d.ts +16 -0
  55. package/dist/types/multiplexers/herdr.d.ts +26 -0
  56. package/dist/types/multiplexers/notify.d.ts +13 -0
  57. package/dist/types/multiplexers/orca.d.ts +12 -0
  58. package/dist/types/multiplexers/screen.d.ts +9 -0
  59. package/dist/types/multiplexers/tmux.d.ts +29 -0
  60. package/dist/types/multiplexers/types.d.ts +100 -0
  61. package/dist/types/multiplexers/wmux.d.ts +12 -0
  62. package/dist/types/multiplexers/zellij.d.ts +12 -0
  63. package/dist/types/native/apply.d.ts +4 -3
  64. package/dist/types/native/backend.d.ts +5 -4
  65. package/dist/types/native/blobs.d.ts +7 -3
  66. package/dist/types/native/encode.d.ts +8 -6
  67. package/dist/types/native/node.d.ts +36 -5
  68. package/dist/types/native/overlay.d.ts +8 -3
  69. package/dist/types/native/settle.d.ts +4 -3
  70. package/dist/types/overlays/agent-hub-projection.d.ts +6 -5
  71. package/dist/types/overlays/agent-hub-types.d.ts +9 -0
  72. package/dist/types/overlays/agent-hub.d.ts +3 -0
  73. package/dist/types/overlays/agent-transcript-viewer.d.ts +12 -2
  74. package/dist/types/overlays/agents-hub.d.ts +9 -0
  75. package/dist/types/overlays/annotation-overlay.d.ts +19 -2
  76. package/dist/types/overlays/annotation-types.d.ts +2 -0
  77. package/dist/types/overlays/ask-dialog.d.ts +5 -2
  78. package/dist/types/overlays/btw-history-panel.d.ts +13 -1
  79. package/dist/types/overlays/copy-selector.d.ts +1 -0
  80. package/dist/types/overlays/history-search.d.ts +10 -0
  81. package/dist/types/overlays/hook-selector.d.ts +9 -4
  82. package/dist/types/overlays/hub-frame.d.ts +4 -1
  83. package/dist/types/overlays/jobs-panel.d.ts +1 -7
  84. package/dist/types/overlays/model-browser.d.ts +54 -7
  85. package/dist/types/overlays/model-hub.d.ts +37 -0
  86. package/dist/types/overlays/oauth-selector.d.ts +0 -1
  87. package/dist/types/overlays/plan-review-overlay.d.ts +3 -2
  88. package/dist/types/overlays/report-panel.d.ts +68 -0
  89. package/dist/types/overlays/rewind-selector.d.ts +1 -0
  90. package/dist/types/overlays/session-observer-registry.d.ts +2 -0
  91. package/dist/types/overlays/session-selector.d.ts +6 -2
  92. package/dist/types/overlays/settings-defs.d.ts +1 -0
  93. package/dist/types/overlays/thinking-selector.d.ts +2 -2
  94. package/dist/types/overlays/usage-dashboard.d.ts +12 -8
  95. package/dist/types/overlays/usage-display.d.ts +3 -1
  96. package/dist/types/prompt/composer-cache.d.ts +0 -16
  97. package/dist/types/prompt/composer.d.ts +18 -3
  98. package/dist/types/prompt/custom-editor.d.ts +25 -13
  99. package/dist/types/prompt/image-source.d.ts +2 -1
  100. package/dist/types/prompt/interactive-context-helpers.d.ts +5 -1
  101. package/dist/types/prompt/model-mention-autocomplete.d.ts +1 -1
  102. package/dist/types/prompt/welcome.d.ts +7 -2
  103. package/dist/types/render/composite.d.ts +2 -0
  104. package/dist/types/render/output-pane.d.ts +1 -0
  105. package/dist/types/render/render-utils.d.ts +1 -0
  106. package/dist/types/render/tool-card.d.ts +6 -0
  107. package/dist/types/render/width-aware-text.d.ts +2 -0
  108. package/dist/types/setup/scenes/types.d.ts +1 -0
  109. package/dist/types/space-hold.d.ts +40 -25
  110. package/dist/types/status-line/component.d.ts +3 -0
  111. package/dist/types/status-line/context-usage.d.ts +7 -5
  112. package/dist/types/status-line/host.d.ts +4 -1
  113. package/dist/types/status-line/segments.d.ts +2 -0
  114. package/dist/types/status-line/types.d.ts +14 -3
  115. package/dist/types/terminal-capabilities.d.ts +41 -36
  116. package/dist/types/terminal-multiplexer.d.ts +127 -14
  117. package/dist/types/terminal.d.ts +23 -16
  118. package/dist/types/theme/active-symbols.d.ts +1 -1
  119. package/dist/types/theme/schema.d.ts +11 -2
  120. package/dist/types/theme/symbols.d.ts +1 -1
  121. package/dist/types/theme/theme-class.d.ts +7 -0
  122. package/dist/types/theme/theme.d.ts +1 -1
  123. package/dist/types/theme/tui-adapters.d.ts +3 -0
  124. package/dist/types/thinking.d.ts +1 -1
  125. package/dist/types/tmux.d.ts +11 -4
  126. package/dist/types/tools/bash.d.ts +7 -2
  127. package/dist/types/tools/daemon.d.ts +1 -1
  128. package/dist/types/tools/eval.d.ts +22 -3
  129. package/dist/types/tools/glob.d.ts +4 -0
  130. package/dist/types/tools/native-view.d.ts +47 -0
  131. package/dist/types/tools/output-meta.d.ts +11 -1
  132. package/dist/types/tools/renderer.d.ts +37 -2
  133. package/dist/types/tools/streaming-output.d.ts +20 -1
  134. package/dist/types/tools/task.d.ts +2 -3
  135. package/dist/types/tools/write.d.ts +9 -2
  136. package/dist/types/tools/yield.d.ts +22 -0
  137. package/dist/types/tui.d.ts +36 -0
  138. package/dist/types/utils.d.ts +6 -0
  139. package/dist/types/vim.d.ts +19 -5
  140. package/dist/types/windows-input-mode.d.ts +33 -1
  141. package/package.json +13 -9
  142. package/src/active-terminal.d.ts +30 -0
  143. package/src/active-terminal.ts +80 -0
  144. package/src/app-keybindings.d.ts +7 -2
  145. package/src/app-keybindings.ts +7 -2
  146. package/src/apps/autoresearch-dashboard.ts +7 -3
  147. package/src/apps/cleanse-board.ts +2 -2
  148. package/src/apps/cleanse-picker.ts +1 -1
  149. package/src/apps/debug/log-viewer.ts +188 -72
  150. package/src/apps/debug/raw-sse-buffer.ts +8 -0
  151. package/src/apps/debug/raw-sse.ts +39 -5
  152. package/src/apps/git/diff-pane.ts +96 -23
  153. package/src/apps/git/git-tui.ts +23 -16
  154. package/src/apps/git/help.ts +29 -20
  155. package/src/apps/git/sidebar.ts +8 -3
  156. package/src/apps/if-bench-board.ts +2 -2
  157. package/src/apps/ps-top.ts +2 -2
  158. package/src/apps/setup-model-picker.ts +1 -1
  159. package/src/autocomplete.d.ts +5 -2
  160. package/src/autocomplete.ts +67 -43
  161. package/src/bracketed-paste.ts +10 -3
  162. package/src/charts/chart-plan.d.ts +674 -0
  163. package/src/charts/chart-plan.ts +5081 -0
  164. package/src/charts/chart-svg.d.ts +59 -0
  165. package/src/charts/chart-svg.ts +4657 -0
  166. package/src/charts/table-data.d.ts +175 -0
  167. package/src/charts/table-data.ts +1478 -0
  168. package/src/chat/advisor-message.ts +11 -0
  169. package/src/chat/assistant-message.d.ts +11 -1
  170. package/src/chat/assistant-message.ts +245 -54
  171. package/src/chat/chat-transcript-builder.d.ts +3 -0
  172. package/src/chat/chat-transcript-builder.ts +9 -3
  173. package/src/chat/compaction-summary-message.d.ts +1 -0
  174. package/src/chat/compaction-summary-message.ts +8 -0
  175. package/src/chat/execution-shared.d.ts +6 -5
  176. package/src/chat/execution-shared.ts +42 -25
  177. package/src/chat/fence-figure.d.ts +42 -0
  178. package/src/chat/fence-figure.ts +80 -0
  179. package/src/chat/figure-markdown.d.ts +42 -0
  180. package/src/chat/figure-markdown.ts +174 -0
  181. package/src/chat/messages.d.ts +1 -1
  182. package/src/chat/messages.ts +1 -1
  183. package/src/chat/read-tool-group.ts +6 -4
  184. package/src/chat/skill-message.ts +6 -0
  185. package/src/chat/svg-figure.d.ts +33 -0
  186. package/src/chat/svg-figure.ts +399 -0
  187. package/src/chat/svg-source.d.ts +49 -0
  188. package/src/chat/svg-source.ts +188 -0
  189. package/src/chat/table-chart.d.ts +89 -0
  190. package/src/chat/table-chart.ts +294 -0
  191. package/src/chat/tool-execution.d.ts +2 -0
  192. package/src/chat/tool-execution.ts +86 -7
  193. package/src/chat/user-message.d.ts +17 -4
  194. package/src/chat/user-message.ts +71 -8
  195. package/src/chrome/shared.ts +53 -0
  196. package/src/chrome/status-notice.ts +25 -7
  197. package/src/chrome/transcript-container.d.ts +6 -0
  198. package/src/chrome/transcript-container.ts +162 -19
  199. package/src/components/box.d.ts +1 -0
  200. package/src/components/box.ts +5 -0
  201. package/src/components/disclosure.d.ts +1 -0
  202. package/src/components/disclosure.ts +8 -0
  203. package/src/components/editor.d.ts +23 -5
  204. package/src/components/editor.ts +316 -47
  205. package/src/components/form.d.ts +1 -0
  206. package/src/components/form.ts +8 -0
  207. package/src/components/image.d.ts +26 -44
  208. package/src/components/image.ts +115 -152
  209. package/src/components/input.d.ts +2 -1
  210. package/src/components/input.ts +17 -7
  211. package/src/components/key-value-list.d.ts +1 -0
  212. package/src/components/key-value-list.ts +4 -0
  213. package/src/components/layout/row.d.ts +1 -0
  214. package/src/components/layout/row.ts +7 -0
  215. package/src/components/layout/stack.d.ts +1 -0
  216. package/src/components/layout/stack.ts +7 -0
  217. package/src/components/loader.d.ts +5 -6
  218. package/src/components/loader.ts +10 -17
  219. package/src/components/markdown.d.ts +47 -5
  220. package/src/components/markdown.ts +786 -303
  221. package/src/components/scroll-view.d.ts +1 -0
  222. package/src/components/scroll-view.ts +5 -0
  223. package/src/components/section.d.ts +1 -0
  224. package/src/components/section.ts +5 -0
  225. package/src/components/settings-list.ts +2 -0
  226. package/src/components/table.d.ts +1 -0
  227. package/src/components/table.ts +4 -0
  228. package/src/components/text.d.ts +1 -0
  229. package/src/components/text.ts +7 -0
  230. package/src/components/tree-view.d.ts +7 -0
  231. package/src/components/tree-view.ts +38 -8
  232. package/src/components/truncated-text.d.ts +1 -0
  233. package/src/components/truncated-text.ts +5 -0
  234. package/src/desktop-notify.ts +3 -2
  235. package/src/fuzzy.d.ts +29 -1
  236. package/src/fuzzy.ts +115 -30
  237. package/src/hotkeys-markdown.ts +9 -2
  238. package/src/key-hint-format.d.ts +9 -1
  239. package/src/key-hint-format.ts +26 -11
  240. package/src/keybinding-matchers.d.ts +3 -0
  241. package/src/keybinding-matchers.ts +6 -2
  242. package/src/keybindings.d.ts +1 -1
  243. package/src/keybindings.ts +1 -1
  244. package/src/kitty-graphics.d.ts +2 -2
  245. package/src/kitty-graphics.ts +11 -5
  246. package/src/loop-watchdog.d.ts +8 -7
  247. package/src/loop-watchdog.ts +18 -13
  248. package/src/multiplexers/cmux.d.ts +16 -0
  249. package/src/multiplexers/cmux.ts +29 -0
  250. package/src/multiplexers/herdr.d.ts +26 -0
  251. package/src/multiplexers/herdr.ts +64 -0
  252. package/src/multiplexers/notify.d.ts +13 -0
  253. package/src/multiplexers/notify.ts +24 -0
  254. package/src/multiplexers/orca.d.ts +12 -0
  255. package/src/multiplexers/orca.ts +16 -0
  256. package/src/multiplexers/screen.d.ts +9 -0
  257. package/src/multiplexers/screen.ts +15 -0
  258. package/src/multiplexers/tmux.d.ts +29 -0
  259. package/src/multiplexers/tmux.ts +40 -0
  260. package/src/multiplexers/types.d.ts +100 -0
  261. package/src/multiplexers/types.ts +110 -0
  262. package/src/multiplexers/wmux.d.ts +12 -0
  263. package/src/multiplexers/wmux.ts +16 -0
  264. package/src/multiplexers/zellij.d.ts +12 -0
  265. package/src/multiplexers/zellij.ts +24 -0
  266. package/src/native/apply.d.ts +4 -3
  267. package/src/native/apply.ts +4 -3
  268. package/src/native/backend.d.ts +5 -4
  269. package/src/native/backend.ts +199 -22
  270. package/src/native/blobs.d.ts +7 -3
  271. package/src/native/blobs.ts +47 -12
  272. package/src/native/encode.d.ts +8 -6
  273. package/src/native/encode.ts +76 -34
  274. package/src/native/node.d.ts +36 -5
  275. package/src/native/node.ts +31 -5
  276. package/src/native/overlay.d.ts +8 -3
  277. package/src/native/overlay.ts +17 -4
  278. package/src/native/reconcile.ts +16 -4
  279. package/src/native/settle.d.ts +4 -3
  280. package/src/native/settle.ts +4 -3
  281. package/src/overlays/agent-hub-projection.d.ts +6 -5
  282. package/src/overlays/agent-hub-projection.ts +124 -13
  283. package/src/overlays/agent-hub-types.d.ts +9 -0
  284. package/src/overlays/agent-hub-types.ts +6 -0
  285. package/src/overlays/agent-hub.ts +48 -29
  286. package/src/overlays/agent-transcript-viewer.ts +93 -23
  287. package/src/overlays/agents-hub.ts +28 -8
  288. package/src/overlays/annotation-overlay.ts +293 -90
  289. package/src/overlays/annotation-types.ts +2 -0
  290. package/src/overlays/ask-dialog.ts +10 -17
  291. package/src/overlays/btw-history-panel.ts +136 -39
  292. package/src/overlays/copy-selector.d.ts +1 -0
  293. package/src/overlays/copy-selector.ts +32 -8
  294. package/src/overlays/extensions/extension-dashboard.ts +4 -4
  295. package/src/overlays/extensions/state-manager.ts +52 -12
  296. package/src/overlays/history-search.ts +100 -7
  297. package/src/overlays/hook-selector.ts +12 -5
  298. package/src/overlays/hub-frame.ts +19 -1
  299. package/src/overlays/jobs-panel.ts +33 -52
  300. package/src/overlays/model-browser.ts +367 -122
  301. package/src/overlays/model-hub.ts +435 -59
  302. package/src/overlays/move-overlay.ts +28 -14
  303. package/src/overlays/oauth-selector.ts +54 -70
  304. package/src/overlays/pause-screen.ts +2 -2
  305. package/src/overlays/plan-review-overlay.ts +208 -70
  306. package/src/overlays/plugin-settings.ts +3 -3
  307. package/src/overlays/report-panel.ts +191 -0
  308. package/src/overlays/rewind-selector.ts +88 -17
  309. package/src/overlays/session-observer-registry.d.ts +2 -0
  310. package/src/overlays/session-observer-registry.ts +10 -2
  311. package/src/overlays/session-selector.ts +64 -26
  312. package/src/overlays/settings-defs.ts +5 -4
  313. package/src/overlays/settings-selector.ts +33 -8
  314. package/src/overlays/snapcompact-shape-preview.ts +2 -3
  315. package/src/overlays/stats-notice.ts +1 -1
  316. package/src/overlays/thinking-selector.ts +11 -10
  317. package/src/overlays/tree-selector.ts +41 -20
  318. package/src/overlays/usage-dashboard.ts +117 -58
  319. package/src/overlays/usage-display.ts +10 -4
  320. package/src/overlays/usage-row.ts +22 -8
  321. package/src/prompt/attachment-chips.ts +15 -11
  322. package/src/prompt/composer-attachments.ts +6 -4
  323. package/src/prompt/composer-cache.ts +7 -86
  324. package/src/prompt/composer.ts +37 -26
  325. package/src/prompt/custom-editor.ts +282 -105
  326. package/src/prompt/image-source.ts +2 -1
  327. package/src/prompt/interactive-context-helpers.ts +32 -18
  328. package/src/prompt/macos-spelling.ts +7 -0
  329. package/src/prompt/magic-keywords.ts +11 -2
  330. package/src/prompt/model-mention-autocomplete.ts +14 -14
  331. package/src/prompt/queued-messages.ts +6 -4
  332. package/src/prompt/tips.txt +13 -9
  333. package/src/prompt/welcome.ts +64 -46
  334. package/src/prompt/word-completion.ts +38 -5
  335. package/src/render/composite.d.ts +2 -0
  336. package/src/render/composite.ts +64 -0
  337. package/src/render/output-pane.d.ts +1 -0
  338. package/src/render/output-pane.ts +6 -0
  339. package/src/render/render-utils.d.ts +1 -0
  340. package/src/render/render-utils.ts +16 -1
  341. package/src/render/tool-card.d.ts +6 -0
  342. package/src/render/tool-card.ts +15 -0
  343. package/src/render/width-aware-text.d.ts +2 -0
  344. package/src/render/width-aware-text.ts +5 -0
  345. package/src/setup/scenes/model.ts +2 -1
  346. package/src/setup/scenes/types.ts +1 -0
  347. package/src/space-hold.d.ts +40 -25
  348. package/src/space-hold.ts +156 -52
  349. package/src/status-line/component.ts +292 -78
  350. package/src/status-line/context-usage.ts +21 -46
  351. package/src/status-line/host.ts +4 -1
  352. package/src/status-line/segments.ts +40 -22
  353. package/src/status-line/types.ts +15 -3
  354. package/src/terminal-capabilities.d.ts +41 -36
  355. package/src/terminal-capabilities.ts +137 -213
  356. package/src/terminal-multiplexer.d.ts +127 -14
  357. package/src/terminal-multiplexer.ts +112 -33
  358. package/src/terminal.d.ts +23 -16
  359. package/src/terminal.ts +203 -99
  360. package/src/theme/active-symbols.d.ts +1 -1
  361. package/src/theme/active-symbols.ts +1 -1
  362. package/src/theme/schema-validation.ts +1 -0
  363. package/src/theme/schema.d.ts +11 -2
  364. package/src/theme/schema.ts +14 -1
  365. package/src/theme/session-color.ts +1 -7
  366. package/src/theme/symbols.d.ts +1 -1
  367. package/src/theme/symbols.ts +11 -4
  368. package/src/theme/theme-class.d.ts +7 -0
  369. package/src/theme/theme-class.ts +26 -1
  370. package/src/theme/theme-schema.json +5 -1
  371. package/src/theme/theme.d.ts +1 -1
  372. package/src/theme/theme.ts +8 -4
  373. package/src/theme/tui-adapters.d.ts +3 -0
  374. package/src/theme/tui-adapters.ts +68 -0
  375. package/src/thinking.d.ts +1 -1
  376. package/src/thinking.ts +1 -1
  377. package/src/tmux.d.ts +11 -4
  378. package/src/tmux.ts +47 -11
  379. package/src/tools/ast-edit.ts +59 -35
  380. package/src/tools/ast-grep.ts +35 -16
  381. package/src/tools/bash.d.ts +7 -2
  382. package/src/tools/bash.ts +116 -47
  383. package/src/tools/daemon.d.ts +1 -1
  384. package/src/tools/daemon.ts +1 -1
  385. package/src/tools/default-renderer.ts +35 -19
  386. package/src/tools/edit.ts +142 -120
  387. package/src/tools/eval-format/index.ts +28 -2
  388. package/src/tools/eval-format/javascript.ts +7 -5
  389. package/src/tools/eval.d.ts +22 -3
  390. package/src/tools/eval.ts +364 -208
  391. package/src/tools/fetch.ts +5 -6
  392. package/src/tools/glob.d.ts +4 -0
  393. package/src/tools/glob.ts +33 -65
  394. package/src/tools/grep.ts +81 -48
  395. package/src/tools/index.ts +2 -0
  396. package/src/tools/lsp.ts +81 -59
  397. package/src/tools/mcp.ts +111 -68
  398. package/src/tools/native-view.d.ts +47 -0
  399. package/src/tools/native-view.ts +102 -1
  400. package/src/tools/output-meta.d.ts +11 -1
  401. package/src/tools/output-meta.ts +23 -3
  402. package/src/tools/read.ts +7 -5
  403. package/src/tools/renderer.d.ts +37 -2
  404. package/src/tools/renderer.ts +42 -2
  405. package/src/tools/streaming-output.d.ts +20 -1
  406. package/src/tools/streaming-output.ts +154 -50
  407. package/src/tools/task.d.ts +2 -3
  408. package/src/tools/task.ts +115 -62
  409. package/src/tools/todo.ts +119 -30
  410. package/src/tools/wait.ts +34 -24
  411. package/src/tools/web-search.ts +1 -1
  412. package/src/tools/write.d.ts +9 -2
  413. package/src/tools/write.ts +147 -95
  414. package/src/tools/yield.d.ts +22 -0
  415. package/src/tools/yield.ts +225 -0
  416. package/src/tui.d.ts +36 -0
  417. package/src/tui.ts +666 -217
  418. package/src/utils.d.ts +6 -0
  419. package/src/utils.ts +19 -0
  420. package/src/vim.d.ts +19 -5
  421. package/src/vim.ts +333 -18
  422. package/src/windows-input-mode.d.ts +33 -1
  423. package/src/windows-input-mode.ts +212 -6
@@ -0,0 +1,64 @@
1
+ import { TERMINAL } from "../terminal-capabilities";
2
+ import { extractSegments, sliceByColumn, sliceWithWidth, visibleWidth } from "../utils";
3
+
4
+ const SEGMENT_RESET = "\x1b[0m";
5
+
6
+ /** Splice overlay content into a base line at a specific column. Single-pass optimized. */
7
+ export function compositeLineAt(
8
+ baseLine: string,
9
+ overlayLine: string,
10
+ startCol: number,
11
+ overlayWidth: number,
12
+ totalWidth: number,
13
+ ): string {
14
+ // A full-width image row owns its terminal row; the writer must see its exact placement shape.
15
+ if (startCol === 0 && overlayWidth >= totalWidth && TERMINAL.isImageLine(overlayLine)) return overlayLine;
16
+ if (TERMINAL.isImageLine(baseLine)) {
17
+ // Full-width overlays such as /switch are opaque: replace the
18
+ // Unicode placeholder cells so the image cannot cover the modal.
19
+ // Partial overlays cannot safely splice placement control sequences.
20
+ if (startCol !== 0 || overlayWidth < totalWidth) return baseLine;
21
+ const overlay = sliceWithWidth(overlayLine, 0, totalWidth, true);
22
+ return SEGMENT_RESET + overlay.text + " ".repeat(Math.max(0, totalWidth - overlay.width));
23
+ }
24
+
25
+ // Single pass through baseLine extracts both before and after segments
26
+ const afterStart = startCol + overlayWidth;
27
+ const base = extractSegments(baseLine, startCol, afterStart, totalWidth - afterStart, true);
28
+
29
+ // Extract overlay with width tracking (strict=true to exclude wide chars at boundary)
30
+ const overlay = sliceWithWidth(overlayLine, 0, overlayWidth, true);
31
+
32
+ // Pad segments to target widths
33
+ const beforePad = Math.max(0, startCol - base.beforeWidth);
34
+ const overlayPad = Math.max(0, overlayWidth - overlay.width);
35
+ const actualBeforeWidth = Math.max(startCol, base.beforeWidth);
36
+ const actualOverlayWidth = Math.max(overlayWidth, overlay.width);
37
+ const afterTarget = Math.max(0, totalWidth - actualBeforeWidth - actualOverlayWidth);
38
+ const afterPad = Math.max(0, afterTarget - base.afterWidth);
39
+
40
+ // Compose result
41
+ const r = SEGMENT_RESET;
42
+ const result =
43
+ base.before +
44
+ " ".repeat(beforePad) +
45
+ r +
46
+ overlay.text +
47
+ " ".repeat(overlayPad) +
48
+ r +
49
+ base.after +
50
+ " ".repeat(afterPad);
51
+
52
+ // CRITICAL: Always verify and truncate to terminal width.
53
+ // This is the final safeguard against width overflow which would crash the TUI.
54
+ // Width tracking can drift from actual visible width due to:
55
+ // - Complex ANSI/OSC sequences (hyperlinks, colors)
56
+ // - Wide characters at segment boundaries
57
+ // - Edge cases in segment extraction
58
+ const resultWidth = visibleWidth(result);
59
+ if (resultWidth <= totalWidth) {
60
+ return result;
61
+ }
62
+ // Truncate with strict=true to ensure we don't exceed totalWidth
63
+ return sliceByColumn(result, 0, totalWidth, true);
64
+ }
@@ -92,6 +92,7 @@ export declare class OutputPane implements Component {
92
92
  describe(_cx: DescribeContext): NativeNode;
93
93
  render(width: number): readonly string[];
94
94
  invalidate(): void;
95
+ releaseRenderCaches(): void;
95
96
  }
96
97
  /** Default styling for plain tool output while preserving existing ANSI. */
97
98
  export declare function styleToolOutputLine(line: string, theme: Theme): string;
@@ -302,6 +302,12 @@ export class OutputPane implements Component {
302
302
  this.#text.invalidate();
303
303
  }
304
304
 
305
+ releaseRenderCaches(): void {
306
+ this.#renderKey = "";
307
+ this.#text.setText("");
308
+ this.#text.releaseRenderCaches();
309
+ }
310
+
305
311
  #appendToTail(text: string): void {
306
312
  if (!text) return;
307
313
  if (this.#lines.length === 0) this.#lines.push("");
@@ -298,6 +298,7 @@ export interface RenderedStringCache {
298
298
  export declare function createRenderedStringCache(): RenderedStringCache;
299
299
  /** Drop the memo so the next lookup re-renders (e.g. the render function identity changed). */
300
300
  export declare function invalidateRenderedStringCache(cache: RenderedStringCache): void;
301
+ export declare function releaseRenderedStringCache(cache: RenderedStringCache): void;
301
302
  /** Reuse a rendered string while its theme, expansion, content, and salt match. */
302
303
  export declare function cachedRenderedString(cache: RenderedStringCache | undefined, theme: Theme, expanded: boolean, salt: string, content: string, render: () => string): string;
303
304
  /**
@@ -1042,8 +1042,12 @@ export function formatToolWorkingDirectory(workdir: string | undefined, projectD
1042
1042
  return undefined;
1043
1043
  }
1044
1044
  const relativePath = path.relative(resolvedProjectDir, resolvedWorkdir);
1045
+ // On Windows, `path.relative` across drives returns the absolute target, which is not inside the project.
1045
1046
  const isWithinProject =
1046
- relativePath.length > 0 && !relativePath.startsWith("..") && !relativePath.startsWith(`..${path.sep}`);
1047
+ relativePath.length > 0 &&
1048
+ !path.isAbsolute(relativePath) &&
1049
+ !relativePath.startsWith("..") &&
1050
+ !relativePath.startsWith(`..${path.sep}`);
1047
1051
  const displayWorkdir = isWithinProject ? relativePath : shortenPath(resolvedWorkdir);
1048
1052
  return replaceTabs(displayWorkdir);
1049
1053
  }
@@ -1124,6 +1128,9 @@ export function createCachedComponent(
1124
1128
  invalidate() {
1125
1129
  cached = undefined;
1126
1130
  },
1131
+ releaseRenderCaches() {
1132
+ cached = undefined;
1133
+ },
1127
1134
  };
1128
1135
  }
1129
1136
 
@@ -1156,6 +1163,14 @@ export function invalidateRenderedStringCache(cache: RenderedStringCache): void
1156
1163
  cache.theme = null;
1157
1164
  }
1158
1165
 
1166
+ export function releaseRenderedStringCache(cache: RenderedStringCache): void {
1167
+ cache.theme = null;
1168
+ cache.expanded = false;
1169
+ cache.salt = "";
1170
+ cache.content = "";
1171
+ cache.value = "";
1172
+ }
1173
+
1159
1174
  /** Reuse a rendered string while its theme, expansion, content, and salt match. */
1160
1175
  export function cachedRenderedString(
1161
1176
  cache: RenderedStringCache | undefined,
@@ -46,6 +46,11 @@ export interface ToolCardOptions {
46
46
  paddingY?: number;
47
47
  ignoreTight?: boolean;
48
48
  onInvalidate?: () => void;
49
+ /**
50
+ * Drop builder-owned memos the next build recomputes. Called by
51
+ * {@link ToolCard.releaseRenderCaches}, which never calls `onInvalidate`.
52
+ */
53
+ onReleaseRenderCaches?: () => void;
49
54
  onDispose?: () => void;
50
55
  }
51
56
  /** Map transcript lifecycle terminology onto the output frame's visual states. */
@@ -71,6 +76,7 @@ export declare class ToolCard implements Component {
71
76
  handleInput(data: string): void;
72
77
  setIgnoreTight(ignore: boolean): void;
73
78
  invalidate(): void;
79
+ releaseRenderCaches(): void;
74
80
  dispose(): void;
75
81
  }
76
82
  /** Construct a lazy framed tool card. */
@@ -69,6 +69,11 @@ export interface ToolCardOptions {
69
69
  paddingY?: number;
70
70
  ignoreTight?: boolean;
71
71
  onInvalidate?: () => void;
72
+ /**
73
+ * Drop builder-owned memos the next build recomputes. Called by
74
+ * {@link ToolCard.releaseRenderCaches}, which never calls `onInvalidate`.
75
+ */
76
+ onReleaseRenderCaches?: () => void;
72
77
  onDispose?: () => void;
73
78
  }
74
79
 
@@ -381,6 +386,16 @@ export class ToolCard implements Component {
381
386
  this.#options.onInvalidate?.();
382
387
  }
383
388
 
389
+ releaseRenderCaches(): void {
390
+ this.#block.invalidate();
391
+ this.#lastBlockOptions = undefined;
392
+ this.#plainKey = "";
393
+ this.#plainText.setText("");
394
+ this.#plainText.releaseRenderCaches();
395
+ for (const child of this.#renderedChildren) child.releaseRenderCaches?.();
396
+ this.#options.onReleaseRenderCaches?.();
397
+ }
398
+
384
399
  dispose(): void {
385
400
  if (this.#disposed) return;
386
401
  this.#disposed = true;
@@ -20,6 +20,8 @@ export declare class WidthAwareText implements Component {
20
20
  setCustomBgFn(customBgFn?: (text: string) => string): void;
21
21
  setIgnoreTight(ignore: boolean): this;
22
22
  invalidate(): void;
23
+ /** The formatted text stays: the inner `Text` renders from it, so dropping it would free nothing. */
24
+ releaseRenderCaches(): void;
23
25
  /**
24
26
  * Re-run the formatter on the next render because its inputs changed.
25
27
  * Unlike {@link invalidate}, the inner `Text` keeps its wrap cache, so a
@@ -51,6 +51,11 @@ export class WidthAwareText implements Component {
51
51
  this.#inner.invalidate();
52
52
  }
53
53
 
54
+ /** The formatted text stays: the inner `Text` renders from it, so dropping it would free nothing. */
55
+ releaseRenderCaches(): void {
56
+ this.#inner.releaseRenderCaches();
57
+ }
58
+
54
59
  /**
55
60
  * Re-run the formatter on the next render because its inputs changed.
56
61
  * Unlike {@link invalidate}, the inner `Text` keeps its wrap cache, so a
@@ -1,4 +1,5 @@
1
1
  import type { Model } from "@linxiraos/pi-ai";
2
+ import { modelKind } from "@linxiraos/pi-catalog/types";
2
3
  import type { SgrMouseEvent } from "../../mouse";
3
4
  import { Text } from "../../components/text";
4
5
  import { WizardStep } from "../../components/wizard-step";
@@ -114,7 +115,7 @@ class ModelSceneController implements SetupSceneController {
114
115
  const { available, all, current } = this.#host.ctx.getModels();
115
116
  const source = this.#host.ctx.modelSource;
116
117
  const roles = resolveRoleAssignments(source, all, available);
117
- const items = buildBrowserItems(available);
118
+ const items = buildBrowserItems(available.filter(model => modelKind(model) === "chat"));
118
119
  sortModelItems(items, { roles, mruOrder: source.mruOrder });
119
120
  this.#browser.setRoles(roles);
120
121
  this.#browser.setMruOrder(source.mruOrder);
@@ -29,6 +29,7 @@ export interface SetupHost extends SetupUiHost {
29
29
  readonly disabledProviders: readonly string[];
30
30
  readonly authStorage: AuthStorage;
31
31
  readonly modelSource: ModelBrowserSource;
32
+ /** Available and catalog models of every kind; role chips resolve over all of them, rows list chat models. */
32
33
  getModels(): { available: Model[]; all: Model[]; current: Model | undefined };
33
34
  refreshModels(): Promise<void>;
34
35
  selectModel(model: Model, selector: string): Promise<void>;
@@ -1,45 +1,60 @@
1
- /** Max gap (ms) between two spaces for the later one to count as OS key auto-repeat rather than a
2
- * deliberate press. OS auto-repeat is fast; a deliberate tap (even a fast one) is slower. */
1
+ import { type KeyId } from "./keys";
2
+ /** Printable text a push-to-talk candidate key types (plain or shifted single characters and Space),
3
+ * or undefined for non-printable keys and chords, which the gesture reserves instead of typing. */
4
+ export declare function getSpaceHoldText(data: string, canonical: string | undefined): string | undefined;
5
+ /** Max gap (ms) between two configured-key presses for a later one to count as OS key auto-repeat
6
+ * rather than a deliberate press. OS auto-repeat is fast; a deliberate tap (even a fast one) is slower. */
3
7
  export declare const SPACE_REPEAT_MAX_GAP_MS = 120;
4
- /** Two consecutive inter-space gaps are "mechanical" (machine-driven auto-repeat) when both are
8
+ /** Two consecutive inter-key gaps are "mechanical" (machine-driven auto-repeat) when both are
5
9
  * within {@link SPACE_REPEAT_MAX_GAP_MS} and differ by no more than this — an absolute jitter floor
6
10
  * or, for slower repeat rates, {@link SPACE_REPEAT_JITTER_RATIO} of the smaller gap. OS key-repeat
7
- * is metronomic; a human smashing the bar is fast but irregular, so its deltas never stay this
8
- * steady. */
11
+ * is metronomic; human typing is irregular, so its deltas do not stay this steady. */
9
12
  export declare const SPACE_REPEAT_JITTER_MS = 18;
10
13
  export declare const SPACE_REPEAT_JITTER_RATIO = 0.35;
11
- /** Consecutive mechanical (fast + steady) deltas that confirm the space bar is held and start
12
- * recording. Needs a sustained metronomic cadence, so jittery smashing and deliberate taps never
14
+ /** Consecutive mechanical (fast + steady) deltas that confirm a configured key is held and start
15
+ * recording. Needs a sustained metronomic cadence, so jittery typing and deliberate taps never
13
16
  * reach it. */
14
17
  export declare const SPACE_HOLD_MECHANICAL_RUN = 2;
15
- /** Idle gap (ms) after the last repeated space that counts as the space bar being released, ending
16
- * the push-to-talk recording. Must comfortably exceed the OS key-repeat interval. */
18
+ /** Idle gap (ms) after the last repeated configured-key press that counts as release and ends
19
+ * push-to-talk. Must comfortably exceed the OS key-repeat interval. */
17
20
  export declare const SPACE_HOLD_RELEASE_MS = 250;
18
- /** What a recognized space-bar hold drives — the push-to-talk start and stop. */
21
+ /** A release timer firing more than this (ms) past its deadline means the event loop stalled (a
22
+ * synchronous microphone open, GC, a heavy render) while the bar may still be held: the repeats
23
+ * emitted during the stall are queued but unread, so the stall must not read as a release. */
24
+ export declare const SPACE_HOLD_STALL_SLACK_MS = 100;
25
+ /** What a recognized push-to-talk hold drives — the start and stop callbacks. */
19
26
  export interface SpaceHoldHandler {
20
- /** Gate for the gesture. Returns false to keep the space bar inserting spaces normally, so
21
- * disabling push-to-talk restores plain space behavior. */
27
+ /** Gate for the gesture. Returns false to let matching keys behave normally. */
22
28
  enabled(): boolean;
23
- /** A sustained hold was recognized. The optimistically-typed spaces have already been deleted. */
29
+ /** A sustained hold was recognized; any safely typed candidate text has already been deleted. */
24
30
  onStart(): void;
25
- /** The held space bar was released (an idle gap with no further repeated spaces, or any other key). */
31
+ /** The held key was released (an idle gap with no further repeats, or any other key). */
26
32
  onEnd(): void;
27
33
  }
28
34
  /** What the host must do with a keypress after {@link SpaceHoldGesture.process} has seen it:
29
- * `"pass"` — handle it normally; `"type"` — insert the space now (it is tracked back out if the run
30
- * turns into a hold); `"swallow"` — drop it, the gesture consumed it. */
35
+ * `"pass"` — handle it normally; `"type"` — insert its safely decoded printable text now (it is
36
+ * tracked back out if the run turns into a hold); `"swallow"` — drop it, the gesture consumed it. */
31
37
  export type SpaceHoldStep = "pass" | "type" | "swallow";
32
- /** Space-hold push-to-talk state machine. A held space bar emits OS auto-repeat: a *steady* stream
33
- * of spaces at a fixed fast interval. We watch the inter-space deltas and only recognize a hold once
34
- * {@link SPACE_HOLD_MECHANICAL_RUN} consecutive deltas are "mechanical" — both auto-repeat-fast and
35
- * near-identical (see {@link gapsAreMechanical}). Smashing the bar is fast but jittery and deliberate
36
- * taps are too slow, so neither escalates and both keep typing real spaces; the few spaces typed
37
- * before a real hold is recognized are tracked back out of the host input. */
38
+ /** Push-to-talk key-hold state machine. A held key emits OS auto-repeat at a steady fast interval.
39
+ * We watch inter-key deltas and only recognize a hold once {@link SPACE_HOLD_MECHANICAL_RUN}
40
+ * consecutive deltas are mechanical (see {@link gapsAreMechanical}). Deliberate taps and jittery
41
+ * runs stay typed; only text typed during the current steady run is removed on recognition. */
38
42
  export declare class SpaceHoldGesture {
39
43
  #private;
40
- /** Unset (the default) keeps the space bar typing spaces. */
44
+ /** Configured alternative keys. Alternatives are tracked independently and never combine into one run. */
45
+ get keys(): readonly KeyId[];
46
+ set keys(keys: readonly KeyId[]);
47
+ /** Unset (the default) keeps configured keys behaving normally. */
41
48
  handler: SpaceHoldHandler | undefined;
42
49
  constructor(deleteTyped: (count: number) => void, allowed?: () => boolean);
43
- /** Feed one keypress, `isSpace` when it is a plain space bar press. */
44
- process(isSpace: boolean): SpaceHoldStep;
50
+ /** Pure focused-owner preflight on raw input: whether the key must reach the host before
51
+ * TUI-wide input listeners. Only non-printable bindings are claimed — they would otherwise
52
+ * trigger app shortcuts mid-hold; printable candidates keep the normal listener path so
53
+ * raw-input observers (e.g. extensions) still see typed text, as they did before remapping. */
54
+ shouldRoute(data: string): boolean;
55
+ /** Feed one canonical keypress and, when safely printable, its decoded text length. The host
56
+ * records the actual inserted length with {@link recordTyped} after applying normal typing. */
57
+ process(key: string | undefined, insertedCharacterLength?: number): SpaceHoldStep;
58
+ /** Account for text the host actually inserted for the preceding `"type"` step. */
59
+ recordTyped(insertedCharacterLength: number): void;
45
60
  }
package/src/space-hold.ts CHANGED
@@ -1,22 +1,40 @@
1
- /** Max gap (ms) between two spaces for the later one to count as OS key auto-repeat rather than a
2
- * deliberate press. OS auto-repeat is fast; a deliberate tap (even a fast one) is slower. */
1
+ import { addKeyAliases, canonicalKeyId } from "./keybindings";
2
+ import { extractPrintableText, type KeyId, parseKey } from "./keys";
3
+
4
+ /** Printable text a push-to-talk candidate key types (plain or shifted single characters and Space),
5
+ * or undefined for non-printable keys and chords, which the gesture reserves instead of typing. */
6
+ export function getSpaceHoldText(data: string, canonical: string | undefined): string | undefined {
7
+ if (canonical === undefined) return undefined;
8
+ const shifted = canonical.startsWith("shift+");
9
+ const base = shifted ? canonical.slice("shift+".length) : canonical;
10
+ if (base !== "space" && base.length !== 1) return undefined;
11
+ if (base === "space" && canonical !== "space" && !shifted) return undefined;
12
+ const text = extractPrintableText(data);
13
+ return text && (base !== "space" || text === " ") ? text : undefined;
14
+ }
15
+
16
+ /** Max gap (ms) between two configured-key presses for a later one to count as OS key auto-repeat
17
+ * rather than a deliberate press. OS auto-repeat is fast; a deliberate tap (even a fast one) is slower. */
3
18
  export const SPACE_REPEAT_MAX_GAP_MS = 120;
4
- /** Two consecutive inter-space gaps are "mechanical" (machine-driven auto-repeat) when both are
19
+ /** Two consecutive inter-key gaps are "mechanical" (machine-driven auto-repeat) when both are
5
20
  * within {@link SPACE_REPEAT_MAX_GAP_MS} and differ by no more than this — an absolute jitter floor
6
21
  * or, for slower repeat rates, {@link SPACE_REPEAT_JITTER_RATIO} of the smaller gap. OS key-repeat
7
- * is metronomic; a human smashing the bar is fast but irregular, so its deltas never stay this
8
- * steady. */
22
+ * is metronomic; human typing is irregular, so its deltas do not stay this steady. */
9
23
  export const SPACE_REPEAT_JITTER_MS = 18;
10
24
  export const SPACE_REPEAT_JITTER_RATIO = 0.35;
11
- /** Consecutive mechanical (fast + steady) deltas that confirm the space bar is held and start
12
- * recording. Needs a sustained metronomic cadence, so jittery smashing and deliberate taps never
25
+ /** Consecutive mechanical (fast + steady) deltas that confirm a configured key is held and start
26
+ * recording. Needs a sustained metronomic cadence, so jittery typing and deliberate taps never
13
27
  * reach it. */
14
28
  export const SPACE_HOLD_MECHANICAL_RUN = 2;
15
- /** Idle gap (ms) after the last repeated space that counts as the space bar being released, ending
16
- * the push-to-talk recording. Must comfortably exceed the OS key-repeat interval. */
29
+ /** Idle gap (ms) after the last repeated configured-key press that counts as release and ends
30
+ * push-to-talk. Must comfortably exceed the OS key-repeat interval. */
17
31
  export const SPACE_HOLD_RELEASE_MS = 250;
32
+ /** A release timer firing more than this (ms) past its deadline means the event loop stalled (a
33
+ * synchronous microphone open, GC, a heavy render) while the bar may still be held: the repeats
34
+ * emitted during the stall are queued but unread, so the stall must not read as a release. */
35
+ export const SPACE_HOLD_STALL_SLACK_MS = 100;
18
36
 
19
- /** Whether two consecutive inter-space gaps look machine-driven: both within the auto-repeat band
37
+ /** Whether two consecutive inter-key gaps look machine-driven: both within the auto-repeat band
20
38
  * and steady enough (small absolute or proportional difference). OS key-repeat is metronomic, so
21
39
  * its successive deltas match closely; human smashing is fast but irregular and deliberate taps are
22
40
  * too slow, so neither passes. */
@@ -26,46 +44,62 @@ function gapsAreMechanical(gap: number, prevGap: number): boolean {
26
44
  return Math.abs(gap - prevGap) <= tolerance;
27
45
  }
28
46
 
29
- /** What a recognized space-bar hold drives — the push-to-talk start and stop. */
47
+ /** What a recognized push-to-talk hold drives — the start and stop callbacks. */
30
48
  export interface SpaceHoldHandler {
31
- /** Gate for the gesture. Returns false to keep the space bar inserting spaces normally, so
32
- * disabling push-to-talk restores plain space behavior. */
49
+ /** Gate for the gesture. Returns false to let matching keys behave normally. */
33
50
  enabled(): boolean;
34
- /** A sustained hold was recognized. The optimistically-typed spaces have already been deleted. */
51
+ /** A sustained hold was recognized; any safely typed candidate text has already been deleted. */
35
52
  onStart(): void;
36
- /** The held space bar was released (an idle gap with no further repeated spaces, or any other key). */
53
+ /** The held key was released (an idle gap with no further repeats, or any other key). */
37
54
  onEnd(): void;
38
55
  }
39
56
 
40
57
  /** What the host must do with a keypress after {@link SpaceHoldGesture.process} has seen it:
41
- * `"pass"` — handle it normally; `"type"` — insert the space now (it is tracked back out if the run
42
- * turns into a hold); `"swallow"` — drop it, the gesture consumed it. */
58
+ * `"pass"` — handle it normally; `"type"` — insert its safely decoded printable text now (it is
59
+ * tracked back out if the run turns into a hold); `"swallow"` — drop it, the gesture consumed it. */
43
60
  export type SpaceHoldStep = "pass" | "type" | "swallow";
44
61
 
45
- /** Space-hold push-to-talk state machine. A held space bar emits OS auto-repeat: a *steady* stream
46
- * of spaces at a fixed fast interval. We watch the inter-space deltas and only recognize a hold once
47
- * {@link SPACE_HOLD_MECHANICAL_RUN} consecutive deltas are "mechanical" — both auto-repeat-fast and
48
- * near-identical (see {@link gapsAreMechanical}). Smashing the bar is fast but jittery and deliberate
49
- * taps are too slow, so neither escalates and both keep typing real spaces; the few spaces typed
50
- * before a real hold is recognized are tracked back out of the host input. */
62
+ /** Push-to-talk key-hold state machine. A held key emits OS auto-repeat at a steady fast interval.
63
+ * We watch inter-key deltas and only recognize a hold once {@link SPACE_HOLD_MECHANICAL_RUN}
64
+ * consecutive deltas are mechanical (see {@link gapsAreMechanical}). Deliberate taps and jittery
65
+ * runs stay typed; only text typed during the current steady run is removed on recognition. */
51
66
  export class SpaceHoldGesture {
52
- /** Unset (the default) keeps the space bar typing spaces. */
67
+ /** Canonical aliases of {@link keys}, rebuilt only when the bindings change (not per keystroke). */
68
+ #keyAliases = new Set<string>(["space"]);
69
+ #keys: readonly KeyId[] = ["space"];
70
+ /** Configured alternative keys. Alternatives are tracked independently and never combine into one run. */
71
+ get keys(): readonly KeyId[] {
72
+ return this.#keys;
73
+ }
74
+ set keys(keys: readonly KeyId[]) {
75
+ this.#keys = keys;
76
+ const aliases = new Set<string>();
77
+ for (const key of keys) addKeyAliases(aliases, key);
78
+ this.#keyAliases = aliases;
79
+ }
80
+ /** Unset (the default) keeps configured keys behaving normally. */
53
81
  handler: SpaceHoldHandler | undefined;
54
82
  /** Deletes the given number of characters before the host input's cursor. */
55
83
  readonly #deleteTyped: (count: number) => void;
56
- /** Host input state that rules the gesture out, e.g. an editor mode where space is not text. */
84
+ /** Host input state that rules the gesture out, e.g. an editor mode where keys are not text. */
57
85
  readonly #allowed: () => boolean;
58
- /** Spaces the host typed in the current run; tracked back out when a hold is recognized. */
86
+ /** Safely inserted text units in the current mechanical run; removed if a hold is recognized. */
59
87
  #typed = 0;
60
- /** Consecutive "mechanical" deltas (fast + steady); a sustained run of these confirms a held bar. */
88
+ /** Length of the most recent candidate event actually typed into the host. */
89
+ #lastTypedLength = 0;
90
+ /** Canonical key in the current run, preventing alternate bindings from combining. */
91
+ #runKey: string | undefined;
92
+ /** Consecutive mechanical (fast + steady) deltas; a sustained run confirms a held key. */
61
93
  #mechanicalRun = 0;
62
- /** Inter-space gap (ms) of the previous space pair, compared against the next to judge steadiness. */
94
+ /** Inter-key gap (ms) of the previous pair, compared against the next to judge steadiness. */
63
95
  #prevGap: number | undefined;
64
- /** Monotonic timestamp (ms) of the last space, to measure the gap to the next one. */
65
- #lastSpaceAt = Number.NEGATIVE_INFINITY;
66
- /** True while a recognized space-hold push-to-talk recording is in progress. */
96
+ /** Monotonic timestamp (ms) of the last configured keypress. */
97
+ #lastKeyAt = Number.NEGATIVE_INFINITY;
98
+ /** Canonical key while a recognized push-to-talk recording is in progress. */
99
+ #activeKey: string | undefined;
100
+ /** True while a recognized key-hold push-to-talk recording is in progress. */
67
101
  #active = false;
68
- /** Idle timer that ends the hold once repeated spaces stop arriving. */
102
+ /** Idle timer that ends the hold once repeated keypresses stop arriving. */
69
103
  #releaseTimer: NodeJS.Timeout | undefined;
70
104
 
71
105
  constructor(deleteTyped: (count: number) => void, allowed: () => boolean = () => true) {
@@ -73,58 +107,127 @@ export class SpaceHoldGesture {
73
107
  this.#allowed = allowed;
74
108
  }
75
109
 
76
- /** Feed one keypress, `isSpace` when it is a plain space bar press. */
77
- process(isSpace: boolean): SpaceHoldStep {
110
+ /** Pure focused-owner preflight on raw input: whether the key must reach the host before
111
+ * TUI-wide input listeners. Only non-printable bindings are claimed — they would otherwise
112
+ * trigger app shortcuts mid-hold; printable candidates keep the normal listener path so
113
+ * raw-input observers (e.g. extensions) still see typed text, as they did before remapping. */
114
+ shouldRoute(data: string): boolean {
115
+ if (!(this.handler?.enabled() && this.#allowed())) return false;
116
+ const parsedKey = parseKey(data);
117
+ if (parsedKey === undefined) return false;
118
+ const key = canonicalKeyId(parsedKey);
119
+ return this.#keyAliases.has(key) && getSpaceHoldText(data, key) === undefined;
120
+ }
121
+
122
+ /** Feed one canonical keypress and, when safely printable, its decoded text length. The host
123
+ * records the actual inserted length with {@link recordTyped} after applying normal typing. */
124
+ process(key: string | undefined, insertedCharacterLength = 0): SpaceHoldStep {
125
+ const handler = this.handler;
126
+ if (!(handler?.enabled() && this.#allowed())) {
127
+ if (this.#active) this.#end();
128
+ else this.#resetRun();
129
+ return "pass";
130
+ }
131
+
132
+ const candidateKey = key !== undefined && this.#keyAliases.has(key) ? key : undefined;
133
+
78
134
  if (this.#active) {
79
- if (isSpace) {
80
- // Auto-repeat while held: swallow it and keep the release timer alive.
135
+ if (candidateKey === this.#activeKey) {
136
+ // Auto-repeat while the same configured key is held: keep the release timer alive.
81
137
  this.#armReleaseTimer();
82
138
  return "swallow";
83
139
  }
84
- // Any non-space means the bar was released — stop recording, then let the key through.
140
+ // Any other key means the active key was released. End this recording, then treat a
141
+ // different configured alternative as a fresh gesture rather than merging the keys.
85
142
  this.#end();
86
- return "pass";
87
143
  }
88
- if (!isSpace) {
144
+
145
+ if (candidateKey === undefined) {
89
146
  this.#resetRun();
90
147
  return "pass";
91
148
  }
92
- if (!(this.handler?.enabled() && this.#allowed())) return "pass";
149
+
150
+ if (this.#runKey !== undefined && candidateKey !== this.#runKey) this.#resetRun();
93
151
  const now = performance.now();
94
- const gap = now - this.#lastSpaceAt;
152
+ const hasPreviousKey = this.#lastKeyAt !== Number.NEGATIVE_INFINITY;
153
+ const gap = now - this.#lastKeyAt;
95
154
  const prevGap = this.#prevGap;
96
- this.#lastSpaceAt = now;
155
+ this.#lastKeyAt = now;
156
+ this.#runKey = candidateKey;
157
+ const printable = insertedCharacterLength > 0;
158
+
159
+ if (!hasPreviousKey) {
160
+ this.#mechanicalRun = 0;
161
+ this.#typed = 0;
162
+ this.#lastTypedLength = 0;
163
+ return printable ? "type" : "swallow";
164
+ }
165
+
97
166
  this.#prevGap = gap;
98
- if (prevGap === undefined || !gapsAreMechanical(gap, prevGap)) {
99
- // First space, a deliberate tap, or jittery smashing: not a steady machine cadence yet, so
100
- // type a real space and reset the mechanical run.
167
+ if (prevGap === undefined) {
168
+ // The second event establishes the first interval. A fast gap keeps earlier literal
169
+ // typing eligible for rollback; a slow gap makes that first tap final.
170
+ this.#mechanicalRun = 0;
171
+ if (gap > SPACE_REPEAT_MAX_GAP_MS) {
172
+ this.#typed = 0;
173
+ this.#lastTypedLength = 0;
174
+ }
175
+ return printable ? "type" : "swallow";
176
+ }
177
+
178
+ if (!gapsAreMechanical(gap, prevGap)) {
179
+ // A fast interval after a slow gap may roll back the previous event and this one.
180
+ // Slow current gaps and jitter breaks keep prior text final.
101
181
  this.#mechanicalRun = 0;
102
- this.#typed++;
103
- return "type";
182
+ if (gap <= SPACE_REPEAT_MAX_GAP_MS && prevGap > SPACE_REPEAT_MAX_GAP_MS) {
183
+ this.#typed = this.#lastTypedLength;
184
+ } else {
185
+ this.#typed = 0;
186
+ this.#lastTypedLength = 0;
187
+ }
188
+ return printable ? "type" : "swallow";
104
189
  }
105
- // Steady fast repeat: swallow it. Once the cadence has held for SPACE_HOLD_MECHANICAL_RUN
106
- // deltas it's a held bar — track back the few pre-burst spaces already typed and start.
190
+
191
+ // Steady fast repeat: swallow it. Once the cadence confirms a held key, remove only the
192
+ // literal candidate text from the current uninterrupted run and start recording.
107
193
  if (++this.#mechanicalRun >= SPACE_HOLD_MECHANICAL_RUN) {
108
194
  this.#deleteTyped(this.#typed);
109
195
  this.#resetRun();
110
196
  this.#active = true;
197
+ this.#activeKey = candidateKey;
111
198
  this.#armReleaseTimer();
112
- this.handler?.onStart();
199
+ handler.onStart();
113
200
  }
114
201
  return "swallow";
115
202
  }
116
203
 
204
+ /** Account for text the host actually inserted for the preceding `"type"` step. */
205
+ recordTyped(insertedCharacterLength: number): void {
206
+ const insertedLength = Math.max(0, insertedCharacterLength);
207
+ this.#typed += insertedLength;
208
+ this.#lastTypedLength = insertedLength;
209
+ }
210
+
117
211
  #resetRun(): void {
212
+ this.#lastTypedLength = 0;
118
213
  this.#typed = 0;
214
+ this.#runKey = undefined;
119
215
  this.#mechanicalRun = 0;
120
216
  this.#prevGap = undefined;
121
- this.#lastSpaceAt = Number.NEGATIVE_INFINITY;
217
+ this.#lastKeyAt = Number.NEGATIVE_INFINITY;
122
218
  }
123
219
 
124
220
  #armReleaseTimer(): void {
125
- if (this.#releaseTimer) clearTimeout(this.#releaseTimer);
221
+ clearTimeout(this.#releaseTimer);
222
+ const armedAt = performance.now();
126
223
  this.#releaseTimer = setTimeout(() => {
127
224
  this.#releaseTimer = undefined;
225
+ // Late firing means input went unobserved; wait one more window so queued repeats can
226
+ // re-arm the timer before deciding the key was released.
227
+ if (performance.now() - armedAt > SPACE_HOLD_RELEASE_MS + SPACE_HOLD_STALL_SLACK_MS) {
228
+ this.#armReleaseTimer();
229
+ return;
230
+ }
128
231
  this.#end();
129
232
  }, SPACE_HOLD_RELEASE_MS);
130
233
  this.#releaseTimer.unref?.();
@@ -133,6 +236,7 @@ export class SpaceHoldGesture {
133
236
  #end(): void {
134
237
  if (!this.#active) return;
135
238
  this.#active = false;
239
+ this.#activeKey = undefined;
136
240
  this.#resetRun();
137
241
  if (this.#releaseTimer) {
138
242
  clearTimeout(this.#releaseTimer);