@linxiraos/pi-tui 1.1.27 → 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 (373) hide show
  1. package/CHANGELOG.md +47 -463
  2. package/README.md +7 -5
  3. package/dist/types/app-keybindings.d.ts +1 -1
  4. package/dist/types/apps/cleanse-board.d.ts +1 -1
  5. package/dist/types/apps/debug/log-viewer.d.ts +4 -0
  6. package/dist/types/apps/debug/raw-sse-buffer.d.ts +2 -0
  7. package/dist/types/apps/git/git-tui.d.ts +2 -2
  8. package/dist/types/apps/git/help.d.ts +3 -1
  9. package/dist/types/apps/if-bench-board.d.ts +2 -2
  10. package/dist/types/apps/setup-model-picker.d.ts +1 -1
  11. package/dist/types/autocomplete.d.ts +5 -2
  12. package/dist/types/charts/chart-plan.d.ts +674 -0
  13. package/dist/types/charts/chart-svg.d.ts +59 -0
  14. package/dist/types/charts/table-data.d.ts +175 -0
  15. package/dist/types/chat/assistant-message.d.ts +5 -0
  16. package/dist/types/chat/chat-transcript-builder.d.ts +2 -0
  17. package/dist/types/chat/compaction-summary-message.d.ts +1 -0
  18. package/dist/types/chat/execution-shared.d.ts +6 -5
  19. package/dist/types/chat/fence-figure.d.ts +42 -0
  20. package/dist/types/chat/figure-markdown.d.ts +42 -0
  21. package/dist/types/chat/messages.d.ts +1 -1
  22. package/dist/types/chat/svg-figure.d.ts +33 -0
  23. package/dist/types/chat/svg-source.d.ts +49 -0
  24. package/dist/types/chat/table-chart.d.ts +89 -0
  25. package/dist/types/chat/tool-execution.d.ts +2 -0
  26. package/dist/types/chat/user-message.d.ts +15 -2
  27. package/dist/types/chrome/shared.d.ts +9 -0
  28. package/dist/types/chrome/transcript-container.d.ts +6 -0
  29. package/dist/types/components/box.d.ts +1 -0
  30. package/dist/types/components/disclosure.d.ts +1 -0
  31. package/dist/types/components/editor.d.ts +15 -4
  32. package/dist/types/components/form.d.ts +1 -0
  33. package/dist/types/components/image.d.ts +12 -44
  34. package/dist/types/components/key-value-list.d.ts +1 -0
  35. package/dist/types/components/layout/row.d.ts +1 -0
  36. package/dist/types/components/layout/stack.d.ts +1 -0
  37. package/dist/types/components/markdown.d.ts +36 -3
  38. package/dist/types/components/scroll-view.d.ts +1 -0
  39. package/dist/types/components/section.d.ts +1 -0
  40. package/dist/types/components/table.d.ts +1 -0
  41. package/dist/types/components/text.d.ts +1 -0
  42. package/dist/types/components/tree-view.d.ts +7 -0
  43. package/dist/types/components/truncated-text.d.ts +1 -0
  44. package/dist/types/fuzzy.d.ts +29 -1
  45. package/dist/types/key-hint-format.d.ts +1 -1
  46. package/dist/types/keybinding-matchers.d.ts +3 -0
  47. package/dist/types/keybindings.d.ts +1 -1
  48. package/dist/types/kitty-graphics.d.ts +2 -2
  49. package/dist/types/loop-watchdog.d.ts +8 -7
  50. package/dist/types/multiplexers/cmux.d.ts +16 -0
  51. package/dist/types/multiplexers/herdr.d.ts +26 -0
  52. package/dist/types/multiplexers/notify.d.ts +13 -0
  53. package/dist/types/multiplexers/orca.d.ts +12 -0
  54. package/dist/types/multiplexers/screen.d.ts +9 -0
  55. package/dist/types/multiplexers/tmux.d.ts +29 -0
  56. package/dist/types/multiplexers/types.d.ts +100 -0
  57. package/dist/types/multiplexers/wmux.d.ts +12 -0
  58. package/dist/types/multiplexers/zellij.d.ts +12 -0
  59. package/dist/types/native/backend.d.ts +3 -2
  60. package/dist/types/native/blobs.d.ts +7 -3
  61. package/dist/types/native/encode.d.ts +3 -4
  62. package/dist/types/native/overlay.d.ts +5 -0
  63. package/dist/types/overlays/agent-hub-projection.d.ts +6 -5
  64. package/dist/types/overlays/agent-hub-types.d.ts +9 -0
  65. package/dist/types/overlays/agent-hub.d.ts +2 -0
  66. package/dist/types/overlays/agent-transcript-viewer.d.ts +11 -2
  67. package/dist/types/overlays/agents-hub.d.ts +9 -0
  68. package/dist/types/overlays/annotation-overlay.d.ts +19 -2
  69. package/dist/types/overlays/annotation-types.d.ts +2 -0
  70. package/dist/types/overlays/ask-dialog.d.ts +5 -2
  71. package/dist/types/overlays/history-search.d.ts +10 -0
  72. package/dist/types/overlays/hook-selector.d.ts +9 -4
  73. package/dist/types/overlays/jobs-panel.d.ts +1 -7
  74. package/dist/types/overlays/model-browser.d.ts +54 -7
  75. package/dist/types/overlays/model-hub.d.ts +23 -0
  76. package/dist/types/overlays/oauth-selector.d.ts +0 -1
  77. package/dist/types/overlays/plan-review-overlay.d.ts +3 -2
  78. package/dist/types/overlays/session-observer-registry.d.ts +2 -0
  79. package/dist/types/overlays/session-selector.d.ts +6 -2
  80. package/dist/types/overlays/settings-defs.d.ts +1 -0
  81. package/dist/types/overlays/usage-dashboard.d.ts +8 -4
  82. package/dist/types/overlays/usage-display.d.ts +3 -1
  83. package/dist/types/prompt/composer.d.ts +1 -1
  84. package/dist/types/prompt/custom-editor.d.ts +11 -8
  85. package/dist/types/prompt/image-source.d.ts +2 -1
  86. package/dist/types/prompt/interactive-context-helpers.d.ts +4 -1
  87. package/dist/types/prompt/model-mention-autocomplete.d.ts +1 -1
  88. package/dist/types/prompt/welcome.d.ts +6 -1
  89. package/dist/types/render/composite.d.ts +2 -0
  90. package/dist/types/render/output-pane.d.ts +1 -0
  91. package/dist/types/render/render-utils.d.ts +1 -0
  92. package/dist/types/render/tool-card.d.ts +6 -0
  93. package/dist/types/render/width-aware-text.d.ts +2 -0
  94. package/dist/types/setup/scenes/types.d.ts +1 -0
  95. package/dist/types/status-line/component.d.ts +3 -0
  96. package/dist/types/status-line/host.d.ts +4 -1
  97. package/dist/types/status-line/segments.d.ts +2 -0
  98. package/dist/types/status-line/types.d.ts +14 -3
  99. package/dist/types/terminal-capabilities.d.ts +27 -35
  100. package/dist/types/terminal-multiplexer.d.ts +127 -14
  101. package/dist/types/terminal.d.ts +22 -6
  102. package/dist/types/theme/active-symbols.d.ts +1 -1
  103. package/dist/types/theme/schema.d.ts +11 -2
  104. package/dist/types/theme/symbols.d.ts +1 -1
  105. package/dist/types/theme/theme-class.d.ts +7 -0
  106. package/dist/types/theme/theme.d.ts +1 -1
  107. package/dist/types/theme/tui-adapters.d.ts +3 -0
  108. package/dist/types/tmux.d.ts +11 -4
  109. package/dist/types/tools/daemon.d.ts +1 -1
  110. package/dist/types/tools/eval.d.ts +22 -3
  111. package/dist/types/tools/glob.d.ts +4 -0
  112. package/dist/types/tools/native-view.d.ts +47 -0
  113. package/dist/types/tools/output-meta.d.ts +11 -1
  114. package/dist/types/tools/renderer.d.ts +37 -2
  115. package/dist/types/tools/streaming-output.d.ts +20 -1
  116. package/dist/types/tools/task.d.ts +0 -4
  117. package/dist/types/tools/write.d.ts +9 -2
  118. package/dist/types/tools/yield.d.ts +22 -0
  119. package/dist/types/tui.d.ts +31 -0
  120. package/dist/types/utils.d.ts +6 -0
  121. package/dist/types/vim.d.ts +19 -5
  122. package/package.json +13 -9
  123. package/src/app-keybindings.d.ts +1 -1
  124. package/src/app-keybindings.ts +1 -1
  125. package/src/apps/autoresearch-dashboard.ts +7 -3
  126. package/src/apps/cleanse-board.ts +2 -2
  127. package/src/apps/cleanse-picker.ts +1 -1
  128. package/src/apps/debug/log-viewer.ts +188 -72
  129. package/src/apps/debug/raw-sse-buffer.ts +8 -0
  130. package/src/apps/debug/raw-sse.ts +39 -5
  131. package/src/apps/git/diff-pane.ts +26 -5
  132. package/src/apps/git/git-tui.ts +2 -2
  133. package/src/apps/git/help.ts +29 -20
  134. package/src/apps/if-bench-board.ts +2 -2
  135. package/src/apps/ps-top.ts +2 -2
  136. package/src/apps/setup-model-picker.ts +1 -1
  137. package/src/autocomplete.d.ts +5 -2
  138. package/src/autocomplete.ts +67 -43
  139. package/src/charts/chart-plan.d.ts +674 -0
  140. package/src/charts/chart-plan.ts +5081 -0
  141. package/src/charts/chart-svg.d.ts +59 -0
  142. package/src/charts/chart-svg.ts +4657 -0
  143. package/src/charts/table-data.d.ts +175 -0
  144. package/src/charts/table-data.ts +1478 -0
  145. package/src/chat/advisor-message.ts +11 -0
  146. package/src/chat/assistant-message.d.ts +5 -0
  147. package/src/chat/assistant-message.ts +219 -50
  148. package/src/chat/chat-transcript-builder.d.ts +2 -0
  149. package/src/chat/chat-transcript-builder.ts +5 -3
  150. package/src/chat/compaction-summary-message.d.ts +1 -0
  151. package/src/chat/compaction-summary-message.ts +8 -0
  152. package/src/chat/execution-shared.d.ts +6 -5
  153. package/src/chat/execution-shared.ts +42 -25
  154. package/src/chat/fence-figure.d.ts +42 -0
  155. package/src/chat/fence-figure.ts +80 -0
  156. package/src/chat/figure-markdown.d.ts +42 -0
  157. package/src/chat/figure-markdown.ts +174 -0
  158. package/src/chat/messages.d.ts +1 -1
  159. package/src/chat/messages.ts +1 -1
  160. package/src/chat/read-tool-group.ts +6 -4
  161. package/src/chat/skill-message.ts +6 -0
  162. package/src/chat/svg-figure.d.ts +33 -0
  163. package/src/chat/svg-figure.ts +399 -0
  164. package/src/chat/svg-source.d.ts +49 -0
  165. package/src/chat/svg-source.ts +188 -0
  166. package/src/chat/table-chart.d.ts +89 -0
  167. package/src/chat/table-chart.ts +294 -0
  168. package/src/chat/tool-execution.d.ts +2 -0
  169. package/src/chat/tool-execution.ts +80 -6
  170. package/src/chat/user-message.d.ts +15 -2
  171. package/src/chat/user-message.ts +55 -3
  172. package/src/chrome/shared.ts +53 -0
  173. package/src/chrome/transcript-container.d.ts +6 -0
  174. package/src/chrome/transcript-container.ts +93 -13
  175. package/src/components/box.d.ts +1 -0
  176. package/src/components/box.ts +5 -0
  177. package/src/components/disclosure.d.ts +1 -0
  178. package/src/components/disclosure.ts +8 -0
  179. package/src/components/editor.d.ts +15 -4
  180. package/src/components/editor.ts +294 -46
  181. package/src/components/form.d.ts +1 -0
  182. package/src/components/form.ts +8 -0
  183. package/src/components/image.d.ts +12 -44
  184. package/src/components/image.ts +47 -151
  185. package/src/components/key-value-list.d.ts +1 -0
  186. package/src/components/key-value-list.ts +4 -0
  187. package/src/components/layout/row.d.ts +1 -0
  188. package/src/components/layout/row.ts +7 -0
  189. package/src/components/layout/stack.d.ts +1 -0
  190. package/src/components/layout/stack.ts +7 -0
  191. package/src/components/markdown.d.ts +36 -3
  192. package/src/components/markdown.ts +433 -318
  193. package/src/components/scroll-view.d.ts +1 -0
  194. package/src/components/scroll-view.ts +5 -0
  195. package/src/components/section.d.ts +1 -0
  196. package/src/components/section.ts +5 -0
  197. package/src/components/settings-list.ts +2 -0
  198. package/src/components/table.d.ts +1 -0
  199. package/src/components/table.ts +4 -0
  200. package/src/components/text.d.ts +1 -0
  201. package/src/components/text.ts +7 -0
  202. package/src/components/tree-view.d.ts +7 -0
  203. package/src/components/tree-view.ts +38 -8
  204. package/src/components/truncated-text.d.ts +1 -0
  205. package/src/components/truncated-text.ts +5 -0
  206. package/src/desktop-notify.ts +1 -1
  207. package/src/fuzzy.d.ts +29 -1
  208. package/src/fuzzy.ts +115 -30
  209. package/src/key-hint-format.d.ts +1 -1
  210. package/src/key-hint-format.ts +1 -1
  211. package/src/keybinding-matchers.d.ts +3 -0
  212. package/src/keybinding-matchers.ts +6 -2
  213. package/src/keybindings.d.ts +1 -1
  214. package/src/keybindings.ts +1 -1
  215. package/src/kitty-graphics.d.ts +2 -2
  216. package/src/kitty-graphics.ts +4 -4
  217. package/src/loop-watchdog.d.ts +8 -7
  218. package/src/loop-watchdog.ts +18 -13
  219. package/src/multiplexers/cmux.d.ts +16 -0
  220. package/src/multiplexers/cmux.ts +29 -0
  221. package/src/multiplexers/herdr.d.ts +26 -0
  222. package/src/multiplexers/herdr.ts +64 -0
  223. package/src/multiplexers/notify.d.ts +13 -0
  224. package/src/multiplexers/notify.ts +24 -0
  225. package/src/multiplexers/orca.d.ts +12 -0
  226. package/src/multiplexers/orca.ts +16 -0
  227. package/src/multiplexers/screen.d.ts +9 -0
  228. package/src/multiplexers/screen.ts +15 -0
  229. package/src/multiplexers/tmux.d.ts +29 -0
  230. package/src/multiplexers/tmux.ts +40 -0
  231. package/src/multiplexers/types.d.ts +100 -0
  232. package/src/multiplexers/types.ts +110 -0
  233. package/src/multiplexers/wmux.d.ts +12 -0
  234. package/src/multiplexers/wmux.ts +16 -0
  235. package/src/multiplexers/zellij.d.ts +12 -0
  236. package/src/multiplexers/zellij.ts +24 -0
  237. package/src/native/backend.d.ts +3 -2
  238. package/src/native/backend.ts +174 -18
  239. package/src/native/blobs.d.ts +7 -3
  240. package/src/native/blobs.ts +47 -12
  241. package/src/native/encode.d.ts +3 -4
  242. package/src/native/encode.ts +58 -31
  243. package/src/native/overlay.d.ts +5 -0
  244. package/src/native/overlay.ts +13 -0
  245. package/src/native/reconcile.ts +4 -0
  246. package/src/overlays/agent-hub-projection.d.ts +6 -5
  247. package/src/overlays/agent-hub-projection.ts +124 -13
  248. package/src/overlays/agent-hub-types.d.ts +9 -0
  249. package/src/overlays/agent-hub-types.ts +6 -0
  250. package/src/overlays/agent-hub.ts +44 -29
  251. package/src/overlays/agent-transcript-viewer.ts +91 -23
  252. package/src/overlays/agents-hub.ts +28 -8
  253. package/src/overlays/annotation-overlay.ts +293 -90
  254. package/src/overlays/annotation-types.ts +2 -0
  255. package/src/overlays/ask-dialog.ts +7 -10
  256. package/src/overlays/btw-history-panel.ts +12 -1
  257. package/src/overlays/copy-selector.ts +30 -8
  258. package/src/overlays/extensions/state-manager.ts +52 -12
  259. package/src/overlays/history-search.ts +100 -7
  260. package/src/overlays/hook-selector.ts +12 -5
  261. package/src/overlays/jobs-panel.ts +28 -46
  262. package/src/overlays/model-browser.ts +341 -110
  263. package/src/overlays/model-hub.ts +300 -49
  264. package/src/overlays/move-overlay.ts +28 -14
  265. package/src/overlays/oauth-selector.ts +54 -70
  266. package/src/overlays/plan-review-overlay.ts +194 -70
  267. package/src/overlays/plugin-settings.ts +3 -3
  268. package/src/overlays/report-panel.ts +2 -2
  269. package/src/overlays/rewind-selector.ts +86 -17
  270. package/src/overlays/session-observer-registry.d.ts +2 -0
  271. package/src/overlays/session-observer-registry.ts +10 -2
  272. package/src/overlays/session-selector.ts +64 -26
  273. package/src/overlays/settings-defs.ts +4 -3
  274. package/src/overlays/settings-selector.ts +33 -8
  275. package/src/overlays/snapcompact-shape-preview.ts +2 -3
  276. package/src/overlays/stats-notice.ts +1 -1
  277. package/src/overlays/tree-selector.ts +41 -20
  278. package/src/overlays/usage-dashboard.ts +100 -50
  279. package/src/overlays/usage-display.ts +10 -4
  280. package/src/prompt/attachment-chips.ts +15 -11
  281. package/src/prompt/composer-attachments.ts +6 -4
  282. package/src/prompt/composer.ts +4 -15
  283. package/src/prompt/custom-editor.ts +54 -26
  284. package/src/prompt/image-source.ts +2 -1
  285. package/src/prompt/interactive-context-helpers.ts +30 -18
  286. package/src/prompt/macos-spelling.ts +7 -0
  287. package/src/prompt/magic-keywords.ts +11 -2
  288. package/src/prompt/model-mention-autocomplete.ts +14 -14
  289. package/src/prompt/tips.txt +6 -5
  290. package/src/prompt/welcome.ts +55 -12
  291. package/src/prompt/word-completion.ts +38 -5
  292. package/src/render/composite.d.ts +2 -0
  293. package/src/render/composite.ts +64 -0
  294. package/src/render/output-pane.d.ts +1 -0
  295. package/src/render/output-pane.ts +6 -0
  296. package/src/render/render-utils.d.ts +1 -0
  297. package/src/render/render-utils.ts +11 -0
  298. package/src/render/tool-card.d.ts +6 -0
  299. package/src/render/tool-card.ts +15 -0
  300. package/src/render/width-aware-text.d.ts +2 -0
  301. package/src/render/width-aware-text.ts +5 -0
  302. package/src/setup/scenes/model.ts +2 -1
  303. package/src/setup/scenes/types.ts +1 -0
  304. package/src/status-line/component.ts +292 -78
  305. package/src/status-line/host.ts +4 -1
  306. package/src/status-line/segments.ts +40 -22
  307. package/src/status-line/types.ts +15 -3
  308. package/src/terminal-capabilities.d.ts +27 -35
  309. package/src/terminal-capabilities.ts +75 -189
  310. package/src/terminal-multiplexer.d.ts +127 -14
  311. package/src/terminal-multiplexer.ts +112 -33
  312. package/src/terminal.d.ts +22 -6
  313. package/src/terminal.ts +139 -37
  314. package/src/theme/active-symbols.d.ts +1 -1
  315. package/src/theme/active-symbols.ts +1 -1
  316. package/src/theme/schema-validation.ts +1 -0
  317. package/src/theme/schema.d.ts +11 -2
  318. package/src/theme/schema.ts +14 -1
  319. package/src/theme/session-color.ts +1 -7
  320. package/src/theme/symbols.d.ts +1 -1
  321. package/src/theme/symbols.ts +6 -1
  322. package/src/theme/theme-class.d.ts +7 -0
  323. package/src/theme/theme-class.ts +26 -1
  324. package/src/theme/theme-schema.json +5 -1
  325. package/src/theme/theme.d.ts +1 -1
  326. package/src/theme/theme.ts +8 -4
  327. package/src/theme/tui-adapters.d.ts +3 -0
  328. package/src/theme/tui-adapters.ts +68 -0
  329. package/src/tmux.d.ts +11 -4
  330. package/src/tmux.ts +47 -11
  331. package/src/tools/ast-edit.ts +59 -35
  332. package/src/tools/ast-grep.ts +35 -16
  333. package/src/tools/bash.ts +92 -42
  334. package/src/tools/daemon.d.ts +1 -1
  335. package/src/tools/daemon.ts +1 -1
  336. package/src/tools/default-renderer.ts +35 -19
  337. package/src/tools/edit.ts +142 -120
  338. package/src/tools/eval-format/index.ts +28 -2
  339. package/src/tools/eval-format/javascript.ts +7 -5
  340. package/src/tools/eval.d.ts +22 -3
  341. package/src/tools/eval.ts +364 -208
  342. package/src/tools/fetch.ts +5 -6
  343. package/src/tools/glob.d.ts +4 -0
  344. package/src/tools/glob.ts +33 -65
  345. package/src/tools/grep.ts +81 -48
  346. package/src/tools/index.ts +2 -0
  347. package/src/tools/lsp.ts +81 -59
  348. package/src/tools/mcp.ts +111 -68
  349. package/src/tools/native-view.d.ts +47 -0
  350. package/src/tools/native-view.ts +102 -1
  351. package/src/tools/output-meta.d.ts +11 -1
  352. package/src/tools/output-meta.ts +23 -3
  353. package/src/tools/read.ts +7 -5
  354. package/src/tools/renderer.d.ts +37 -2
  355. package/src/tools/renderer.ts +42 -2
  356. package/src/tools/streaming-output.d.ts +20 -1
  357. package/src/tools/streaming-output.ts +149 -50
  358. package/src/tools/task.d.ts +0 -4
  359. package/src/tools/task.ts +112 -59
  360. package/src/tools/todo.ts +119 -30
  361. package/src/tools/wait.ts +34 -24
  362. package/src/tools/web-search.ts +1 -1
  363. package/src/tools/write.d.ts +9 -2
  364. package/src/tools/write.ts +147 -95
  365. package/src/tools/yield.d.ts +22 -0
  366. package/src/tools/yield.ts +225 -0
  367. package/src/tui.d.ts +31 -0
  368. package/src/tui.ts +309 -160
  369. package/src/utils.d.ts +6 -0
  370. package/src/utils.ts +19 -0
  371. package/src/vim.d.ts +19 -5
  372. package/src/vim.ts +333 -18
  373. package/src/windows-input-mode.ts +8 -0
@@ -4,7 +4,8 @@
4
4
  * to the session's `local://` root, and generated video contact-sheet previews.
5
5
  * Symbol metadata stays out of serialized/model-bound image data while traveling
6
6
  * with the draft object, until AgentSession creates the hidden companion message
7
- * that tells the model the path (and link materialization prefers it over a blob copy).
7
+ * that tells the model the path. The model keeps `local://` references across
8
+ * `/move`; chips for internal URLs instead use a stable blob copy.
8
9
  */
9
10
  import type { ImageContent } from "@linxiraos/pi-ai";
10
11
  import { isRecord } from "@linxiraos/pi-utils";
@@ -1,5 +1,5 @@
1
1
  /** Shared assistant transcript construction and model-authored link caching. */
2
- import type { AssistantMessage, Model } from "@linxiraos/pi-ai";
2
+ import type { AssistantMessage, Model, TextContent } from "@linxiraos/pi-ai";
3
3
  import { getMarkdownLinkUrls } from "../index";
4
4
  import { EMPTY_LINK_TARGETS } from "../render/render-utils";
5
5
  import type { ImageBudget } from "../components/image";
@@ -18,10 +18,13 @@ export interface AssistantMessageHost {
18
18
  readonly proseOnlyThinking: boolean;
19
19
  readonly expandThinkingBlocks: boolean;
20
20
  readonly assistantImagesVisible: boolean;
21
+ /** Whether the viewed session's tables get charts: the main session's do, a focused subagent's do not. */
22
+ readonly tableChartsVisible: boolean;
21
23
  readonly hideToolActivity: boolean;
22
24
  readonly toolOutputExpanded: boolean;
23
25
  readonly ui: { requestRender(): void; readonly imageBudget: ImageBudget };
24
- resolveAssistantMessageLinks(texts: readonly string[]): Promise<ReadonlyMap<string, string>>;
26
+ /** Resolve already-extracted Markdown link destinations to local targets. */
27
+ resolveAssistantMessageLinkHrefs(hrefs: readonly string[]): Promise<ReadonlyMap<string, string>>;
25
28
  }
26
29
 
27
30
  const kMarkdownLinkTargets = Symbol("markdownLinkTargets");
@@ -29,14 +32,26 @@ type SessionWithMarkdownLinkTargets = AssistantMessageSession & {
29
32
  [kMarkdownLinkTargets]?: ReadonlyMap<string, string>;
30
33
  };
31
34
 
32
- function assistantTextBlocks(messages: readonly AssistantMessage[]): string[] {
33
- const texts: string[] = [];
34
- for (const message of messages) {
35
- for (const content of message.content) {
36
- if (content.type === "text") texts.push(content.text);
35
+ /** Link destinations per text block, revalidated against the block's text (streaming blocks grow in place). */
36
+ const textBlockHrefs = new WeakMap<TextContent, { text: string; hrefs: readonly string[] }>();
37
+
38
+ /**
39
+ * Distinct Markdown link destinations authored in `message`'s text blocks.
40
+ * Each block is lexed once per text revision; transcript rebuilds and the
41
+ * message_end refresh reuse the result.
42
+ */
43
+ function assistantMessageLinkHrefs(message: AssistantMessage): string[] {
44
+ const hrefs = new Set<string>();
45
+ for (const content of message.content) {
46
+ if (content.type !== "text") continue;
47
+ let memo = textBlockHrefs.get(content);
48
+ if (memo?.text !== content.text) {
49
+ memo = { text: content.text, hrefs: getMarkdownLinkUrls(content.text) };
50
+ textBlockHrefs.set(content, memo);
37
51
  }
52
+ for (const href of memo.hrefs) hrefs.add(href);
38
53
  }
39
- return texts;
54
+ return [...hrefs];
40
55
  }
41
56
 
42
57
  /**
@@ -51,13 +66,12 @@ export async function refreshAssistantMessageLinkTargets(
51
66
  ): Promise<ReadonlyMap<string, string>> {
52
67
  const session: SessionWithMarkdownLinkTargets = ctx.viewSession;
53
68
  const previous = session[kMarkdownLinkTargets] ?? EMPTY_LINK_TARGETS;
54
- const texts = assistantTextBlocks(messages);
55
69
  const hrefs = new Set<string>();
56
- for (const text of texts) {
57
- for (const href of getMarkdownLinkUrls(text)) hrefs.add(href);
70
+ for (const message of messages) {
71
+ for (const href of assistantMessageLinkHrefs(message)) hrefs.add(href);
58
72
  }
59
73
  if (hrefs.size === 0) return previous;
60
- const resolved = await ctx.resolveAssistantMessageLinks(texts);
74
+ const resolved = await ctx.resolveAssistantMessageLinkHrefs([...hrefs]);
61
75
  let changed = false;
62
76
  for (const href of hrefs) {
63
77
  if (previous.get(href) !== resolved.get(href)) {
@@ -85,12 +99,9 @@ export function assistantMessageLinkTargets(
85
99
  targets: ReadonlyMap<string, string>,
86
100
  ): ReadonlyMap<string, string> {
87
101
  const selected = new Map<string, string>();
88
- for (const content of message.content) {
89
- if (content.type !== "text") continue;
90
- for (const href of getMarkdownLinkUrls(content.text)) {
91
- const target = targets.get(href);
92
- if (target) selected.set(href, target);
93
- }
102
+ for (const href of assistantMessageLinkHrefs(message)) {
103
+ const target = targets.get(href);
104
+ if (target) selected.set(href, target);
94
105
  }
95
106
  return selected;
96
107
  }
@@ -116,6 +127,7 @@ export function createAssistantMessageComponent(
116
127
  ctx.expandThinkingBlocks,
117
128
  );
118
129
  component.setImagesVisible(ctx.assistantImagesVisible);
130
+ component.setTableChartsVisible(ctx.tableChartsVisible);
119
131
  component.setToolResultImagesVisible(!ctx.hideToolActivity);
120
132
  component.setExpanded(ctx.toolOutputExpanded);
121
133
  // A wire the `stream-revision` axis marks `possible` can rewrite text it has
@@ -56,6 +56,8 @@ export class MacOSSpellingProvider implements EditorTextAssistProvider {
56
56
  #availabilityChecked = false;
57
57
  #cacheGeneration = 0;
58
58
  #typoCache = new Map<string, readonly native.SpellingRange[]>();
59
+ /** {@link #projectTypoRanges} answers for the current {@link #typoCache}; segments re-ask every frame. */
60
+ #projectionMemo = new Map<string, readonly native.SpellingRange[] | undefined>();
59
61
  #typoInFlight = new Map<string, Promise<readonly native.SpellingRange[]>>();
60
62
  #automaticTypoActive = false;
61
63
  #automaticTypoQueue = new Map<string, string>();
@@ -237,6 +239,7 @@ export class MacOSSpellingProvider implements EditorTextAssistProvider {
237
239
  return request;
238
240
  }
239
241
  #projectTypoRanges(text: string): readonly native.SpellingRange[] | undefined {
242
+ if (this.#projectionMemo.has(text)) return this.#projectionMemo.get(text);
240
243
  let projected: native.SpellingRange[] | undefined;
241
244
  let matchLength = -1;
242
245
  for (const [previous, ranges] of this.#typoCache) {
@@ -265,6 +268,8 @@ export class MacOSSpellingProvider implements EditorTextAssistProvider {
265
268
  });
266
269
  matchLength = prefix + suffix;
267
270
  }
271
+ if (this.#projectionMemo.size >= CACHE_LIMIT) this.#projectionMemo.clear();
272
+ this.#projectionMemo.set(text, projected);
268
273
  return projected;
269
274
  }
270
275
 
@@ -316,6 +321,7 @@ export class MacOSSpellingProvider implements EditorTextAssistProvider {
316
321
  const hadProjectedRanges = (this.#projectTypoRanges(text)?.length ?? 0) > 0;
317
322
  if (this.#typoCache.size >= CACHE_LIMIT) this.#typoCache.clear();
318
323
  this.#typoCache.set(text, ranges);
324
+ this.#projectionMemo.clear();
319
325
  if (ranges.length > 0 || hadProjectedRanges) this.onUpdate?.();
320
326
  return ranges;
321
327
  }
@@ -323,6 +329,7 @@ export class MacOSSpellingProvider implements EditorTextAssistProvider {
323
329
  #clearCaches(): void {
324
330
  this.#cacheGeneration++;
325
331
  this.#typoCache.clear();
332
+ this.#projectionMemo.clear();
326
333
  this.#typoInFlight.clear();
327
334
  this.#automaticTypoQueue.clear();
328
335
  }
@@ -148,6 +148,9 @@ export function magicKeywordRanges(text: string): { from: number; to: number }[]
148
148
  return ranges.sort((a, b) => a.from - b.from);
149
149
  }
150
150
 
151
+ /** Last {@link hasMagicKeyword} answer: the live editor asks once per layout segment with the same buffer. */
152
+ let lastProbe: { text: string; registry: readonly RegisteredKeyword[]; found: boolean } | undefined;
153
+
151
154
  /**
152
155
  * Cheap test for "does this text contain any registered keyword as standalone
153
156
  * prose?". Short-circuits on a substring probe before paying for the
@@ -155,8 +158,14 @@ export function magicKeywordRanges(text: string): { from: number; to: number }[]
155
158
  * `String#includes` per word. Used by the live editor to gate the shimmer timer.
156
159
  */
157
160
  export function hasMagicKeyword(text: string): boolean {
161
+ if (lastProbe?.text === text && lastProbe.registry === registry) return lastProbe.found;
162
+ let found = false;
158
163
  for (const keyword of registry) {
159
- if (text.includes(keyword.word) && containsMagicKeyword(text, keyword.word)) return true;
164
+ if (text.includes(keyword.word) && containsMagicKeyword(text, keyword.word)) {
165
+ found = true;
166
+ break;
167
+ }
160
168
  }
161
- return false;
169
+ lastProbe = { text, registry, found };
170
+ return found;
162
171
  }
@@ -5,9 +5,7 @@ import { modelMentionDisplayName } from "./model-mention-syntax";
5
5
  import type {
6
6
  buildSearchAffinity as BuildSearchAffinity,
7
7
  ModelBrowserItem,
8
- rankModelItems as RankModelItems,
9
- SearchAffinity,
10
- SessionModelScope,
8
+ ModelItemRanker as ModelItemRankerClass,
11
9
  SessionModelScopeCache as SessionModelScopeCacheClass,
12
10
  } from "../overlays/model-browser";
13
11
  import { theme } from "../theme/theme";
@@ -21,7 +19,7 @@ export type ModelMentionCandidateSource = (query: string) => ReadonlyArray<Model
21
19
  interface ModelBrowserModules {
22
20
  SessionModelScopeCache: typeof SessionModelScopeCacheClass;
23
21
  buildSearchAffinity: typeof BuildSearchAffinity;
24
- rankModelItems: typeof RankModelItems;
22
+ ModelItemRanker: typeof ModelItemRankerClass;
25
23
  }
26
24
 
27
25
  /** Synchronous first-use boundary for the interactive model browser implementation. */
@@ -80,7 +78,7 @@ export function applyModelMentionCompletion(
80
78
 
81
79
  /**
82
80
  * Create a session-scoped model candidate lookup using picker ordering. The
83
- * scope and search affinity are reused across queries until their inputs change.
81
+ * scope and its prepared ranker are reused across queries until their inputs change.
84
82
  */
85
83
  export function createModelMentionSource(host: {
86
84
  source: ModelBrowserSource;
@@ -88,20 +86,22 @@ export function createModelMentionSource(host: {
88
86
  scopedModels: () => ReadonlyArray<Model>;
89
87
  }): ModelMentionCandidateSource {
90
88
  let scopeCache: SessionModelScopeCacheClass | undefined;
91
- let affinityScope: SessionModelScope | undefined;
92
- let affinityProviderOrder: readonly string[] | undefined;
93
- let affinity: SearchAffinity | undefined;
89
+ let ranker: ModelItemRankerClass | undefined;
90
+ let rankerProviderOrder: readonly string[] | undefined;
94
91
  return query => {
95
- const { SessionModelScopeCache, buildSearchAffinity, rankModelItems } = loadModelBrowser();
92
+ const { SessionModelScopeCache, buildSearchAffinity, ModelItemRanker } = loadModelBrowser();
96
93
  scopeCache ??= new SessionModelScopeCache(host.source, host.registry);
97
94
  const scope = scopeCache.get(host.scopedModels());
98
95
  if (!query.trim()) return scope.items;
99
96
  const providerOrder = host.source.modelProviderOrder;
100
- if (!affinity || affinityScope !== scope || affinityProviderOrder !== providerOrder) {
101
- affinity = buildSearchAffinity(providerOrder, scope.roles, scope.mruOrder);
102
- affinityScope = scope;
103
- affinityProviderOrder = providerOrder;
97
+ if (ranker?.items !== scope.items || rankerProviderOrder !== providerOrder) {
98
+ ranker = new ModelItemRanker(scope.items, {
99
+ roles: scope.roles,
100
+ mruOrder: scope.mruOrder,
101
+ affinity: buildSearchAffinity(providerOrder, scope.roles, scope.mruOrder),
102
+ });
103
+ rankerProviderOrder = providerOrder;
104
104
  }
105
- return rankModelItems(query, scope.items, { roles: scope.roles, mruOrder: scope.mruOrder, affinity });
105
+ return ranker.rank(query);
106
106
  };
107
107
  }
@@ -6,17 +6,17 @@ Start your prompt with `$ ` to run Python — `$$ ` runs it without adding the o
6
6
  You can /btw to ask a side question
7
7
  Use /tan to fork the current conversation into a background agent
8
8
  With default keybindings, {key:ctrl+d} deletes the character under the cursor instead of quitting your draft — see /hotkeys for your exit key
9
- Find out which model you emotionally abuse the most with `zeta stats`
9
+ Find out which model you emotionally abuse the most with `zetacode stats`
10
10
  Try task isolation to create CoW worktrees
11
- Need a cheap nested model call? Use `completion(x...)`. Have a big batch of tasks? Ask a subagent to use it!
11
+ Need a cheap nested model call? Use `completion(x...)`
12
12
  Spaghetti code? Try complaining with /omfg
13
- Did you know? Each kitty/tmux/cmux/zellij/wezterm split keeps its own session — `zeta -c` resumes the right one
13
+ Did you know? Each kitty/tmux/cmux/zellij/wezterm split keeps its own session — `zetacode -c` resumes the right one
14
14
  Drop the word `ultrathink` in your message for harder multi-step reasoning — watch it glow rainbow as you type
15
15
  Say `orchestrate` in your message to drive a multi-phase task with parallel subagents — watch it glow as you type
16
16
  Say `workflowz` in your message to drive the task with parallel subagents in eval — watch it glow as you type
17
17
  Bulk data to classify or triage? Say `jevify` in your message — the eval kernel's `judge()` processes the batch and you read only what it flags — watch it glow as you type
18
18
  Log in to several accounts of the same provider — `/login` again — and zeta load-balances across them automatically
19
- Run `zeta auth-broker serve` once and every machine pulls live tokens over the wire — refresh keys never leave the host; `zeta auth-gateway` fronts it as a drop-in proxy any OpenAI-compatible client can hit
19
+ Run `zetacode auth-broker serve` once and every machine pulls live tokens over the wire — refresh keys never leave the host; `zetacode auth-gateway` fronts it as a drop-in proxy any OpenAI-compatible client can hit
20
20
  Press {key:alt+p} (or /switch) to switch provider, and {key:ctrl+p} to cycle role models smol -> slow -> etc
21
21
  Press {key:ctrl+r} to search your prompt history and reuse a past message
22
22
  `/force read` pins the next turn to one specific tool when the model keeps reaching for the wrong one
@@ -29,4 +29,5 @@ Turn on `/advisor` to attach a second model that reviews every turn and quietly
29
29
  Try starting your prompt with a ->, and writing a list (1. Do X, 2. Do Y)
30
30
  Press {key:shift+tab} to cycle through reasoning effort levels
31
31
  zeta can reconfigure and extend itself — ask any questions you have!
32
- Lint/type errors piling up? `zeta cleanse` (or /cleanse right here) hunts project diagnostics and fixes them with parallel subagents — {key:escape} cancels
32
+ Lint/type errors piling up? `zetacode cleanse` (or /cleanse right here) hunts project diagnostics and fixes them with parallel subagents — {key:escape} cancels
33
+ [NO-TERN] Psst, zeta has a natural habitat 🐦 [tern](https://scl.so/tern)
@@ -6,17 +6,23 @@ import { getKeybindings, type Keybinding } from "../keybindings";
6
6
  import { card, col, kbd, keyed, node, row, span, text } from "../native/describe";
7
7
  import type { DescribeContext, NativeChild, NativeNode, NativeUiEvent } from "../native/node";
8
8
  import { runTranscriptAction } from "../chat/transcript-actions";
9
- import { plainLine } from "../native/spans";
9
+ import { compactText, plainLine, plainText } from "../native/spans";
10
+
10
11
  import { isNativeRendering } from "../native/state";
12
+ import { isHyperlinkEnabled, urlHyperlink, urlLinkSpan } from "../render/hyperlink";
11
13
  import { TERMINAL } from "../terminal-capabilities";
12
14
  import { theme } from "../theme/theme";
13
15
  import type { Component } from "../tui";
14
16
  import { padding, replaceTabs, truncateToWidth, visibleWidth, wrapTextWithAnsi } from "../utils";
15
17
  import tipsText from "./tips.txt" with { type: "text" };
16
18
 
17
- /** Tips embedded at build time, one per line; blanks dropped. Key placeholders
19
+ /** Leading marker for a tip that pitches Tern: shown only while no TSP surface
20
+ * is live, since a live one means the user is already in Tern. */
21
+ const NO_TERN_TIP_MARKER = /^\[NO-TERN\]\s*/;
22
+
23
+ /** tips.txt lines embedded at build time; blanks dropped. Key placeholders
18
24
  * (see {@link expandTipKeys}) stay raw until render time. */
19
- const TIPS: readonly string[] = tipsText
25
+ const TIP_LINES: readonly string[] = tipsText
20
26
  .split("\n")
21
27
  .map(line => line.trim())
22
28
  .filter(line => line.length > 0);
@@ -33,6 +39,12 @@ export const WELCOME_SESSION_SLOTS = 4;
33
39
  */
34
40
  export const WELCOME_LSP_SLOTS = 4;
35
41
 
42
+ /** Tips for the classic renderer: every tip, {@link NO_TERN_TIP_MARKER} stripped. */
43
+ const TIPS: readonly string[] = TIP_LINES.map(line => line.replace(NO_TERN_TIP_MARKER, ""));
44
+
45
+ /** Tips for a live TSP surface: those marked {@link NO_TERN_TIP_MARKER} dropped. */
46
+ const TSP_TIPS: readonly string[] = TIP_LINES.filter(line => !NO_TERN_TIP_MARKER.test(line));
47
+
36
48
  /** Trailing marker that flags a tip as a "what's new" callout. Stripped before
37
49
  * wrapping (with any preceding whitespace) and replaced by {@link NEW_TAG_TEXT}
38
50
  * painted as a shimmering rainbow. Non-global so `.test` stays stateless. */
@@ -120,14 +132,44 @@ function expandTipKeys(tip: string): string {
120
132
  });
121
133
  }
122
134
 
123
- export function renderWelcomeTip(tip: string, boxWidth: number, phase = 0): string[] {
135
+ /** Markdown-style links in tips.txt: `[Tern](https://scl.so/tern)` shows the label, linked to the URL. */
136
+ const TIP_LINK = /\[([^\]]+)\]\(([^\s)]+)\)/g;
137
+
138
+ /** Tip links as underlined OSC 8 labels, or `label (url)` where the terminal cannot link. */
139
+ function linkTipText(tip: string): string {
140
+ const linkable = isHyperlinkEnabled();
141
+ return tip.replace(TIP_LINK, (_link, label: string, url: string) =>
142
+ linkable ? urlHyperlink(url, theme.underline(label)) : `${label} (${url})`,
143
+ );
144
+ }
145
+
146
+ /** The tip as native text: escapes dropped, links as `href` spans. */
147
+ function tipSpans(tip: string): TspSpan[] {
148
+ const spans: TspSpan[] = [];
149
+ let cursor = 0;
150
+ for (const match of tip.matchAll(TIP_LINK)) {
151
+ const [link, label, url] = match;
152
+ if (match.index > cursor) spans.push(span(plainText(tip.slice(cursor, match.index))));
153
+ spans.push(urlLinkSpan(url, label));
154
+ cursor = match.index + link.length;
155
+ }
156
+ if (cursor < tip.length) spans.push(span(plainText(tip.slice(cursor))));
157
+ return spans;
158
+ }
159
+
160
+ /**
161
+ * The welcome tip as lines of at most `width` columns: `Tip:` and the body
162
+ * wrapped together, with no indent, so the banner can center each line.
163
+ * `[]` when `width` leaves no room for a useful line.
164
+ */
165
+ export function renderWelcomeTip(tip: string, width: number, phase = 0): string[] {
124
166
  const label = "Tip: ";
125
167
  const labelWidth = visibleWidth(label);
126
- const bodyBudget = boxWidth - labelWidth;
168
+ const bodyBudget = width - labelWidth;
127
169
  if (bodyBudget < 8) return [];
128
170
 
129
171
  const isNew = NEW_TIP_MARKER.test(tip);
130
- const body = expandTipKeys(isNew ? tip.replace(NEW_TIP_MARKER, "") : tip);
172
+ const body = linkTipText(expandTipKeys(isNew ? tip.replace(NEW_TIP_MARKER, "") : tip));
131
173
 
132
174
  const wrappedBody = wrapTextWithAnsi(replaceTabs(body), bodyBudget);
133
175
  if (wrappedBody.length === 0) return [];
@@ -152,7 +194,7 @@ export function renderWelcomeTip(tip: string, boxWidth: number, phase = 0): stri
152
194
  const tag = renderNewTag(phase, encoding);
153
195
  const tagWidth = 1 + visibleWidth(NEW_TAG_TEXT); // 1 = space separator
154
196
  const lastLine = lines[lines.length - 1];
155
- if (lastLine !== undefined && visibleWidth(lastLine) + tagWidth <= boxWidth) {
197
+ if (lastLine !== undefined && visibleWidth(lastLine) + tagWidth <= width) {
156
198
  lines[lines.length - 1] = `${lastLine} ${tag}`;
157
199
  } else {
158
200
  lines.push(continuationIndent + tag);
@@ -183,9 +225,10 @@ export class WelcomeComponent implements Component {
183
225
  #animTimer: Timer | null = null;
184
226
  #requestRender: (() => void) | null = null;
185
227
  // Tip randomness is latched once so the tip is stable across renders, but
186
- // the nerdfont-nag gate re-reads the live preset: the startup prepaint can
187
- // run under the default "unicode" preset before settings resolve the real
188
- // one, and a memoized nag would survive the switch to "nerd".
228
+ // the nerdfont-nag gate re-reads the live preset and the pool re-reads the
229
+ // live TSP state: the startup prepaint can run under the default "unicode"
230
+ // preset before settings resolve the real one, and an optimistic Tern start
231
+ // falls back to the classic renderer when `hello` never confirms.
189
232
  #nagRoll: number | undefined;
190
233
  #tipRoll: number | undefined;
191
234
  // Render cache: the welcome box is the first transcript-area component, so
@@ -209,7 +252,7 @@ export class WelcomeComponent implements Component {
209
252
  if (theme.getSymbolPreset() === "unicode" && this.#nagRoll < 0.1) {
210
253
  return "Please use nerdfont 😭.";
211
254
  }
212
- return pickWeightedTip(TIPS, this.#tipRoll) || undefined;
255
+ return pickWeightedTip(isNativeRendering() ? TSP_TIPS : TIPS, this.#tipRoll) || undefined;
213
256
  }
214
257
 
215
258
  invalidate(): void {
@@ -338,7 +381,7 @@ export class WelcomeComponent implements Component {
338
381
  ];
339
382
  if (tip) {
340
383
  const isNew = NEW_TIP_MARKER.test(tip);
341
- const tipText = plainLine(expandTipKeys(isNew ? tip.replace(NEW_TIP_MARKER, "") : tip));
384
+ const tipText = compactText(tipSpans(expandTipKeys(isNew ? tip.replace(NEW_TIP_MARKER, "") : tip)));
342
385
  const tipRow: NativeChild[] = [
343
386
  node("icon", { name: "lightbulb", role: "zeta.welcome.tip-icon" }),
344
387
  text(tipText, { wrap: "word", role: "zeta.welcome.tip-text" }),
@@ -56,9 +56,20 @@ interface WordQuery extends WordCompletionQuery {
56
56
 
57
57
  /** Editor text before the word starting at `start`, truncated to its last {@link BEFORE_LIMIT} code units. */
58
58
  function textBefore(lines: readonly string[], cursorLine: number, start: number): string {
59
- let text = (lines[cursorLine] ?? "").slice(0, start);
60
- for (let line = cursorLine - 1; line >= 0 && text.length < BEFORE_LIMIT; line--) {
61
- text = `${lines[line] ?? ""}\n${text}`;
59
+ const head = (lines[cursorLine] ?? "").slice(0, start);
60
+ // Find the first line the capped tail reaches, then join once.
61
+ let length = head.length;
62
+ let first = cursorLine;
63
+ while (first > 0 && length < BEFORE_LIMIT) {
64
+ first--;
65
+ length += (lines[first] ?? "").length + 1;
66
+ }
67
+ let text = head;
68
+ if (first < cursorLine) {
69
+ const parts: string[] = [];
70
+ for (let line = first; line < cursorLine; line++) parts.push(lines[line] ?? "");
71
+ parts.push(head);
72
+ text = parts.join("\n");
62
73
  }
63
74
  if (text.length <= BEFORE_LIMIT) return text;
64
75
  let cut = text.length - BEFORE_LIMIT;
@@ -111,6 +122,16 @@ export class WordCompletionProvider implements EditorTextAssistProvider {
111
122
  #lastShown: { before: string; word: string } | undefined;
112
123
  /** What the last `getWordCompletion` returned, so a fetch repaints only when it changes that. */
113
124
  #displayed: { key: string; suffix: string | null } | undefined;
125
+ /** Last {@link #query} input snapshot (lines are copied: the editor mutates its array in place). */
126
+ #queryMemo:
127
+ | {
128
+ method: WordCompletionMethod;
129
+ lines: readonly string[];
130
+ cursorLine: number;
131
+ cursorCol: number;
132
+ query: WordQuery | undefined;
133
+ }
134
+ | undefined;
114
135
 
115
136
  /** Invoked when an asynchronous answer changes the rendered ghost text. */
116
137
  onUpdate: (() => void) | undefined;
@@ -171,11 +192,23 @@ export class WordCompletionProvider implements EditorTextAssistProvider {
171
192
 
172
193
  #query(lines: readonly string[], cursorLine: number, cursorCol: number): WordQuery | undefined {
173
194
  if (this.#method === "off") return undefined;
195
+ // Rendering asks every frame; reuse the answer until the buffer, caret, or engine changes.
196
+ const memo = this.#queryMemo;
197
+ if (
198
+ memo?.method === this.#method &&
199
+ memo.cursorLine === cursorLine &&
200
+ memo.cursorCol === cursorCol &&
201
+ memo.lines.length === lines.length &&
202
+ memo.lines.every((line, index) => line === lines[index])
203
+ ) {
204
+ return memo.query;
205
+ }
174
206
  const query = wordCompletionQuery(lines, cursorLine, cursorCol, this.#prose);
175
- if (!query) return undefined;
176
207
  // The method keeps a request still in flight for the previous engine from
177
208
  // shadowing the same word state on the new one.
178
- return { key: `${this.#method}\u0000${query.prefix}\u0000${query.before}`, ...query };
209
+ const result = query && { key: `${this.#method}\u0000${query.prefix}\u0000${query.before}`, ...query };
210
+ this.#queryMemo = { method: this.#method, lines: lines.slice(), cursorLine, cursorCol, query: result };
211
+ return result;
179
212
  }
180
213
 
181
214
  #project(query: WordQuery): string | null {
@@ -0,0 +1,2 @@
1
+ /** Splice overlay content into a base line at a specific column. Single-pass optimized. */
2
+ export declare function compositeLineAt(baseLine: string, overlayLine: string, startCol: number, overlayWidth: number, totalWidth: number): string;
@@ -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
  /**
@@ -1128,6 +1128,9 @@ export function createCachedComponent(
1128
1128
  invalidate() {
1129
1129
  cached = undefined;
1130
1130
  },
1131
+ releaseRenderCaches() {
1132
+ cached = undefined;
1133
+ },
1131
1134
  };
1132
1135
  }
1133
1136
 
@@ -1160,6 +1163,14 @@ export function invalidateRenderedStringCache(cache: RenderedStringCache): void
1160
1163
  cache.theme = null;
1161
1164
  }
1162
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
+
1163
1174
  /** Reuse a rendered string while its theme, expansion, content, and salt match. */
1164
1175
  export function cachedRenderedString(
1165
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