@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
@@ -14,7 +14,7 @@ import { BracketedPasteHandler, decodeReencodedPasteControls } from "../brackete
14
14
  import { canonicalKeyId, getKeybindings, type KeybindingsManager } from "../keybindings";
15
15
  import { extractPrintableText, matchesKey, parseKey } from "../keys";
16
16
  import { KillRing } from "../kill-ring";
17
- import type { TspEditorDecoration, TspEditorProps, TspTone } from "@linxiraos/pi-wire";
17
+ import type { TspEditorDecoration, TspEditorProps, TspText, TspTone } from "@linxiraos/pi-wire";
18
18
  import { col, node } from "../native/describe";
19
19
  import { sameItems, sameProps } from "../native/memo";
20
20
  import { plainText } from "../native/spans";
@@ -74,12 +74,16 @@ export interface NativeEditorLayout {
74
74
  readonly caret: string;
75
75
  }
76
76
 
77
- /** Vim mode as the TSP `editor.mode` label. */
78
- const VIM_MODE_LABELS: Record<VimMode, string> = {
79
- insert: "INSERT",
77
+ /**
78
+ * Vim mode as the TSP `editor.mode` label. Insert carries none: Insert edits like a plain field,
79
+ * and a host such as Tern stops native editing (selection, ⌘A/⌘C/⌘X) for any labelled mode.
80
+ */
81
+ const VIM_MODE_LABELS: Record<VimMode, string | undefined> = {
82
+ insert: undefined,
80
83
  normal: "NORMAL",
81
84
  visual: "VISUAL",
82
85
  "visual-line": "VISUAL",
86
+ replace: "REPLACE",
83
87
  };
84
88
 
85
89
  const PASSTHROUGH_COLOR = (text: string): string => text;
@@ -625,8 +629,11 @@ export class Editor implements Component, Focusable {
625
629
  * dialog's text field; only the prompt composer claims `zeta.editor`).
626
630
  */
627
631
  describeLayout: ((input: NativeNode, cx: DescribeContext) => NativeEditorLayout) | undefined;
628
- /** TSP placeholder for the empty buffer; natively it replaces the rotating ANSI {@link placeholder} hints. */
629
- describePlaceholder: (() => string) | undefined;
632
+ /**
633
+ * TSP placeholder for the empty buffer; natively it replaces the rotating ANSI {@link placeholder} hints.
634
+ * Return a stable value (the same spans array) while it is unchanged: the editor node is reused by identity.
635
+ */
636
+ describePlaceholder: (() => TspText) | undefined;
630
637
  #promptGutter: string | undefined;
631
638
  /** Bumped by {@link invalidate}: host-side decoration inputs (spelling results, settings) changed. */
632
639
  #nativeGeneration = 0;
@@ -666,7 +673,9 @@ export class Editor implements Component, Focusable {
666
673
  // Emacs-style kill ring
667
674
  #killRing = new KillRing();
668
675
  /** Previous edit, for kill/yank chaining, undo coalescing, and the provisional space after a Tab word accept. */
669
- #lastAction: "kill" | "yank" | "type-word" | "accept-word" | null = null;
676
+ #lastAction: "kill" | "yank" | "type-word" | "accept-word" | "replace" | null = null;
677
+ /** Graphemes overwritten in the current `R` session, so Backspace can restore them. */
678
+ #replaceLog: { line: number; col: number; removed: string; text: string }[] = [];
670
679
 
671
680
  // Character jump mode
672
681
  #jumpMode: "forward" | "backward" | null = null;
@@ -713,6 +722,7 @@ export class Editor implements Component, Focusable {
713
722
 
714
723
  // Host-registered atomic chip tokens: exact buffer label → expansion emitted on submit.
715
724
  #atoms: Map<string, string> = new Map();
725
+ #atomsRevision = 0;
716
726
 
717
727
  /** Optional pattern matching atomic placeholder tokens (e.g. `[Image #1, 800x600]` or
718
728
  * `[Paste #2, +30 lines]`) that the editor treats as indivisible: a backspace or forward-delete
@@ -737,9 +747,6 @@ export class Editor implements Component, Focusable {
737
747
  #undoStack: EditorState[] = [];
738
748
  #suspendUndo = false;
739
749
 
740
- // Debounce timer for autocomplete updates
741
- #autocompleteTimeout?: NodeJS.Timeout;
742
-
743
750
  onSubmit?: (text: string) => void | Promise<void>;
744
751
  onAltEnter?: (text: string) => void;
745
752
  onChange?: (text: string) => void;
@@ -754,11 +761,22 @@ export class Editor implements Component, Focusable {
754
761
  onLargePaste?: (text: string, lineCount: number, options: PasteOptions) => boolean;
755
762
  onAutocompleteCancel?: () => void;
756
763
  disableSubmit: boolean = false;
764
+ /** Base editors accept native edits, not atomic sends. Implementations that
765
+ * handle `send` override this readiness check for their submission path. */
766
+ protected get nativeSendable(): boolean {
767
+ return false;
768
+ }
757
769
  /** Placeholder painted right-aligned on the cursor row while the editor is empty and no
758
770
  * autocomplete is open; hidden when it can't keep {@link PLACEHOLDER_MIN_GAP} cells from the
759
771
  * cursor. The host styles it (ANSI allowed). Re-evaluated on every render, so hosts can derive
760
772
  * it from live state. */
761
773
  placeholder?: () => string | undefined;
774
+ /** Whole-message prediction for the next prompt, painted as ghost text after the cursor while
775
+ * the single-line buffer is a strict prefix of it, the cursor sits at its end, and no
776
+ * autocomplete is open. A shown prediction replaces the {@link placeholder}; Tab or Right inserts
777
+ * the rest without submitting. Must be a single line. Re-evaluated on every render, so hosts can
778
+ * derive it from live state. */
779
+ prediction?: () => string | undefined;
762
780
 
763
781
  // Custom top border (for status line integration). Either an eager `content`
764
782
  // (set once, reused every frame) or a `provider` that recomputes lazily just
@@ -1023,6 +1041,7 @@ export class Editor implements Component, Focusable {
1023
1041
  this.#pastes.clear();
1024
1042
  this.#pasteCounter = 0;
1025
1043
  this.#atoms.clear();
1044
+ this.#atomsRevision++;
1026
1045
  this.#historyDraftActive = false;
1027
1046
  }
1028
1047
 
@@ -1062,6 +1081,7 @@ export class Editor implements Component, Focusable {
1062
1081
  if (entry?.draft || this.#historyDraftActive) {
1063
1082
  this.#pastes = new Map(entry?.draft?.pastes);
1064
1083
  this.#atoms = new Map(entry?.draft?.atoms);
1084
+ this.#atomsRevision++;
1065
1085
  this.#pasteCounter = entry?.draft?.pasteCounter ?? 0;
1066
1086
  this.restoreHistoryState(entry?.draft?.restore);
1067
1087
  this.#historyDraftActive = entry?.draft !== undefined;
@@ -1664,7 +1684,10 @@ export class Editor implements Component, Focusable {
1664
1684
 
1665
1685
  const placeholder = this.#getPlaceholder();
1666
1686
  const nativePlaceholder =
1667
- this.describePlaceholder !== undefined && !this.#autocompleteState && this.#isEditorEmpty()
1687
+ this.describePlaceholder !== undefined &&
1688
+ !this.#autocompleteState &&
1689
+ this.#isEditorEmpty() &&
1690
+ !this.#getPredictionSuffix()
1668
1691
  ? this.describePlaceholder()
1669
1692
  : placeholder && plainText(placeholder).trim();
1670
1693
  const atLineEnd = cursorCol >= (lines[cursorLine]?.length ?? 0);
@@ -1687,6 +1710,7 @@ export class Editor implements Component, Focusable {
1687
1710
  const props: TspEditorProps = {
1688
1711
  text,
1689
1712
  cursor,
1713
+ sendable: this.nativeSendable,
1690
1714
  anchor: anchor ?? undefined,
1691
1715
  decor: decor.decor,
1692
1716
  ghost: ghost ? plainText(ghost) : undefined,
@@ -1906,7 +1930,7 @@ export class Editor implements Component, Focusable {
1906
1930
  if (kb.matchesCanonical(canonical, "tui.input.tab") || rightArrowAccepts) {
1907
1931
  const selected = this.#autocompleteList.getSelectedItem();
1908
1932
  // Check for stale autocomplete state due to buffer edits since last refresh
1909
- // (destructive keys or paste can outrun the debounced update).
1933
+ // (destructive keys or paste can outrun the async refresh).
1910
1934
  const currentLine = this.#state.lines[this.#state.cursorLine] ?? "";
1911
1935
  const currentTextBeforeCursor = currentLine.slice(0, this.#state.cursorCol);
1912
1936
  if (!this.#autocompletePrefixMatchesCursorText(currentTextBeforeCursor, selected)) {
@@ -1916,7 +1940,7 @@ export class Editor implements Component, Focusable {
1916
1940
  }
1917
1941
  if (!selected) {
1918
1942
  // An `@` popup whose narrowing filter matched nothing stays open with no
1919
- // candidate (see #debouncedUpdateAutocomplete). Nothing to accept: cancel the
1943
+ // candidate (see #refreshAutocomplete). Nothing to accept: cancel the
1920
1944
  // popup and fall through so Tab keeps its normal completion role and a right
1921
1945
  // arrow at end of line moves the cursor.
1922
1946
  this.#cancelAutocomplete();
@@ -1938,7 +1962,7 @@ export class Editor implements Component, Focusable {
1938
1962
  !this.#selectedCompletionIsSkillNamespace()
1939
1963
  ) {
1940
1964
  const selected = this.#autocompleteList.getSelectedItem();
1941
- // Check for stale autocomplete state due to debounce
1965
+ // Check for stale autocomplete state while an async refresh is pending
1942
1966
  const currentLine = this.#state.lines[this.#state.cursorLine] ?? "";
1943
1967
  const currentTextBeforeCursor = currentLine.slice(0, this.#state.cursorCol);
1944
1968
  if (!this.#autocompletePrefixMatchesCursorText(currentTextBeforeCursor, selected)) {
@@ -2115,15 +2139,18 @@ export class Editor implements Component, Focusable {
2115
2139
  const textBeforeCursor = currentLine.slice(0, this.#state.cursorCol);
2116
2140
  if (
2117
2141
  findLeadingSlashCommandStart(textBeforeCursor) !== null &&
2142
+ textBeforeCursor.trim() !== "/" &&
2118
2143
  this.#isInSubmittedSlashCommandContext() &&
2119
2144
  this.#autocompleteProvider?.trySyncSlashCompletion
2120
2145
  ) {
2121
2146
  const syncResult = this.#autocompleteProvider.trySyncSlashCompletion(textBeforeCursor);
2122
- if (syncResult && syncResult.items.length > 0) {
2147
+ // The collapsed `/skill:` namespace row is not a runnable command; Enter submits
2148
+ // immediately after applying, so only a real command may be completed here.
2149
+ const selected = syncResult?.items.find(item => item.value !== SKILL_NAMESPACE);
2150
+ if (syncResult && selected) {
2123
2151
  // Invalidate any pending async autocomplete so its stale results are discarded
2124
2152
  this.#autocompleteRequestId += 1;
2125
2153
  // Apply the best match and submit the completed command
2126
- const selected = syncResult.items[0]!;
2127
2154
  const result = this.#autocompleteProvider.applyCompletion(
2128
2155
  this.#state.lines,
2129
2156
  this.#state.cursorLine,
@@ -2198,8 +2225,8 @@ export class Editor implements Component, Focusable {
2198
2225
  this.#moveCursor(1, 0); // Cursor movement (within text or history entry)
2199
2226
  }
2200
2227
  } else if (kb.matchesCanonical(canonical, "tui.editor.cursorRight")) {
2201
- // Right walks over the ghost word completion as if it were typed: no trailing space.
2202
- if (this.#acceptWordCompletion({ space: false })) return;
2228
+ // Right walks over the ghost prediction or word completion as if it were typed: no trailing space.
2229
+ if (this.#acceptPrediction() || this.#acceptWordCompletion({ space: false })) return;
2203
2230
  this.#moveCursor(0, 1);
2204
2231
  } else if (kb.matchesCanonical(canonical, "tui.editor.cursorLeft")) {
2205
2232
  // Left
@@ -2242,18 +2269,43 @@ export class Editor implements Component, Focusable {
2242
2269
  return this.isShowingAutocomplete() ? false : this.#runVimKey("escape", vim);
2243
2270
  }
2244
2271
  if (vim.mode === "insert") return false;
2272
+ if (vim.mode === "replace") {
2273
+ if (canonical === "backspace" || matchesKey(data, "backspace") || matchesKey(data, "shift+backspace")) {
2274
+ this.#replaceBackspace();
2275
+ return true;
2276
+ }
2277
+ const replacing = extractPrintableText(data);
2278
+ if (replacing) {
2279
+ for (const seg of segmenter.segment(replacing)) this.#overwriteReplaceGrapheme(seg.segment);
2280
+ return true;
2281
+ }
2282
+ }
2245
2283
 
2284
+ // `f`/`t`/`r` are waiting for a character. Space is that character; arrows and other named
2285
+ // keys cancel, instead of being rewritten to `h`/`l` and inserted or searched for.
2286
+ const awaitingTarget = /[fFtTr]$/.test(vim.pendingText);
2287
+ if (awaitingTarget && (canonical === "space" || data === " ")) return this.#runVimKey(" ", vim);
2246
2288
  const mapped = canonical === undefined ? undefined : VIM_NAV_KEYS[canonical];
2289
+ if (awaitingTarget && mapped !== undefined) return this.#runVimKey("escape", vim);
2247
2290
  if (mapped !== undefined) return this.#runVimKey(mapped, vim);
2248
2291
 
2249
- // Control chords carry no printable text and stay with the host.
2292
+ // Control chords, Enter, and Tab carry no printable text and stay with the host. A half-typed
2293
+ // command (`r`, `f`, `d`) is cancelled first, like character-jump mode, so it cannot outlive a
2294
+ // submit and swallow the first key of the next prompt.
2250
2295
  const printable = extractPrintableText(data);
2251
- if (!printable) return false;
2296
+ if (!printable) {
2297
+ if (vim.pending) this.#runVimKey("escape", vim);
2298
+ return false;
2299
+ }
2252
2300
 
2253
2301
  // Batched stdin can deliver several keystrokes at once, so replay the run one grapheme at a
2254
- // time. A command that drops out of Normal mode part-way (`iabc`) turns the rest of the run
2255
- // back into literal text rather than swallowing it.
2302
+ // time. A command that drops out of Normal mode part-way (`iabc`, `Rxx`) turns the rest of
2303
+ // the run into literal text or replace-mode overwrites rather than more Normal commands.
2256
2304
  for (const seg of segmenter.segment(printable)) {
2305
+ if (vim.mode === "replace") {
2306
+ this.#overwriteReplaceGrapheme(seg.segment);
2307
+ continue;
2308
+ }
2257
2309
  if (this.#runVimKey(seg.segment, vim)) continue;
2258
2310
  this.#insertCharacter(printable.slice(seg.index));
2259
2311
  return true;
@@ -2274,11 +2326,22 @@ export class Editor implements Component, Focusable {
2274
2326
  if (vim.mode !== before || vim.pendingText !== pendingBefore || this.vimSelectedLines !== selectedLinesBefore) {
2275
2327
  this.onVimModeChange?.(vim.mode);
2276
2328
  }
2329
+ if (before === "replace" && vim.mode !== "replace") this.#replaceLog.length = 0;
2330
+ if (vim.mode === "replace" && before !== "replace") this.#replaceLog.length = 0;
2277
2331
  return true;
2278
2332
  }
2279
2333
 
2280
2334
  #applyVimCommands(commands: readonly VimCommand[]): void {
2281
2335
  for (const command of commands) {
2336
+ // A delete or other edit in replace mode invalidates Backspace restores. Motions do not.
2337
+ if (
2338
+ this.#vim?.mode === "replace" &&
2339
+ command.kind !== "move" &&
2340
+ command.kind !== "mode" &&
2341
+ command.kind !== "yank"
2342
+ ) {
2343
+ this.#replaceLog.length = 0;
2344
+ }
2282
2345
  switch (command.kind) {
2283
2346
  case "move":
2284
2347
  this.#moveVimCursor(command.to);
@@ -2310,12 +2373,133 @@ export class Editor implements Component, Focusable {
2310
2373
  case "undo":
2311
2374
  this.#applyUndo();
2312
2375
  break;
2376
+ case "replace":
2377
+ this.#replaceVimSpan(command.from, command.to, command.text);
2378
+ break;
2379
+ case "join":
2380
+ this.#joinVimLines(command.fromLine, command.lines);
2381
+ break;
2382
+ case "indent":
2383
+ this.#indentVimLines(command.fromLine, command.toLine, command.out);
2384
+ break;
2313
2385
  }
2314
2386
  }
2315
2387
  this.#clampVimCursor();
2316
2388
  this.invalidate();
2317
2389
  }
2318
2390
 
2391
+ #replaceVimSpan(from: VimPosition, to: VimPosition, text: string): void {
2392
+ const line = this.#state.lines[from.line] ?? "";
2393
+ if (this.#spanCutsAtomicToken(line, from.col, to.col)) return;
2394
+ this.#recordUndoState();
2395
+ this.#lastAction = null;
2396
+ this.#state.lines[from.line] = line.slice(0, from.col) + text + line.slice(to.col);
2397
+ this.#state.cursorLine = from.line;
2398
+ let last = from.col;
2399
+ for (const seg of segmenter.segment(text)) last = from.col + seg.index;
2400
+ this.#setCursorCol(last);
2401
+ this.#afterVimEdit();
2402
+ }
2403
+
2404
+ #overwriteReplaceGrapheme(grapheme: string): void {
2405
+ const lineIdx = this.#state.cursorLine;
2406
+ const line = this.#state.lines[lineIdx] ?? "";
2407
+ const col = this.#state.cursorCol;
2408
+ if (this.#atomicTokenAt(line, col)) return;
2409
+ // Any other edit since the last overwrite (setText, `x`, paste) resets `#lastAction`, which
2410
+ // makes the logged columns meaningless; start a fresh undo step and a fresh log.
2411
+ if (this.#lastAction !== "replace") {
2412
+ this.#recordUndoState();
2413
+ this.#replaceLog.length = 0;
2414
+ }
2415
+ this.#lastAction = "replace";
2416
+ // Editing a recalled prompt makes it a draft, so Up/Down at an edge move the cursor again
2417
+ // instead of navigating history over the overwritten text (same as `#afterVimEdit`).
2418
+ this.#historyIndex = -1;
2419
+ const end = col >= line.length ? col : nextGraphemeStart(line, col);
2420
+ const removed = line.slice(col, end);
2421
+ this.#state.lines[lineIdx] = line.slice(0, col) + grapheme + line.slice(end);
2422
+ this.#replaceLog.push({ line: lineIdx, col, removed, text: grapheme });
2423
+ this.#setCursorCol(col + grapheme.length);
2424
+ this.#notifyChange();
2425
+ }
2426
+
2427
+ #replaceBackspace(): void {
2428
+ if (this.#lastAction !== "replace") {
2429
+ this.#replaceLog.length = 0;
2430
+ return;
2431
+ }
2432
+ const entry = this.#replaceLog.pop();
2433
+ if (!entry) return;
2434
+ const line = this.#state.lines[entry.line] ?? "";
2435
+ const stillThere = line.slice(entry.col, entry.col + entry.text.length) === entry.text;
2436
+ const atCursor = this.#state.cursorLine === entry.line && this.#state.cursorCol === entry.col + entry.text.length;
2437
+ if (!stillThere) return;
2438
+ if (!atCursor) {
2439
+ this.#replaceLog.push(entry);
2440
+ return;
2441
+ }
2442
+ this.#state.lines[entry.line] =
2443
+ line.slice(0, entry.col) + entry.removed + line.slice(entry.col + entry.text.length);
2444
+ this.#setCursorCol(entry.col);
2445
+ this.#notifyChange();
2446
+ }
2447
+
2448
+ #joinVimLines(fromLine: number, span: number): void {
2449
+ const start = Math.max(0, Math.min(fromLine, this.#state.lines.length - 1));
2450
+ const last = Math.min(start + Math.max(span, 2) - 1, this.#state.lines.length - 1);
2451
+ if (last <= start) return;
2452
+ this.#recordUndoState();
2453
+ let text = this.#state.lines[start] ?? "";
2454
+ let cursor = text.length;
2455
+ for (let i = start + 1; i <= last; i++) {
2456
+ const raw = this.#state.lines[i] ?? "";
2457
+ let trim = /^[ \t]*/.exec(raw)?.[0].length ?? 0;
2458
+ const token = this.#atomicTokenAt(raw, 0);
2459
+ if (token && token.start < trim) trim = token.start;
2460
+ const next = raw.slice(trim);
2461
+ const at = text.length;
2462
+ if (i === start + 1) cursor = at;
2463
+ // Vim join: no extra space after whitespace, before `)`, or for an empty line; two spaces after `.!?`.
2464
+ const gap =
2465
+ text.length === 0 || next.length === 0 || /[ \t]$/.test(text) || next.startsWith(")")
2466
+ ? ""
2467
+ : /[.!?]$/.test(text)
2468
+ ? " "
2469
+ : " ";
2470
+ text += gap + next;
2471
+ }
2472
+ this.#state.lines.splice(start, last - start + 1, text);
2473
+ this.#state.cursorLine = start;
2474
+ this.#setCursorCol(cursor);
2475
+ this.#lastAction = null;
2476
+ this.#afterVimEdit();
2477
+ }
2478
+
2479
+ #indentVimLines(fromLine: number, toLine: number, out: boolean): void {
2480
+ const first = Math.max(0, Math.min(fromLine, this.#state.lines.length - 1));
2481
+ const last = Math.max(first, Math.min(toLine, this.#state.lines.length - 1));
2482
+ // Two spaces, not Vim's shiftwidth of 8: a prompt draft should not jump a tab stop. A leading tab shifts by one tab so the line keeps its own indent style.
2483
+ // Empty lines stay empty, as in Vim, so shifting a paragraph never leaves trailing blanks.
2484
+ const next = this.#state.lines.slice(first, last + 1).map(line => {
2485
+ const text = line ?? "";
2486
+ if (!out) return text.length === 0 ? text : (text.startsWith("\t") ? "\t" : " ") + text;
2487
+ if (text.startsWith("\t")) return text.slice(1);
2488
+ const spaces = /^ */.exec(text)?.[0].length ?? 0;
2489
+ return text.slice(Math.min(2, spaces));
2490
+ });
2491
+ if (next.every((line, i) => line === (this.#state.lines[first + i] ?? ""))) return;
2492
+ this.#recordUndoState();
2493
+ for (let i = first; i <= last; i++) this.#state.lines[i] = next[i - first] ?? "";
2494
+ this.#state.cursorLine = first;
2495
+ const landed = this.#state.lines[first] ?? "";
2496
+ let col = 0;
2497
+ while (col < landed.length && /\s/.test(landed.charAt(col))) col++;
2498
+ this.#setCursorCol(col);
2499
+ this.#lastAction = null;
2500
+ this.#afterVimEdit();
2501
+ }
2502
+
2319
2503
  #moveVimCursor(to: VimPosition): void {
2320
2504
  this.#state.cursorLine = Math.max(0, Math.min(to.line, this.#state.lines.length - 1));
2321
2505
  const line = this.#state.lines[this.#state.cursorLine] ?? "";
@@ -2660,6 +2844,11 @@ export class Editor implements Component, Focusable {
2660
2844
  return this.#textRevision;
2661
2845
  }
2662
2846
 
2847
+ /** Monotonic atom-table revision for render caches: advances whenever {@link atoms} is mutated or replaced. */
2848
+ get atomsRevision(): number {
2849
+ return this.#atomsRevision;
2850
+ }
2851
+
2663
2852
  #notifyChange(text?: string): void {
2664
2853
  this.#textRevision++;
2665
2854
  this.onChange?.(text ?? this.getText());
@@ -2698,6 +2887,7 @@ export class Editor implements Component, Focusable {
2698
2887
  * it — for hosts that re-collapse restored draft text via {@link setText}. */
2699
2888
  registerAtom(label: string, expansion: string): void {
2700
2889
  this.#atoms.set(label, expansion);
2890
+ this.#atomsRevision++;
2701
2891
  }
2702
2892
 
2703
2893
  /** Insert `label` (plus a trailing space) at the cursor and register it as an atom expanding
@@ -2734,6 +2924,7 @@ export class Editor implements Component, Focusable {
2734
2924
  /** Drop every registered atom expansion (draft cleared or replaced by the host). */
2735
2925
  clearAtoms(): void {
2736
2926
  this.#atoms.clear();
2927
+ this.#atomsRevision++;
2737
2928
  }
2738
2929
 
2739
2930
  /**
@@ -2773,7 +2964,7 @@ export class Editor implements Component, Focusable {
2773
2964
  * Mirrors the transient state the key dispatch tears down before this action so the two
2774
2965
  * cannot diverge: a pending character jump is cancelled by any other key, and an open
2775
2966
  * spelling-assist popup is dismissed by anything that is not one of its accept keys (its
2776
- * debounced refresh skips assist mode, so a surviving list would hang around forever).
2967
+ * refresh skips assist mode, so a surviving list would hang around forever).
2777
2968
  * While Vim owns the buffer (Normal or Visual) the operation is Vim's `x` — deleting the
2778
2969
  * selection and returning to Normal in Visual mode, the grapheme under the cursor
2779
2970
  * otherwise. Only Insert mode and Vim-off editors delete straight through. */
@@ -2867,9 +3058,10 @@ export class Editor implements Component, Focusable {
2867
3058
  this.#insertTextAtCursor(text);
2868
3059
  }
2869
3060
 
2870
- /** Terminal-side selection edits on the `editor` node (see {@link applyHostEdit}). */
3061
+ /** Terminal-side selection edits and undo on the `editor` node (see {@link applyHostEdit}). */
2871
3062
  handleNativeEvent(event: NativeUiEvent): void {
2872
3063
  if (event.type === "edit") this.applyHostEdit(event);
3064
+ else if (event.type === "undo") this.#applyUndo();
2873
3065
  }
2874
3066
 
2875
3067
  /**
@@ -3040,6 +3232,20 @@ export class Editor implements Component, Focusable {
3040
3232
  return true;
3041
3233
  }
3042
3234
 
3235
+ /** Whether a character-jump hotkey is waiting for its target character. */
3236
+ get isJumpPending(): boolean {
3237
+ return this.#jumpMode !== null;
3238
+ }
3239
+
3240
+ /** Type literal text through the character-editing pipeline without keybinding dispatch. */
3241
+ typeCharacter(text: string): void {
3242
+ // Same as #handleInputChunk: typed input wins over a stale in-flight provider lookup.
3243
+ if (this.#autocompleteRequestRunning && this.#autocompleteState === null) {
3244
+ this.#invalidateAutocompleteRequests();
3245
+ }
3246
+ this.#insertCharacter(text);
3247
+ }
3248
+
3043
3249
  #insertCharacter(char: string): void {
3044
3250
  this.#exitHistoryForEditing();
3045
3251
  // Undo coalescing: consecutive word typing collapses into one undo unit
@@ -3168,7 +3374,7 @@ export class Editor implements Component, Focusable {
3168
3374
  }
3169
3375
  }
3170
3376
  } else {
3171
- this.#debouncedUpdateAutocomplete();
3377
+ this.#refreshAutocomplete();
3172
3378
  }
3173
3379
  }
3174
3380
 
@@ -3264,7 +3470,7 @@ export class Editor implements Component, Focusable {
3264
3470
  /** Re-evaluate autocomplete triggers for the text ending at the cursor (used after bulk edits). */
3265
3471
  #retriggerAutocompleteAtCursor(): void {
3266
3472
  if (this.#autocompleteState) {
3267
- this.#debouncedUpdateAutocomplete();
3473
+ this.#refreshAutocomplete();
3268
3474
  return;
3269
3475
  }
3270
3476
  const currentLine = this.#state.lines[this.#state.cursorLine] || "";
@@ -3377,6 +3583,23 @@ export class Editor implements Component, Focusable {
3377
3583
  return undefined;
3378
3584
  }
3379
3585
 
3586
+ /** True when `[from, to)` overlaps a placeholder. Counted `r` must no-op rather than split it.
3587
+ * One pass over the line's matches, so a long counted replace stays linear. */
3588
+ #spanCutsAtomicToken(line: string, from: number, to: number): boolean {
3589
+ const re = this.#getAtomicTokenRe();
3590
+ if (re === undefined) return false;
3591
+ re.lastIndex = 0;
3592
+ for (;;) {
3593
+ const match = re.exec(line);
3594
+ if (match === null || match.index >= to) return false;
3595
+ if (match[0].length === 0) {
3596
+ re.lastIndex = match.index + 1;
3597
+ continue;
3598
+ }
3599
+ if (match.index + match[0].length > from) return true;
3600
+ }
3601
+ }
3602
+
3380
3603
  /** Expand the half-open range [start, end) so it never cuts through an atomic
3381
3604
  * placeholder token: a boundary landing inside a token pulls the whole token in. */
3382
3605
  #expandRangeOverAtomicTokens(line: string, start: number, end: number): { start: number; end: number } {
@@ -3446,7 +3669,7 @@ export class Editor implements Component, Focusable {
3446
3669
  this.#cancelAutocomplete();
3447
3670
  this.onAutocompleteUpdate?.();
3448
3671
  } else {
3449
- this.#debouncedUpdateAutocomplete();
3672
+ this.#refreshAutocomplete();
3450
3673
  }
3451
3674
  } else {
3452
3675
  // If autocomplete was cancelled (no matches), re-trigger if we're in a completable context
@@ -3626,7 +3849,7 @@ export class Editor implements Component, Focusable {
3626
3849
  this.#notifyChange();
3627
3850
 
3628
3851
  if (this.#autocompleteState) {
3629
- this.#debouncedUpdateAutocomplete();
3852
+ this.#refreshAutocomplete();
3630
3853
  } else {
3631
3854
  const currentLine = this.#state.lines[this.#state.cursorLine] || "";
3632
3855
  const textBeforeCursor = currentLine.slice(0, this.#state.cursorCol);
@@ -3960,7 +4183,7 @@ export class Editor implements Component, Focusable {
3960
4183
 
3961
4184
  // Update or re-trigger autocomplete after forward delete
3962
4185
  if (this.#autocompleteState) {
3963
- this.#debouncedUpdateAutocomplete();
4186
+ this.#refreshAutocomplete();
3964
4187
  } else {
3965
4188
  const currentLine = this.#state.lines[this.#state.cursorLine] || "";
3966
4189
  const textBeforeCursor = currentLine.slice(0, this.#state.cursorCol);
@@ -4244,7 +4467,7 @@ export class Editor implements Component, Focusable {
4244
4467
  if (!token.includes(" ") && !token.slice(1).includes("/")) {
4245
4468
  // Guard the timing window where the popup was built for an earlier
4246
4469
  // query (e.g. bare `/`) and the user typed further characters before
4247
- // the 100 ms debounced refresh fired: accept the stale skill only
4470
+ // the async refresh landed: accept the stale skill only
4248
4471
  // when the refreshed popup would still surface it (same gate as
4249
4472
  // buildMidPromptSkillCompletions). `tmp` after a bare slash
4250
4473
  // therefore falls through to file completion instead of rewriting
@@ -4417,6 +4640,7 @@ export class Editor implements Component, Focusable {
4417
4640
  }
4418
4641
 
4419
4642
  async #handleTabCompletion(): Promise<void> {
4643
+ if (this.#acceptPrediction()) return;
4420
4644
  if (this.#acceptWordCompletion({ space: true })) return;
4421
4645
  if (!this.#autocompleteProvider) return;
4422
4646
 
@@ -4454,6 +4678,13 @@ export class Editor implements Component, Focusable {
4454
4678
  if (space) this.#lastAction = "accept-word";
4455
4679
  return true;
4456
4680
  }
4681
+ /** Insert the rest of the shown {@link prediction}, if any. Never submits. */
4682
+ #acceptPrediction(): boolean {
4683
+ const suffix = this.#getPredictionSuffix();
4684
+ if (!suffix) return false;
4685
+ this.#insertTextAtCursor(suffix);
4686
+ return true;
4687
+ }
4457
4688
 
4458
4689
  async #showSpellingSuggestions(): Promise<void> {
4459
4690
  const cursorLine = this.#state.cursorLine;
@@ -4531,7 +4762,6 @@ export class Editor implements Component, Focusable {
4531
4762
 
4532
4763
  #cancelAutocomplete(notifyCancel: boolean = false): void {
4533
4764
  const wasAutocompleting = this.#autocompleteState !== null;
4534
- this.#clearAutocompleteTimeout();
4535
4765
  this.#invalidateAutocompleteRequests();
4536
4766
  this.#autocompleteState = null;
4537
4767
  this.#autocompleteList = undefined;
@@ -4650,7 +4880,7 @@ export class Editor implements Component, Focusable {
4650
4880
  this.onAutocompleteUpdate?.();
4651
4881
  }
4652
4882
 
4653
- /** Filter a shown `@` file list to the live `@` token until the debounced refresh replaces it. */
4883
+ /** Filter a shown `@` file list to the live `@` token until the async refresh replaces it. */
4654
4884
  #narrowAtFileList(): void {
4655
4885
  if (!this.#autocompleteList || !this.#autocompletePrefix.startsWith("@")) return;
4656
4886
  const currentLine = this.#state.lines[this.#state.cursorLine] ?? "";
@@ -4666,27 +4896,50 @@ export class Editor implements Component, Focusable {
4666
4896
  for (const resolve of waiters) resolve();
4667
4897
  }
4668
4898
 
4669
- #debouncedUpdateAutocomplete(): void {
4670
- if (this.#autocompleteState !== "assist") this.#narrowAtFileList();
4671
- if (this.#autocompleteTimeout) {
4672
- clearTimeout(this.#autocompleteTimeout);
4673
- }
4674
- this.#autocompleteTimeout = setTimeout(() => {
4675
- void this.#updateAutocomplete();
4676
- this.#autocompleteTimeout = undefined;
4677
- }, 100);
4899
+ /**
4900
+ * Re-query suggestions for the live text after an edit while the popup is open. Runs on
4901
+ * every edit; the request queue coalesces bursts (a newer request replaces the pending one
4902
+ * and aborts the in-flight one), so no timer delays the popup behind the typing.
4903
+ */
4904
+ #refreshAutocomplete(): void {
4905
+ if (this.#autocompleteState === "assist") return;
4906
+ if (this.#refreshSlashCommandListSync()) return;
4907
+ this.#narrowAtFileList();
4908
+ void this.#updateAutocomplete();
4678
4909
  }
4679
4910
 
4680
- #clearAutocompleteTimeout(): void {
4681
- if (this.#autocompleteTimeout) {
4682
- clearTimeout(this.#autocompleteTimeout);
4683
- this.#autocompleteTimeout = undefined;
4911
+ /**
4912
+ * Rebuild a shown slash command-name popup from the live token synchronously. An async
4913
+ * refresh resolves a microtask later at best, so Enter/Tab arriving in the same input
4914
+ * batch would accept a row computed for an earlier query (`/` → `mod` → Enter ran `/login`).
4915
+ * Returns false when the async refresh must handle the edit (arguments, mid-prompt
4916
+ * skills, absolute-path fall-through).
4917
+ */
4918
+ #refreshSlashCommandListSync(): boolean {
4919
+ const provider = this.#autocompleteProvider;
4920
+ if (this.#autocompleteState !== "regular" || !provider?.trySyncSlashCompletion) return false;
4921
+ if (!this.#isInSubmittedSlashCommandContext()) return false;
4922
+ const currentLine = this.#state.lines[this.#state.cursorLine] ?? "";
4923
+ const textBeforeCursor = currentLine.slice(0, this.#state.cursorCol);
4924
+ const token = textBeforeCursor.trimStart();
4925
+ if (token.includes(" ")) return false;
4926
+ const suggestions = provider.trySyncSlashCompletion(textBeforeCursor);
4927
+ if (suggestions) {
4928
+ this.#invalidateAutocompleteRequests();
4929
+ this.#showAutocompleteSuggestions(suggestions, "regular");
4930
+ return true;
4684
4931
  }
4932
+ // No command matches: an inner slash may still be an absolute path, which needs the
4933
+ // async file lookup. Otherwise the async refresh would only close the popup.
4934
+ if (token.slice(1).includes("/")) return false;
4935
+ this.#cancelAutocomplete();
4936
+ this.onAutocompleteUpdate?.();
4937
+ return true;
4685
4938
  }
4686
4939
 
4687
4940
  /**
4688
4941
  * Get inline hint text to show as dim ghost text after the cursor.
4689
- * Checks selected autocomplete item's hint first, then falls back to provider.
4942
+ * Checks the selected autocomplete item's hint first, then the prediction, then the provider.
4690
4943
  */
4691
4944
  #getInlineHint(): string | null {
4692
4945
  // Check selected autocomplete item for a hint
@@ -4695,6 +4948,9 @@ export class Editor implements Component, Focusable {
4695
4948
  return selected?.hint ?? null;
4696
4949
  }
4697
4950
 
4951
+ const prediction = this.#getPredictionSuffix();
4952
+ if (prediction) return prediction;
4953
+
4698
4954
  // Fall back to provider's getInlineHint
4699
4955
  if (this.#autocompleteProvider?.getInlineHint) {
4700
4956
  const hint = this.#autocompleteProvider.getInlineHint(
@@ -4708,9 +4964,22 @@ export class Editor implements Component, Focusable {
4708
4964
  return this.#getWordCompletion();
4709
4965
  }
4710
4966
  #getPlaceholder(): string | undefined {
4711
- if (this.#autocompleteState || !this.#isEditorEmpty()) return undefined;
4967
+ if (this.#autocompleteState || !this.#isEditorEmpty() || this.#getPredictionSuffix()) return undefined;
4712
4968
  return this.placeholder?.() || undefined;
4713
4969
  }
4970
+ /** The part of {@link prediction} not yet typed, or `null` when no prediction is shown. */
4971
+ #getPredictionSuffix(): string | null {
4972
+ if (this.#autocompleteState) return null;
4973
+ const prediction = this.prediction?.();
4974
+ if (!prediction) return null;
4975
+ const { lines, cursorCol } = this.#state;
4976
+ if (lines.length !== 1) return null;
4977
+ const typed = lines[0] ?? "";
4978
+ if (cursorCol !== typed.length || typed.length >= prediction.length || !prediction.startsWith(typed)) {
4979
+ return null;
4980
+ }
4981
+ return prediction.slice(typed.length);
4982
+ }
4714
4983
  /** Typing `typed` where it diverges from the shown ghost completion rejects that ghost. */
4715
4984
  #reportTypedPastWordCompletion(typed: string): void {
4716
4985
  if (!this.#textAssistProvider?.wordCompletionFeedback) return;
@@ -44,6 +44,7 @@ export declare class FormField implements Component, Focusable, MouseRoutable {
44
44
  /** Label, description, details, preview, the control, error, summary, hint and footer as one column. */
45
45
  describe(_cx: DescribeContext): NativeNode;
46
46
  get focused(): boolean;
47
+ get hasError(): boolean;
47
48
  get debugChildren(): readonly Component[];
48
49
  set focused(value: boolean);
49
50
  setUseTerminalCursor(useTerminalCursor: boolean): void;