@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
@@ -282,6 +282,10 @@ export function renderReadUrlResult(
282
282
 
283
283
  let lastExpanded: boolean | undefined;
284
284
  let contentPreviewLines: string[] | undefined;
285
+ const dropPreview = () => {
286
+ lastExpanded = undefined;
287
+ contentPreviewLines = undefined;
288
+ };
285
289
  return framedToolCard(
286
290
  uiTheme,
287
291
  () => {
@@ -315,11 +319,6 @@ export function renderReadUrlResult(
315
319
  applyBg: false,
316
320
  };
317
321
  },
318
- {
319
- onInvalidate: () => {
320
- lastExpanded = undefined;
321
- contentPreviewLines = undefined;
322
- },
323
- },
322
+ { onInvalidate: dropPreview, onReleaseRenderCaches: dropPreview },
324
323
  );
325
324
  }
@@ -12,6 +12,10 @@ export interface GlobToolDetails {
12
12
  fileCount?: number;
13
13
  files?: string[];
14
14
  truncated?: boolean;
15
+ /** The scan hit the tool deadline, so the listed files are an incomplete
16
+ * set rather than a complete answer. Distinct from `truncated`, which also
17
+ * covers result-limit and output truncation. */
18
+ timedOut?: boolean;
15
19
  error?: string;
16
20
  /** Working directory at search time. Used by the renderer to resolve relative
17
21
  * file paths to absolute paths for OSC 8 hyperlinks. */
package/src/tools/glob.ts CHANGED
@@ -18,7 +18,6 @@ import {
18
18
  PREVIEW_LIMITS,
19
19
  } from "../render/render-utils";
20
20
  import { formatFullOutputReference } from "./output-meta";
21
- import { tuiText, tuiTextFmt } from "../i18n";
22
21
 
23
22
  /** Display metadata for glob tool results. */
24
23
  export interface GlobToolDetails {
@@ -30,6 +29,10 @@ export interface GlobToolDetails {
30
29
  fileCount?: number;
31
30
  files?: string[];
32
31
  truncated?: boolean;
32
+ /** The scan hit the tool deadline, so the listed files are an incomplete
33
+ * set rather than a complete answer. Distinct from `truncated`, which also
34
+ * covers result-limit and output truncation. */
35
+ timedOut?: boolean;
33
36
  error?: string;
34
37
  /** Working directory at search time. Used by the renderer to resolve relative
35
38
  * file paths to absolute paths for OSC 8 hyperlinks. */
@@ -134,10 +137,7 @@ export const globToolRenderer = {
134
137
  const details = result.details;
135
138
 
136
139
  if (result.isError || details?.error) {
137
- const errorText =
138
- details?.error ||
139
- result.content?.find(c => c.type === "text")?.text ||
140
- tuiText("glUnknownError", "Unknown error");
140
+ const errorText = details?.error || result.content?.find(c => c.type === "text")?.text || "Unknown error";
141
141
  return new Text(formatErrorMessage(errorText, uiTheme), 1, 0);
142
142
  }
143
143
 
@@ -151,7 +151,7 @@ export const globToolRenderer = {
151
151
  textContent.includes("No files found") ||
152
152
  textContent.trim() === ""
153
153
  ) {
154
- return new Text(formatEmptyMessage(tuiText("globNoFiles", "No files found"), uiTheme), 1, 0);
154
+ return new Text(formatEmptyMessage("No files found", uiTheme), 1, 0);
155
155
  }
156
156
 
157
157
  const lines = textContent.split("\n").filter(l => l.trim());
@@ -161,10 +161,7 @@ export const globToolRenderer = {
161
161
  title: "Glob",
162
162
  titleColor: "toolTitle",
163
163
  description: formatGlobRenderPaths(args),
164
- meta:
165
- lines.length === 1
166
- ? [tuiText("globFileOne", "1 file")]
167
- : [tuiTextFmt("globFileManyFmt", "%d files", lines.length)],
164
+ meta: [formatCount("file", lines.length)],
168
165
  },
169
166
  uiTheme,
170
167
  );
@@ -191,36 +188,30 @@ export const globToolRenderer = {
191
188
  const truncation = details?.truncation ?? details?.meta?.truncation;
192
189
  const limits = details?.meta?.limits;
193
190
  const truncated = Boolean(details?.truncated || truncation || details?.resultLimitReached || limits?.resultLimit);
191
+ // A non-empty timed-out result is the case `truncated` alone cannot
192
+ // explain: the listing is short because the scan died, not because it
193
+ // finished. Say which one it was.
194
+ const timedOut = Boolean(details?.timedOut);
194
195
  const files = details?.files ?? [];
195
196
 
196
197
  const missingPaths = details?.missingPaths ?? [];
197
198
  const missingNote =
198
- missingPaths.length > 0
199
- ? uiTheme.fg(
200
- "warning",
201
- tuiTextFmt(
202
- "gpSkippedMissingFmt",
203
- `skipped missing: ${missingPaths.join(", ")}`,
204
- missingPaths.join(", "),
205
- ),
206
- )
207
- : undefined;
199
+ missingPaths.length > 0 ? uiTheme.fg("warning", `skipped missing: ${missingPaths.join(", ")}`) : undefined;
208
200
 
209
201
  if (fileCount === 0) {
210
202
  // `truncated` on an empty result means the scan timed out mid-walk —
211
- // render "incomplete", not a definitive "No files found".
212
- const emptyLabel = truncated
213
- ? tuiText("globNoMatchesBeforeTimeout", "No matches before timeout (scan incomplete)")
214
- : tuiText("globNoFiles", "No files found");
203
+ // render "incomplete", not a definitive "No files found". `timedOut`
204
+ // states it outright; the inference stays for old transcripts that
205
+ // predate the field.
206
+ const emptyTimedOut = timedOut || truncated;
207
+ const emptyLabel = emptyTimedOut ? "No matches before timeout (scan incomplete)" : "No files found";
215
208
  const header = renderStatusLine(
216
209
  {
217
210
  icon: "warning",
218
211
  title: "Glob",
219
212
  titleColor: "toolTitle",
220
213
  description: formatGlobRenderPaths(args),
221
- meta: truncated
222
- ? [tuiText("globZeroFiles", "0 files"), uiTheme.fg("warning", tuiText("globTimedOut", "timed out"))]
223
- : [tuiText("globZeroFiles", "0 files")],
214
+ meta: emptyTimedOut ? ["0 files", uiTheme.fg("warning", "timed out")] : ["0 files"],
224
215
  },
225
216
  uiTheme,
226
217
  );
@@ -228,10 +219,9 @@ export const globToolRenderer = {
228
219
  if (missingNote) lines.push(missingNote);
229
220
  return new Text(lines.join("\n"), 1, 0);
230
221
  }
231
- const meta: string[] =
232
- fileCount === 1 ? [tuiText("globFileOne", "1 file")] : [tuiTextFmt("globFileManyFmt", "%d files", fileCount)];
233
- if (details?.scopePath) meta.push(tuiTextFmt("gpInLabelFmt", `in ${details.scopePath}`, details.scopePath));
234
- if (truncated) meta.push(uiTheme.fg("warning", tuiText("globTruncated", "truncated")));
222
+ const meta: string[] = [formatCount("file", fileCount)];
223
+ if (details?.scopePath) meta.push(`in ${details.scopePath}`);
224
+ if (truncated) meta.push(uiTheme.fg("warning", timedOut ? "timed out" : "truncated"));
235
225
  const header = renderStatusLine(
236
226
  {
237
227
  ...(truncated ? { icon: "warning" as const } : { iconOverride: globStatusIcon(uiTheme) }),
@@ -244,43 +234,15 @@ export const globToolRenderer = {
244
234
  );
245
235
 
246
236
  const truncationReasons: string[] = [];
247
- if (details?.resultLimitReached)
248
- truncationReasons.push(
249
- tuiTextFmt(
250
- "globLimitResultsFmt",
251
- `limit ${details.resultLimitReached} results`,
252
- details.resultLimitReached,
253
- ),
254
- );
255
- if (limits?.resultLimit)
256
- truncationReasons.push(
257
- tuiTextFmt(
258
- "globLimitResultsFmt",
259
- `limit ${limits.resultLimit.reached} results`,
260
- limits.resultLimit.reached,
261
- ),
262
- );
263
- if (truncation)
264
- truncationReasons.push(
265
- truncation.truncatedBy === "lines"
266
- ? tuiText("globLineLimit", "line limit")
267
- : tuiText("globSizeLimit", "size limit"),
268
- );
237
+ if (details?.resultLimitReached) truncationReasons.push(`limit ${details.resultLimitReached} results`);
238
+ if (limits?.resultLimit) truncationReasons.push(`limit ${limits.resultLimit.reached} results`);
239
+ if (truncation) truncationReasons.push(truncation.truncatedBy === "lines" ? "line limit" : "size limit");
269
240
  const artifactId = truncation && "artifactId" in truncation ? truncation.artifactId : undefined;
270
241
  if (artifactId) truncationReasons.push(formatFullOutputReference(artifactId));
271
242
 
272
243
  const extraLines: string[] = [];
273
244
  if (truncationReasons.length > 0) {
274
- extraLines.push(
275
- uiTheme.fg(
276
- "warning",
277
- tuiTextFmt(
278
- "globTruncatedReasonsFmt",
279
- `truncated: ${truncationReasons.join(", ")}`,
280
- truncationReasons.join(", "),
281
- ),
282
- ),
283
- );
245
+ extraLines.push(uiTheme.fg("warning", `truncated: ${truncationReasons.join(", ")}`));
284
246
  }
285
247
  if (missingNote) extraLines.push(missingNote);
286
248
 
@@ -343,6 +305,7 @@ export const globToolRenderer = {
343
305
  const truncation = details.truncation ?? details.meta?.truncation;
344
306
  const limits = details.meta?.limits;
345
307
  const truncated = Boolean(details.truncated || truncation || details.resultLimitReached || limits?.resultLimit);
308
+ const timedOut = Boolean(details.timedOut);
346
309
  const missingPaths = details.missingPaths ?? [];
347
310
  const missingNote = missingPaths.length > 0 ? `skipped missing: ${missingPaths.join(", ")}` : undefined;
348
311
  const scope = details.scopePath ? `in ${details.scopePath}` : undefined;
@@ -350,7 +313,10 @@ export const globToolRenderer = {
350
313
  const foot = footnoteText(compact([missingNote]));
351
314
  return {
352
315
  // `truncated` on an empty result means the scan timed out mid-walk.
353
- tool: { ...globNativeHead(args, compact(["0 files", scope])), note: truncated ? "timed out" : undefined },
316
+ tool: {
317
+ ...globNativeHead(args, compact(["0 files", scope])),
318
+ note: timedOut || truncated ? "timed out" : undefined,
319
+ },
354
320
  tone: "warning",
355
321
  inline: true,
356
322
  body: foot ? [foot] : undefined,
@@ -364,7 +330,9 @@ export const globToolRenderer = {
364
330
  if (artifactId) reasons.push(formatFullOutputReference(artifactId));
365
331
  const head = globNativeHead(args, compact([formatCount("file", details.fileCount), scope]));
366
332
  return {
367
- tool: truncated ? { ...head, badges: [{ text: "truncated", tone: "warning" }] } : head,
333
+ tool: truncated
334
+ ? { ...head, badges: [{ text: timedOut ? "timed out" : "truncated", tone: "warning" }] }
335
+ : head,
368
336
  inline: true,
369
337
  body: compact<NativeChild>([
370
338
  describeGlobFiles(details.files ?? [], details.cwd),
package/src/tools/grep.ts CHANGED
@@ -112,11 +112,6 @@ function parseSearchDisplayLineNumber(line: string): number | undefined {
112
112
 
113
113
  const SEARCH_MATCH_LINE_RE = /^\s*\*\d+(?:│|[:|])/;
114
114
 
115
- interface RenderedSearchLine {
116
- raw: string;
117
- styled: string;
118
- }
119
-
120
115
  function isSearchMatchLine(line: string): boolean {
121
116
  return SEARCH_MATCH_LINE_RE.test(line);
122
117
  }
@@ -127,61 +122,91 @@ function isSearchHeaderLine(line: string): boolean {
127
122
 
128
123
  const URL_HEADER_PREFIX_RE = /^#+\s+/;
129
124
 
130
- function renderSearchDisplayLines(
125
+ /**
126
+ * Build a memoized per-line styler for search display output. Classification
127
+ * and URL-target tracking walk the whole output once (nested directory stacks
128
+ * span blank-line groups), but styling and hyperlinking run only for the rows
129
+ * a preview actually shows.
130
+ */
131
+ function createSearchLineStyler(
131
132
  lines: readonly string[],
132
133
  headerBase: string | undefined,
133
134
  fileScope: string | undefined,
134
135
  uiTheme: Theme,
135
136
  displayTargets: Record<string, string> | undefined,
136
- ): RenderedSearchLine[] {
137
+ ): (index: number) => string {
137
138
  const contexts = classifyGroupedLines(lines, headerBase, fileScope);
138
139
  // `classifyGroupedLines` can't resolve internal URLs (TUI-only), so track the
139
140
  // resolved URL target here and use it for the body lines that follow.
141
+ const urlRaw: (string | undefined)[] = Array.from({ length: lines.length }, () => undefined);
142
+ const urlFiles: (string | undefined)[] = Array.from({ length: lines.length }, () => undefined);
140
143
  let urlFile: string | undefined;
141
- return lines.map((line, index) => {
144
+ for (let index = 0; index < lines.length; index++) {
142
145
  const ctx = contexts[index]!;
143
146
  if (ctx.kind === "dir") {
144
147
  urlFile = undefined;
145
- const styled = uiTheme.fg("accent", line);
146
- return { raw: line, styled: ctx.headerPath ? fileHyperlink(ctx.headerPath, styled) : styled };
147
- }
148
- if (ctx.kind === "file") {
148
+ } else if (ctx.kind === "file") {
149
149
  if (ctx.isUrl) {
150
- const raw = line
151
- .replace(URL_HEADER_PREFIX_RE, "")
150
+ const raw = lines[index]!.replace(URL_HEADER_PREFIX_RE, "")
152
151
  .trimEnd()
153
152
  .replace(/\s+\([^)]*\)\s*$/, "");
154
- const linked = linkUrlLikeSearchHeader(raw, uiTheme.fg("accent", line), displayTargets?.[raw]);
155
- urlFile = linked.absPath;
156
- return { raw: line, styled: linked.line };
153
+ urlRaw[index] = raw;
154
+ urlFile = displayTargets?.[raw];
155
+ } else {
156
+ urlFile = undefined;
157
157
  }
158
- urlFile = undefined;
159
- // Root-level files keep the bright accent; nested file headers are dimmed.
160
- const styled = uiTheme.fg(ctx.depth === 1 ? "accent" : "dim", line);
161
- return { raw: line, styled: ctx.headerPath ? fileHyperlink(ctx.headerPath, styled) : styled };
158
+ } else {
159
+ urlFiles[index] = urlFile;
162
160
  }
163
- const styled = uiTheme.fg("toolOutput", line);
164
- const lineNumber = parseSearchDisplayLineNumber(line);
165
- const filePath = ctx.filePath ?? urlFile;
166
- return {
167
- raw: line,
168
- styled: filePath && lineNumber !== undefined ? fileHyperlink(filePath, styled, { line: lineNumber }) : styled,
169
- };
170
- });
161
+ }
162
+ const styledLines: (string | undefined)[] = Array.from({ length: lines.length }, () => undefined);
163
+ return index => {
164
+ const cached = styledLines[index];
165
+ if (cached !== undefined) return cached;
166
+ const line = lines[index]!;
167
+ const ctx = contexts[index]!;
168
+ let styled: string;
169
+ if (ctx.kind === "dir") {
170
+ const accent = uiTheme.fg("accent", line);
171
+ styled = ctx.headerPath ? fileHyperlink(ctx.headerPath, accent) : accent;
172
+ } else if (ctx.kind === "file") {
173
+ const raw = urlRaw[index];
174
+ if (raw !== undefined) {
175
+ styled = linkUrlLikeSearchHeader(raw, uiTheme.fg("accent", line), displayTargets?.[raw]).line;
176
+ } else {
177
+ // Root-level files keep the bright accent; nested file headers are dimmed.
178
+ const tinted = uiTheme.fg(ctx.depth === 1 ? "accent" : "dim", line);
179
+ styled = ctx.headerPath ? fileHyperlink(ctx.headerPath, tinted) : tinted;
180
+ }
181
+ } else {
182
+ const tinted = uiTheme.fg("toolOutput", line);
183
+ const lineNumber = parseSearchDisplayLineNumber(line);
184
+ const filePath = ctx.filePath ?? urlFiles[index];
185
+ styled = filePath && lineNumber !== undefined ? fileHyperlink(filePath, tinted, { line: lineNumber }) : tinted;
186
+ }
187
+ styledLines[index] = styled;
188
+ return styled;
189
+ };
171
190
  }
172
191
 
173
- function compactSearchPreviewGroup(group: RenderedSearchLine[]): RenderedSearchLine[] {
174
- const compact = group.filter(line => isSearchHeaderLine(line.raw) || isSearchMatchLine(line.raw));
192
+ function compactSearchPreviewGroup(group: readonly number[], lines: readonly string[]): readonly number[] {
193
+ const compact = group.filter(index => isSearchHeaderLine(lines[index]!) || isSearchMatchLine(lines[index]!));
175
194
  return compact.length > 0 ? compact : group;
176
195
  }
177
196
 
178
- function countPreviewMatches(lines: readonly RenderedSearchLine[], hasMarkedMatches: boolean): number {
179
- if (hasMarkedMatches) return lines.reduce((count, line) => count + (isSearchMatchLine(line.raw) ? 1 : 0), 0);
180
- return lines.reduce((count, line) => count + (!isSearchHeaderLine(line.raw) && line.raw.length > 0 ? 1 : 0), 0);
197
+ function countPreviewMatches(group: readonly number[], lines: readonly string[], hasMarkedMatches: boolean): number {
198
+ let count = 0;
199
+ for (const index of group) {
200
+ const line = lines[index]!;
201
+ if (hasMarkedMatches ? isSearchMatchLine(line) : !isSearchHeaderLine(line) && line.length > 0) count++;
202
+ }
203
+ return count;
181
204
  }
182
205
 
183
206
  function renderBudgetedSearchGroups(
184
- groups: RenderedSearchLine[][],
207
+ groups: readonly (readonly number[])[],
208
+ lines: readonly string[],
209
+ styleLine: (index: number) => string,
185
210
  maxLines: number,
186
211
  matchCount: number,
187
212
  uiTheme: Theme,
@@ -189,7 +214,7 @@ function renderBudgetedSearchGroups(
189
214
  ): string[] {
190
215
  if (maxLines <= 0) return [];
191
216
  const renderedGroups = groups
192
- .map(group => (compact ? compactSearchPreviewGroup(group) : group))
217
+ .map(group => (compact ? compactSearchPreviewGroup(group, lines) : group))
193
218
  .filter(group => group.length > 0);
194
219
  if (renderedGroups.length === 0) return [];
195
220
 
@@ -198,13 +223,13 @@ function renderBudgetedSearchGroups(
198
223
  let totalFallbackMatches = 0;
199
224
  for (const group of renderedGroups) {
200
225
  totalLines += group.length;
201
- totalMarkedMatches += countPreviewMatches(group, true);
202
- totalFallbackMatches += countPreviewMatches(group, false);
226
+ totalMarkedMatches += countPreviewMatches(group, lines, true);
227
+ totalFallbackMatches += countPreviewMatches(group, lines, false);
203
228
  }
204
229
  const hasMarkedMatches = totalMarkedMatches > 0;
205
230
  const needsSummary = totalLines > maxLines;
206
231
  const contentBudget = needsSummary ? Math.max(maxLines - 1, 0) : maxLines;
207
- const visibleGroups: RenderedSearchLine[][] = [];
232
+ const visibleGroups: (readonly number[])[] = [];
208
233
  let visibleLineCount = 0;
209
234
  let visibleMatches = 0;
210
235
  for (const group of renderedGroups) {
@@ -215,30 +240,30 @@ function renderBudgetedSearchGroups(
215
240
  const visibleGroup = group.slice(0, take);
216
241
  visibleGroups.push(visibleGroup);
217
242
  visibleLineCount += visibleGroup.length;
218
- visibleMatches += countPreviewMatches(visibleGroup, hasMarkedMatches);
243
+ visibleMatches += countPreviewMatches(visibleGroup, lines, hasMarkedMatches);
219
244
  }
220
245
 
221
246
  const totalMatches = hasMarkedMatches ? totalMarkedMatches : Math.max(matchCount, totalFallbackMatches);
222
247
  const hiddenMatches = Math.max(totalMatches - visibleMatches, 0);
223
248
  const hiddenLines = Math.max(totalLines - visibleLineCount, 0);
224
249
  const hasSummary = needsSummary && (hiddenMatches > 0 || hiddenLines > 0);
225
- const lines: string[] = [];
250
+ const out: string[] = [];
226
251
  for (let i = 0; i < visibleGroups.length; i++) {
227
252
  const group = visibleGroups[i]!;
228
253
  const isLast = !hasSummary && i === visibleGroups.length - 1;
229
254
  const prefix = `${uiTheme.fg("dim", getTreeBranch(isLast, uiTheme))} `;
230
255
  const continuePrefix = uiTheme.fg("dim", getTreeContinuePrefix(isLast, uiTheme));
231
- lines.push(`${prefix}${replaceTabs(group[0]!.styled)}`);
256
+ out.push(`${prefix}${replaceTabs(styleLine(group[0]!))}`);
232
257
  for (let j = 1; j < group.length; j++) {
233
- lines.push(`${continuePrefix}${replaceTabs(group[j]!.styled)}`);
258
+ out.push(`${continuePrefix}${replaceTabs(styleLine(group[j]!))}`);
234
259
  }
235
260
  }
236
261
  if (hasSummary) {
237
262
  const hiddenLabel =
238
263
  hiddenMatches > 0 ? formatMoreItems(hiddenMatches, "match") : formatMoreItems(hiddenLines, "line");
239
- lines.push(`${uiTheme.fg("dim", uiTheme.tree.last)} ${uiTheme.fg("muted", hiddenLabel)}`);
264
+ out.push(`${uiTheme.fg("dim", uiTheme.tree.last)} ${uiTheme.fg("muted", hiddenLabel)}`);
240
265
  }
241
- return lines;
266
+ return out;
242
267
  }
243
268
 
244
269
  function grepStatusIcon(uiTheme: Theme): string {
@@ -382,14 +407,14 @@ export const grepToolRenderer = {
382
407
  // Header/match display paths are cwd-relative, so resolve them against cwd
383
408
  // (falling back to searchPath for legacy results that predate `cwd`); the
384
409
  // scoped file's absolute path seeds body lines in single-file searches.
385
- const renderedLines = renderSearchDisplayLines(
410
+ const styleLine = createSearchLineStyler(
386
411
  allLines,
387
412
  details?.cwd ?? details?.searchPath,
388
413
  details?.searchPath,
389
414
  uiTheme,
390
415
  details?.displayTargets,
391
416
  );
392
- const matchGroups = groupLineIndicesByBlank(allLines).map(indices => indices.map(i => renderedLines[i]!));
417
+ const matchGroups = groupLineIndicesByBlank(allLines);
393
418
 
394
419
  const extraLines: string[] = [];
395
420
  if (missingNote) extraLines.push(missingNote);
@@ -401,7 +426,15 @@ export const grepToolRenderer = {
401
426
  (options.expanded ? EXPANDED_TEXT_LIMIT : COLLAPSED_TEXT_LIMIT) - extraLines.length,
402
427
  0,
403
428
  );
404
- const matchLines = renderBudgetedSearchGroups(matchGroups, budget, matchCount, uiTheme, !options.expanded);
429
+ const matchLines = renderBudgetedSearchGroups(
430
+ matchGroups,
431
+ allLines,
432
+ styleLine,
433
+ budget,
434
+ matchCount,
435
+ uiTheme,
436
+ !options.expanded,
437
+ );
405
438
  return [header, ...matchLines, ...extraLines].map(l => truncateToWidth(l, width, Ellipsis.Omit));
406
439
  },
407
440
  { paddingX: 1 },
@@ -27,6 +27,7 @@ import { todoToolRenderer } from "./todo";
27
27
  import { createVibeToolRenderer } from "./vibe";
28
28
  import { webSearchToolRenderer } from "./web-search";
29
29
  import { writeToolRenderer } from "./write";
30
+ import { yieldToolRenderer } from "./yield";
30
31
  import { setXdevRendererLookup } from "./xdev";
31
32
 
32
33
  export * from "./renderer";
@@ -67,6 +68,7 @@ export const toolRenderers: Record<string, ToolRenderer> = {
67
68
  vibe_kill: createVibeToolRenderer("kill"),
68
69
  vibe_list: createVibeToolRenderer("list"),
69
70
  write: writeToolRenderer,
71
+ yield: yieldToolRenderer,
70
72
  };
71
73
 
72
74
  // Wire the xd:// render delegation without the xdev module importing this registry.
package/src/tools/lsp.ts CHANGED
@@ -205,38 +205,56 @@ export function renderResult(
205
205
  if (request?.new_name) requestLines.push(theme.fg("dim", `new name: ${request.new_name}`));
206
206
  if (request?.apply !== undefined) requestLines.push(theme.fg("dim", `apply: ${request.apply ? "true" : "false"}`));
207
207
 
208
- return framedToolCard(theme, () => {
209
- // Read mutable state at render time
210
- const { expanded, isPartial, spinnerFrame } = options;
211
-
212
- // Determine label, state, bodyLines based on type + current expanded
213
- let label = "Result";
214
- let state: "success" | "warning" | "error" = "success";
215
- let bodyLines: string[] = [];
216
-
208
+ // The body depends only on the (immutable) text, theme and `expanded`; parse
209
+ // and render it once per expanded state instead of on every spinner frame.
210
+ const bodies: Array<LspResultBody | undefined> = [undefined, undefined];
211
+ const renderBody = (expanded: boolean): LspResultBody => {
217
212
  if (codeBlockMatch) {
218
- label = "Hover";
219
- bodyLines = renderHover(codeBlockMatch, text, lines, expanded, theme);
220
- } else if (errorMatch || warningMatch || hasStatusError) {
221
- label = "Diagnostics";
213
+ return {
214
+ label: "Hover",
215
+ state: "success",
216
+ bodyLines: renderHover(codeBlockMatch, text, lines, expanded, theme),
217
+ };
218
+ }
219
+ if (errorMatch || warningMatch || hasStatusError) {
222
220
  const errorCount = errorMatch ? Number.parseInt(errorMatch[1], 10) : 0;
223
221
  const warnCount = warningMatch ? Number.parseInt(warningMatch[1], 10) : 0;
224
- state = errorCount > 0 ? "error" : warnCount > 0 ? "warning" : "success";
225
- bodyLines = renderDiagnostics(errorMatch, warningMatch, lines, expanded, theme);
226
- } else if (refMatch) {
227
- label = "References";
228
- bodyLines = renderReferences(refMatch, lines, expanded, theme);
229
- } else if (symbolsMatch) {
230
- label = "Symbols";
231
- bodyLines = renderSymbols(symbolsMatch, lines, expanded, theme);
232
- } else if (result.details?.action === "diagnostics" && text === "OK") {
233
- label = "Diagnostics";
234
- state = "success";
235
- bodyLines = [`${theme.styledSymbol("tool.lsp", "accent")} ${theme.fg("dim", "OK")}`];
236
- } else {
237
- label = "Response";
238
- bodyLines = renderGeneric(text, lines, expanded, theme);
222
+ return {
223
+ label: "Diagnostics",
224
+ state: errorCount > 0 ? "error" : warnCount > 0 ? "warning" : "success",
225
+ bodyLines: renderDiagnostics(errorMatch, warningMatch, lines, expanded, theme),
226
+ };
227
+ }
228
+ if (refMatch) {
229
+ return {
230
+ label: "References",
231
+ state: "success",
232
+ bodyLines: renderReferences(refMatch, lines, expanded, theme),
233
+ };
239
234
  }
235
+ if (symbolsMatch) {
236
+ return { label: "Symbols", state: "success", bodyLines: renderSymbols(symbolsMatch, lines, expanded, theme) };
237
+ }
238
+ if (result.details?.action === "diagnostics" && text === "OK") {
239
+ return {
240
+ label: "Diagnostics",
241
+ state: "success",
242
+ bodyLines: [`${theme.styledSymbol("tool.lsp", "accent")} ${theme.fg("dim", "OK")}`],
243
+ };
244
+ }
245
+ return { label: "Response", state: "success", bodyLines: renderGeneric(text, lines, expanded, theme) };
246
+ };
247
+
248
+ return framedToolCard(theme, () => {
249
+ // Read mutable state at render time
250
+ const { expanded, isPartial, spinnerFrame } = options;
251
+ const slot = expanded ? 1 : 0;
252
+ let body = bodies[slot];
253
+ if (!body) {
254
+ body = renderBody(expanded);
255
+ bodies[slot] = body;
256
+ }
257
+ const { label, state, bodyLines } = body;
240
258
 
241
259
  const actionLabel = (request?.action ?? result.details?.action ?? label.toLowerCase()).replace(/_/g, " ");
242
260
  const isSuccess = !isPartial && !result.isError;
@@ -550,36 +568,32 @@ function renderSymbols(symbolsMatch: RegExpMatchArray, lines: string[], expanded
550
568
  }
551
569
  }
552
570
 
553
- const isLastSibling = (i: number): boolean => {
571
+ // One linear pass each: `isLast[i]` — the next symbol at indent <= mine is
572
+ // not a sibling (monotonic stack, right to left); `prefixes[i]` — per
573
+ // ancestor level, the nearest earlier symbol at that indent decides the rail.
574
+ const isLast = Array.from({ length: symbols.length }, () => false);
575
+ const pending: number[] = [];
576
+ for (let i = symbols.length - 1; i >= 0; i--) {
554
577
  const myIndent = symbols[i].indent;
555
- for (let j = i + 1; j < symbols.length; j++) {
556
- const nextIndent = symbols[j].indent;
557
- if (nextIndent === myIndent) return false;
558
- if (nextIndent < myIndent) return true;
559
- }
560
- return true;
561
- };
562
-
563
- const getPrefix = (i: number): string => {
564
- const myIndent = symbols[i].indent;
565
- if (myIndent === 0) return " ";
566
-
567
- let prefix = " ";
568
- for (let level = 2; level <= myIndent; level += 2) {
569
- let ancestorIdx = -1;
570
- for (let j = i - 1; j >= 0; j--) {
571
- if (symbols[j].indent === level - 2) {
572
- ancestorIdx = j;
573
- break;
574
- }
575
- }
576
- if (ancestorIdx >= 0 && isLastSibling(ancestorIdx)) {
577
- prefix += " ";
578
- } else {
579
- prefix += `${theme.tree.vertical} `;
578
+ while (pending.length > 0 && symbols[pending[pending.length - 1]].indent > myIndent) pending.pop();
579
+ const next = pending.length > 0 ? pending[pending.length - 1] : -1;
580
+ isLast[i] = next < 0 || symbols[next].indent !== myIndent;
581
+ pending.push(i);
582
+ }
583
+ const getPrefixes = (): string[] => {
584
+ const prefixes: string[] = [];
585
+ const lastAtIndent = new Map<number, number>();
586
+ for (let i = 0; i < symbols.length; i++) {
587
+ const myIndent = symbols[i].indent;
588
+ let prefix = " ";
589
+ for (let level = 2; level <= myIndent; level += 2) {
590
+ const ancestorIdx = lastAtIndent.get(level - 2);
591
+ prefix += ancestorIdx !== undefined && isLast[ancestorIdx] ? " " : `${theme.tree.vertical} `;
580
592
  }
593
+ prefixes.push(prefix);
594
+ lastAtIndent.set(myIndent, i);
581
595
  }
582
- return prefix;
596
+ return prefixes;
583
597
  };
584
598
 
585
599
  const topLevelCount = symbols.filter(s => s.indent === 0).length;
@@ -587,12 +601,13 @@ function renderSymbols(symbolsMatch: RegExpMatchArray, lines: string[], expanded
587
601
  if (expanded) {
588
602
  let output = `${icon} ${theme.fg("dim", `in ${fileName}`)}`;
589
603
 
604
+ const prefixes = getPrefixes();
590
605
  for (let i = 0; i < symbols.length; i++) {
591
606
  const sym = symbols[i];
592
- const prefix = getPrefix(i);
593
- const isLast = isLastSibling(i);
594
- const branch = isLast ? theme.tree.last : theme.tree.branch;
595
- const detailPrefix = isLast ? " " : `${theme.tree.vertical} `;
607
+ const prefix = prefixes[i];
608
+ const symIsLast = isLast[i];
609
+ const branch = symIsLast ? theme.tree.last : theme.tree.branch;
610
+ const detailPrefix = symIsLast ? " " : `${theme.tree.vertical} `;
596
611
  output += `\n${prefix}${theme.fg("dim", branch)} ${theme.fg("accent", sym.icon)} ${theme.fg("accent", sym.name)}`;
597
612
  output += `\n${prefix}${theme.fg("dim", detailPrefix)}${theme.fg("muted", `line ${sym.line}`)}`;
598
613
  }
@@ -681,6 +696,13 @@ interface RawDiagnostic {
681
696
  raw: string;
682
697
  }
683
698
 
699
+ /** A rendered LSP result body for one `expanded` state. */
700
+ interface LspResultBody {
701
+ label: string;
702
+ state: "success" | "warning" | "error";
703
+ bodyLines: string[];
704
+ }
705
+
684
706
  type DiagnosticItem = ParsedDiagnostic | RawDiagnostic;
685
707
 
686
708
  function formatDiagnosticMessage(diagnostic: ParsedDiagnostic): string {