@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
@@ -75,4 +75,6 @@ export type TextReviewAnnotation =
75
75
  export interface TextReviewOverlayResult {
76
76
  action: "paste";
77
77
  annotations: TextReviewAnnotation[];
78
+ /** Source text after an external-editor edit; absent when the source was not edited. */
79
+ editedText?: string;
78
80
  }
@@ -212,11 +212,7 @@ function questionTabLabel(question: ExtensionAskDialogQuestion, index: number):
212
212
  }
213
213
 
214
214
  function wrapQuestionTitle(question: ExtensionAskDialogQuestion, width: number): string[] {
215
- const mdTheme = getMarkdownTheme();
216
- const questionText = renderInlineMarkdown(replaceTabs(sanitizeCarriageReturns(question.question)), mdTheme, t =>
217
- theme.fg("text", t),
218
- );
219
- return wrapTextWithAnsi(questionText, Math.max(1, width));
215
+ return renderPreviewContent(sanitizeCarriageReturns(question.question), Math.max(1, width), "text");
220
216
  }
221
217
 
222
218
  function renderQuestionTitle(question: ExtensionAskDialogQuestion, width: number, maxRows = MAX_HEADER_ROWS): string[] {
@@ -299,10 +295,10 @@ function splitPreviewSegments(preview: string): PreviewSegment[] {
299
295
  return segments;
300
296
  }
301
297
 
302
- function renderPreviewContent(preview: string, width: number): string[] {
298
+ function renderPreviewContent(preview: string, width: number, textColor: "muted" | "text" = "muted"): string[] {
303
299
  const out: string[] = [];
304
300
  const mdTheme = getMarkdownTheme();
305
- const accentStyle = { color: (text: string) => theme.fg("muted", text) };
301
+ const accentStyle = { color: (text: string) => theme.fg(textColor, text) };
306
302
  for (const segment of splitPreviewSegments(preview)) {
307
303
  if (segment.kind === "code") {
308
304
  const highlighted = highlightCode(segment.text, segment.language);
@@ -715,8 +711,11 @@ export class AskDialogComponent implements Component {
715
711
  }
716
712
 
717
713
  /**
718
- * A bottom-anchored glass sheet over the composer (the `overlay` hoists into
719
- * the terminal's layer; the dialog's own slot in the dock stays empty).
714
+ * In the dock, in the composer's place and framed as the composer is: a
715
+ * `col` with the prompt editor's root role (`zeta.editor`), so the terminal
716
+ * gives it the composer's insets and spacing. Not a modal `overlay` sheet:
717
+ * one anchored at the bottom covered the transcript rows that explain the
718
+ * question and blocked scrolling until the question was answered.
720
719
  */
721
720
  describe(cx: DescribeContext): NativeNode {
722
721
  const inputGuard = this.options.inputGuard;
@@ -732,13 +731,7 @@ export class AskDialogComponent implements Component {
732
731
  if (this.#isSubmitTab()) this.#describeSubmitBody(children);
733
732
  else this.#describeQuestionBody(children);
734
733
  children.push(this.#describeActions(blocked));
735
- const sheet = node(
736
- "overlay",
737
- { role: "zeta.overlay.ask", anchor: "bottom", size: "md", modal: true },
738
- [col(children, { gap: "md" })],
739
- "sheet",
740
- );
741
- this.#native = col([sheet]);
734
+ this.#native = col(children, { role: "zeta.editor", gap: "md" });
742
735
  this.#nativeBlocked = blocked;
743
736
  return this.#native;
744
737
  }
@@ -1141,7 +1134,7 @@ export class AskDialogComponent implements Component {
1141
1134
  const action = question?.multi
1142
1135
  ? `${formatKeyHint("space")} toggle · ${enter} ${enterAction}`
1143
1136
  : `${enter} select · ${formatKeyHint("n")} note`;
1144
- const tabs = this.#hasSubmitTab() ? ` · ${formatKeyHints(["tab", "left", "right"])}` : "";
1137
+ const tabs = this.#hasSubmitTab() ? ` · ${formatKeyHints(["tab", "left", "right"])} question` : "";
1145
1138
  const expand = this.#expandHint();
1146
1139
  if (this.#questionCanPage && indicator) {
1147
1140
  const pageKeys = editorKeys("tui.select.pageUp", "tui.select.pageDown");
@@ -29,15 +29,15 @@ import type { SpaceHoldHandler } from "../space-hold";
29
29
  import { sanitizeErrorLine } from "../chrome/error-block";
30
30
  import { sanitizeDisplayLine, sanitizeDisplayText } from "./extensions/display-text";
31
31
  import { editorKey, editorKeys, keyHint, rawKeyHint } from "../chrome/keybinding-hints";
32
- import { formatKeyHint } from "../app-keybindings";
32
+ import { formatKeyHint, type KeyId } from "../app-keybindings";
33
33
  import { bottomBorder, row, topBorder } from "../chrome/overlay-box";
34
34
  import { padToWidth } from "../render/utils";
35
35
  import { SplitPane } from "../components/layout/split-pane";
36
36
  import { clampSelection, contentRowWidth, padLinesToHeight, renderScrollableList } from "../chrome/selector-helpers";
37
- import type { TspSpan } from "@linxiraos/pi-wire";
38
- import type { NativeChild, NativeNode, NativeUiEvent } from "../native/node";
37
+ import type { TspScrollBy, TspSpan } from "@linxiraos/pi-wire";
38
+ import type { NativeChild, NativeNode, NativeScroll, NativeUiEvent } from "../native/node";
39
39
  import { col, md, node, span, text } from "../native/describe";
40
- import { actionHint, hintsRow, type NativeHint, statusHintsRow } from "../native/overlay";
40
+ import { actionBar, actionButton, actionHint, hintsRow, type NativeHint, statusHintsRow } from "../native/overlay";
41
41
 
42
42
  interface BtwHistoryPanelOptions {
43
43
  records: readonly BtwHistoryRecord[];
@@ -46,10 +46,17 @@ interface BtwHistoryPanelOptions {
46
46
  onCancel: (record: BtwHistoryRecord) => void;
47
47
  canFollowUp?: (record: BtwHistoryRecord) => boolean;
48
48
  onFollowUp?: (record: BtwHistoryRecord, question: string, signal: AbortSignal) => Promise<boolean>;
49
- /** Space-bar push-to-talk for a follow-up composer: dictates into `input` while Space is held. */
49
+ /** Configured push-to-talk keys for a follow-up composer. */
50
+ spaceHoldKeys?: readonly KeyId[];
50
51
  spaceHold?: (input: Input) => SpaceHoldHandler;
51
52
  requestRender: () => void;
52
53
  getHeight: () => number;
54
+ /**
55
+ * Esc closes the panel even while the selected answer runs (it keeps
56
+ * streaming in the background) and `x` cancels it, instead of Esc cancelling
57
+ * first. For a host with no inline panel to fall back to (Tern).
58
+ */
59
+ escapeHides?: boolean;
53
60
  }
54
61
 
55
62
  interface FollowUpComposer {
@@ -78,6 +85,7 @@ interface BtwHistoryNativeMemo {
78
85
  followUpPending: boolean;
79
86
  canFollowUp: boolean;
80
87
  copied: boolean;
88
+ scroll: NativeScroll | undefined;
81
89
  node: NativeNode;
82
90
  }
83
91
 
@@ -108,6 +116,8 @@ export class BtwHistoryPanel implements Component, Focusable {
108
116
  #lastCopiedId: string | undefined;
109
117
  #lastCopiedText: string | undefined;
110
118
  #native: BtwHistoryNativeMemo | undefined;
119
+ /** The last answer-pane scroll key, forwarded natively (see {@link NativeNode.scroll}). */
120
+ #scroll: NativeScroll | undefined;
111
121
  readonly #detail = new ScrollView([], {
112
122
  height: 1,
113
123
  scrollbar: "auto",
@@ -159,6 +169,7 @@ export class BtwHistoryPanel implements Component, Focusable {
159
169
  this.#options = options;
160
170
  this.#records = options.records;
161
171
  this.#selectedId = options.records[0]?.id;
172
+ if (options.records.length === 1) this.#focus = "answer";
162
173
  }
163
174
 
164
175
  get focused(): boolean {
@@ -187,6 +198,8 @@ export class BtwHistoryPanel implements Component, Focusable {
187
198
  this.#followLatest = false;
188
199
  this.#detail.scrollToTop();
189
200
  }
201
+ // One side question has nothing to pick: its answer keeps the arrow keys.
202
+ if (records.length === 1) this.#focus = "answer";
190
203
  this.#options.requestRender();
191
204
  }
192
205
 
@@ -247,9 +260,30 @@ export class BtwHistoryPanel implements Component, Focusable {
247
260
  return true;
248
261
  }
249
262
 
263
+ /** Select a record with its answer pane focused, read from the top (a just-asked /btw). */
264
+ showRecord(recordId: string): void {
265
+ const index = this.#records.findIndex(record => record.id === recordId);
266
+ if (index === -1) return;
267
+ this.#select(index);
268
+ this.#focus = "answer";
269
+ this.#options.requestRender();
270
+ }
271
+
272
+ /**
273
+ * Jump the answer pane to its bottom once (opening or sending a follow-up):
274
+ * text mode keeps following the newest row at render until a scroll key;
275
+ * natively the sheet body scrolls to its end. Tern reports no wheel scroll,
276
+ * so streamed updates never re-pin it there and a reader is never yanked.
277
+ */
278
+ #jumpToEnd(): void {
279
+ this.#followLatest = true;
280
+ this.#scroll = { by: "end", n: (this.#scroll?.n ?? 0) + 1 };
281
+ }
282
+
250
283
  #openComposer(record: BtwHistoryRecord): void {
251
284
  const input = new Input();
252
285
  input.prompt = theme.fg("accent", "Follow up: ");
286
+ if (this.#options.spaceHoldKeys !== undefined) input.spaceHold.keys = this.#options.spaceHoldKeys;
253
287
  input.spaceHold.handler = this.#options.spaceHold?.(input);
254
288
  const composer: FollowUpComposer = { recordId: record.id, input, abortController: new AbortController() };
255
289
  input.onEscape = () => {
@@ -261,6 +295,8 @@ export class BtwHistoryPanel implements Component, Focusable {
261
295
  };
262
296
  this.#composer = composer;
263
297
  this.#focus = "answer";
298
+ // Following up reads from the newest turn, right above the composer.
299
+ this.#jumpToEnd();
264
300
  this.#options.requestRender();
265
301
  }
266
302
 
@@ -286,7 +322,7 @@ export class BtwHistoryPanel implements Component, Focusable {
286
322
  this.#composer = undefined;
287
323
  this.#selectedId = composer.recordId;
288
324
  this.#focus = "answer";
289
- this.#followLatest = true;
325
+ this.#jumpToEnd();
290
326
  } else {
291
327
  composer.notice = `Follow-up was not started. Your draft is kept; ${editorKey("tui.input.submit")} to retry.`;
292
328
  }
@@ -300,6 +336,11 @@ export class BtwHistoryPanel implements Component, Focusable {
300
336
  }
301
337
  }
302
338
 
339
+ /** Only an active follow-up composer can claim its configured push-to-talk keys. */
340
+ capturesInput(data: string): boolean {
341
+ return this.#composer?.input.capturesInput(data) ?? false;
342
+ }
343
+
303
344
  /** Enhanced clipboard pastes belong only to the active composer. */
304
345
  pasteText(text: string): void {
305
346
  if (!this.#composer) return;
@@ -314,11 +355,17 @@ export class BtwHistoryPanel implements Component, Focusable {
314
355
  return;
315
356
  }
316
357
  const record = this.#selected();
358
+ const running = record !== undefined && getBtwLatestTurn(record).status === "running" ? record : undefined;
359
+ const hides = this.#options.escapeHides === true;
317
360
  if (matchesSelectCancel(data) || matchesKey(data, "escape")) {
318
- if (record && getBtwLatestTurn(record).status === "running") this.#options.onCancel(record);
361
+ if (running && !hides) this.#options.onCancel(running);
319
362
  else this.#options.onClose();
320
363
  return;
321
364
  }
365
+ if (hides && matchesKey(data, "x")) {
366
+ if (running) this.#options.onCancel(running);
367
+ return;
368
+ }
322
369
  if (matchesKey(data, "f") || matchesKey(data, "enter")) {
323
370
  if (record) this.openFollowUp(record.id);
324
371
  return;
@@ -335,11 +382,12 @@ export class BtwHistoryPanel implements Component, Focusable {
335
382
  matchesKey(data, "ctrl+_") ||
336
383
  data === String.fromCharCode(31)
337
384
  ) {
338
- this.#focus = this.#focus === "list" ? "answer" : "list";
385
+ // One side question has nothing to pick: its answer keeps the arrow keys.
386
+ if (this.#records.length > 1) this.#focus = this.#focus === "list" ? "answer" : "list";
339
387
  } else if (matchesKey(data, "right")) {
340
388
  this.#focus = "answer";
341
389
  } else if (matchesKey(data, "left")) {
342
- this.#focus = "list";
390
+ if (this.#records.length > 1) this.#focus = "list";
343
391
  } else if (this.#focus === "list") {
344
392
  const index = this.#selectedIndex();
345
393
  if (matchesSelectUp(data)) this.#select(index - 1);
@@ -350,11 +398,25 @@ export class BtwHistoryPanel implements Component, Focusable {
350
398
  else if (matchesKey(data, "end")) this.#select(this.#records.length - 1);
351
399
  else return;
352
400
  } else {
353
- if (matchesSelectUp(data)) this.#detail.scroll(-1);
354
- else if (matchesSelectDown(data)) this.#detail.scroll(1);
355
- else if (matchesSelectPageUp(data)) this.#detail.page(-1);
356
- else if (matchesSelectPageDown(data)) this.#detail.page(1);
401
+ const by: TspScrollBy | undefined = matchesSelectUp(data)
402
+ ? "line-up"
403
+ : matchesSelectDown(data)
404
+ ? "line-down"
405
+ : matchesSelectPageUp(data)
406
+ ? "page-up"
407
+ : matchesSelectPageDown(data)
408
+ ? "page-down"
409
+ : matchesKey(data, "home")
410
+ ? "start"
411
+ : matchesKey(data, "end")
412
+ ? "end"
413
+ : undefined;
414
+ if (by === "line-up") this.#detail.scroll(-1);
415
+ else if (by === "line-down") this.#detail.scroll(1);
416
+ else if (by === "page-up") this.#detail.page(-1);
417
+ else if (by === "page-down") this.#detail.page(1);
357
418
  else if (!this.#detail.handleScrollKey(data)) return;
419
+ if (by) this.#scroll = { by, n: (this.#scroll?.n ?? 0) + 1 };
358
420
  this.#followLatest = matchesKey(data, "end");
359
421
  }
360
422
  this.#options.requestRender();
@@ -378,31 +440,39 @@ export class BtwHistoryPanel implements Component, Focusable {
378
440
  memo.notice === composer?.notice &&
379
441
  memo.followUpPending === this.#followUpPending &&
380
442
  memo.canFollowUp === canFollowUp &&
381
- memo.copied === copied
443
+ memo.copied === copied &&
444
+ memo.scroll === this.#scroll
382
445
  ) {
383
446
  return memo.node;
384
447
  }
385
- const children: NativeChild[] = [
386
- node(
387
- "row",
388
- { gap: "md", align: "start" },
389
- [
448
+ // Plain columns, not `section`s: Tern draws a section as a foldable `##` heading,
449
+ // and a flex item's min-content width (a long code line in the answer) would
450
+ // crush the list pane. The answer pane may shrink to nothing; the list may not.
451
+ // With one side question the list and the pane labels are noise: the answer alone.
452
+ const showList = this.#records.length > 1;
453
+ const detail = this.#describeDetail(record, copied);
454
+ const answerPane: NativeNode = {
455
+ ...node(
456
+ "col",
457
+ { gap: "sm", grow: 1, basis: 0, min: { w: 0 } },
458
+ showList ? [text(this.#describeFocusLabel("answer"), { truncate: "end", lines: 1 }), detail] : [detail],
459
+ "details",
460
+ ),
461
+ // Natively the sheet body is the scroller; arrow/page keys on the answer pane move it.
462
+ scroll: this.#scroll,
463
+ };
464
+ const panes: NativeChild[] = showList
465
+ ? [
390
466
  node(
391
- "section",
392
- { head: this.#describeFocusLabel("list"), basis: 0.42, max: { w: "46ch" } },
393
- [this.#describeList()],
467
+ "col",
468
+ { gap: "sm", shrink: 0, basis: 0.3, max: { w: "40ch" } },
469
+ [text(this.#describeFocusLabel("list"), { truncate: "end", lines: 1 }), this.#describeList()],
394
470
  "history",
395
471
  ),
396
- node(
397
- "section",
398
- { head: this.#describeFocusLabel("answer"), grow: 1 },
399
- [this.#describeDetail(record, copied)],
400
- "details",
401
- ),
402
- ],
403
- "body",
404
- ),
405
- ];
472
+ answerPane,
473
+ ]
474
+ : [answerPane];
475
+ const children: NativeChild[] = [node("row", { gap: "lg", align: "start" }, panes, "body")];
406
476
  if (composer) {
407
477
  composer.input.focused = this.#focused;
408
478
  const lines: NativeChild[] = [
@@ -415,6 +485,13 @@ export class BtwHistoryPanel implements Component, Focusable {
415
485
  children.push(node("col", undefined, lines, "composer"));
416
486
  }
417
487
  children.push({ ...this.#describeFooter(record, canFollowUp, copied), key: "footer" });
488
+ // Clicking Close always dismisses the sheet; the Esc keycap only shows where Esc does too.
489
+ const running = record !== undefined && getBtwLatestTurn(record).status === "running";
490
+ const escapeCloses = !composer && !(running && !this.#options.escapeHides);
491
+ children.push({
492
+ ...actionBar([null, actionButton("Close", "close", escapeCloses ? { keys: "escape" } : undefined)]),
493
+ key: "actions",
494
+ });
418
495
  // The sheet is the frame (`nativeOverlay`): a borderless column.
419
496
  const described = col(children, { gap: "md" });
420
497
  this.#native = {
@@ -427,12 +504,17 @@ export class BtwHistoryPanel implements Component, Focusable {
427
504
  followUpPending: this.#followUpPending,
428
505
  canFollowUp,
429
506
  copied,
507
+ scroll: this.#scroll,
430
508
  node: described,
431
509
  };
432
510
  return described;
433
511
  }
434
512
 
435
513
  handleNativeEvent(event: NativeUiEvent): void {
514
+ if (event.type === "action" && event.act === "close") {
515
+ this.#options.onClose();
516
+ return;
517
+ }
436
518
  if ((event.type !== "select" && event.type !== "activate") || this.#composer) return;
437
519
  const index = this.#records.findIndex(record => record.id === event.item);
438
520
  if (index === -1) return;
@@ -492,10 +574,21 @@ export class BtwHistoryPanel implements Component, Focusable {
492
574
  const answer = turn.answer.trim()
493
575
  ? md(sanitizeDisplayText(turn.answer), { stream: turn.status === "running" })
494
576
  : text([span(turn.status === "running" ? "Waiting for response…" : "No answer text.", "dim")]);
577
+ // Plain labels, not `section`s (Tern draws those as foldable `##` headings);
578
+ // the question stays markdown so its inline code and links render.
495
579
  const children: NativeChild[] = [
496
- text([span(`${status.label} · ${this.#dateFormat.format(turn.createdAt)}`, status.color)], { wrap: "word" }),
497
- node("section", { head: [span("Question", "accent strong")] }, [md(sanitizeDisplayText(turn.question))]),
498
- node("section", { head: [span("Answer", "accent strong")] }, [answer]),
580
+ text(
581
+ [
582
+ span("Question", "accent strong"),
583
+ span(" · ", "dim"),
584
+ span(status.label, status.color),
585
+ span(` · ${this.#dateFormat.format(turn.createdAt)}`, "dim"),
586
+ ],
587
+ { wrap: "word" },
588
+ ),
589
+ md(sanitizeDisplayText(turn.question)),
590
+ text([span("Answer", "accent strong")]),
591
+ answer,
499
592
  ];
500
593
  if (turn.error) children.push(text([span(sanitizeErrorLine(turn.error), "error")], { wrap: "word" }));
501
594
  if (turn.status === "interrupted") children.push(text([span("Not resumed in this view.", "muted")]));
@@ -510,9 +603,11 @@ export class BtwHistoryPanel implements Component, Focusable {
510
603
  ]);
511
604
  }
512
605
  const latest = record ? getBtwLatestTurn(record) : undefined;
606
+ const running = latest?.status === "running";
513
607
  const hints: (NativeHint | undefined)[] = [
514
- actionHint("tui.select.cancel", latest?.status === "running" ? "cancel" : "close"),
515
- { keys: ["tab", "ctrl+/"], label: "switch pane" },
608
+ actionHint("tui.select.cancel", running && !this.#options.escapeHides ? "cancel" : "close"),
609
+ running && this.#options.escapeHides ? { keys: ["x"], label: "cancel" } : undefined,
610
+ this.#records.length > 1 ? { keys: ["tab", "ctrl+/"], label: "switch pane" } : undefined,
516
611
  actionHint(["tui.select.up", "tui.select.down"], this.#focus === "list" ? "select" : "scroll"),
517
612
  ];
518
613
  if (canFollowUp) hints.push({ keys: ["f", "enter"], label: "follow up" });
@@ -689,14 +784,16 @@ export class BtwHistoryPanel implements Component, Focusable {
689
784
  const record = this.#selected();
690
785
  const composer = this.#composer;
691
786
  const latest = record ? getBtwLatestTurn(record) : undefined;
787
+ const running = latest?.status === "running";
692
788
  const actions = composer
693
789
  ? [
694
790
  keyHint("tui.input.submit", this.#followUpPending ? "starting…" : "send"),
695
791
  keyHint("tui.select.cancel", "cancel"),
696
792
  ]
697
793
  : [
698
- keyHint("tui.select.cancel", latest?.status === "running" ? "cancel" : "close"),
699
- rawKeyHint(["tab", "ctrl+/"], "switch pane"),
794
+ keyHint("tui.select.cancel", running && !this.#options.escapeHides ? "cancel" : "close"),
795
+ ...(running && this.#options.escapeHides ? [rawKeyHint("x", "cancel")] : []),
796
+ ...(this.#records.length > 1 ? [rawKeyHint(["tab", "ctrl+/"], "switch pane")] : []),
700
797
  ];
701
798
  if (!composer) {
702
799
  if (record && this.#canFollowUp(record)) actions.push(rawKeyHint(["f", "enter"], "follow up"));
@@ -31,6 +31,7 @@ export interface CopySelectorDeps {
31
31
  cwd: string;
32
32
  hideThinkingBlock?: () => boolean;
33
33
  proseOnlyThinking?: () => boolean;
34
+ expandThinkingBlocks?: () => boolean;
34
35
  linkTargets?: ReadonlyMap<string, string>;
35
36
  requestRender: () => void;
36
37
  /** Replaces the "Copy" header when the picker is reused for another purpose. */
@@ -274,6 +274,7 @@ export interface CopySelectorDeps {
274
274
  cwd: string;
275
275
  hideThinkingBlock?: () => boolean;
276
276
  proseOnlyThinking?: () => boolean;
277
+ expandThinkingBlocks?: () => boolean;
277
278
  linkTargets?: ReadonlyMap<string, string>;
278
279
  requestRender: () => void;
279
280
  /** Replaces the "Copy" header when the picker is reused for another purpose. */
@@ -326,6 +327,12 @@ interface ControlRegion {
326
327
  end: number;
327
328
  }
328
329
 
330
+ /** A block's preview rows (highlighted, width-cut, plus the "more lines" row) and its full line count. */
331
+ interface BlockPreview {
332
+ rows: string[];
333
+ lineCount: number;
334
+ }
335
+
329
336
  export class CopySelectorComponent implements Component {
330
337
  #builder: ChatTranscriptBuilder;
331
338
  #browser: TranscriptBrowser;
@@ -347,6 +354,8 @@ export class CopySelectorComponent implements Component {
347
354
 
348
355
  /** Last described root and the state it was built from. */
349
356
  #native: { memo: string; targets: OutlineTarget[]; blocks: CopyBlock[] | undefined; node: NativeNode } | undefined;
357
+ /** ANSI block previews (syntax highlight + width cut) of the exploded turn. */
358
+ #blockPreviews: { blocks: CopyBlock[]; inner: number; previews: readonly BlockPreview[] } | undefined;
350
359
  /** Transcript list items, rebuilt only when the replayed targets change. */
351
360
  #nativeItems: { targets: OutlineTarget[]; truncated: boolean; items: NativeNode[] } | undefined;
352
361
  /** Timeline picker items of the replayed targets. */
@@ -379,6 +388,7 @@ export class CopySelectorComponent implements Component {
379
388
  cwd: this.deps.cwd,
380
389
  hideThinkingBlock: this.deps.hideThinkingBlock,
381
390
  proseOnlyThinking: this.deps.proseOnlyThinking,
391
+ expandThinkingBlocks: this.deps.expandThinkingBlocks,
382
392
  linkTargets: this.deps.linkTargets,
383
393
  requestRender: this.deps.requestRender,
384
394
  });
@@ -412,6 +422,7 @@ export class CopySelectorComponent implements Component {
412
422
  }
413
423
 
414
424
  invalidate(): void {
425
+ this.#blockPreviews = undefined;
415
426
  this.#builder.container.invalidate();
416
427
  this.#browser.invalidate();
417
428
  }
@@ -943,18 +954,13 @@ export class CopySelectorComponent implements Component {
943
954
  */
944
955
  #composeBlocks(blocks: CopyBlock[], columnWidth: number, lineOffset: number): ComposedColumn {
945
956
  const inner = Math.max(10, columnWidth - 4);
957
+ const previews = this.#previewBlocks(blocks, inner);
946
958
  const lines: string[] = [];
947
959
  let selStart = -1;
948
960
  let selEnd = -1;
949
961
  for (let index = 0; index < blocks.length; index++) {
950
962
  const block = blocks[index]!;
951
- const raw = block.content.split("\n");
952
- const shown = raw.slice(0, BLOCK_PREVIEW_LINES);
953
- const styled = block.language ? highlightCode(shown.join("\n"), block.language) : shown;
954
- const rows = styled.map(row => truncateToWidth(replaceTabs(row), inner));
955
- if (raw.length > shown.length) {
956
- rows.push(theme.fg("dim", `… +${raw.length - shown.length} more lines`));
957
- }
963
+ const { rows, lineCount } = previews[index]!;
958
964
  const selected = index === this.#blockSelected;
959
965
  const captionColor: ThemeColor = selected ? OUTLINE_COLOR : "dim";
960
966
  const controls: Array<{ action: ControlRegion["action"]; text: string }> = [
@@ -963,7 +969,7 @@ export class CopySelectorComponent implements Component {
963
969
  if (block.href && this.deps.onOpen) controls.push({ action: "open", text: `${theme.cmd.share} open` });
964
970
  const controlsWidth = controls.reduce((sum, control) => sum + visibleWidth(control.text) + 2, 0);
965
971
  const summary = truncateToWidth(
966
- `${index + 1}/${blocks.length}${theme.sep.dot}${block.label}${theme.sep.dot}${raw.length} line${raw.length === 1 ? "" : "s"}`,
972
+ `${index + 1}/${blocks.length}${theme.sep.dot}${block.label}${theme.sep.dot}${lineCount} line${lineCount === 1 ? "" : "s"}`,
967
973
  Math.max(4, inner - controlsWidth),
968
974
  );
969
975
  // Caption: two-space gutter, summary, then the controls, each preceded by two spaces.
@@ -996,6 +1002,24 @@ export class CopySelectorComponent implements Component {
996
1002
  lines.push("");
997
1003
  return { lines, selStart, selEnd };
998
1004
  }
1005
+
1006
+ /** Highlighted, width-cut previews of each block; rebuilt when the block set or width changes. */
1007
+ #previewBlocks(blocks: CopyBlock[], inner: number): readonly BlockPreview[] {
1008
+ const cached = this.#blockPreviews;
1009
+ if (cached?.blocks === blocks && cached.inner === inner) return cached.previews;
1010
+ const previews = blocks.map(block => {
1011
+ const raw = block.content.split("\n");
1012
+ const shown = raw.slice(0, BLOCK_PREVIEW_LINES);
1013
+ const styled = block.language ? highlightCode(shown.join("\n"), block.language) : shown;
1014
+ const rows = styled.map(row => truncateToWidth(replaceTabs(row), inner));
1015
+ if (raw.length > shown.length) {
1016
+ rows.push(theme.fg("dim", `… +${raw.length - shown.length} more lines`));
1017
+ }
1018
+ return { rows, lineCount: raw.length };
1019
+ });
1020
+ this.#blockPreviews = { blocks, inner, previews };
1021
+ return previews;
1022
+ }
999
1023
  }
1000
1024
 
1001
1025
  /** Raw multi-line text of a user message (string or text blocks). */
@@ -25,7 +25,7 @@ import { Memo } from "../../native/memo";
25
25
  import type { DescribeContext, NativeNode, NativeScroll, NativeUiEvent } from "../../native/node";
26
26
  import { actionBar, actionButton } from "../../native/overlay";
27
27
  import { CLOSE_ACTION, type PickerEvent, picker, pickerAction, pickerEvent } from "../../native/picker";
28
- import { matchesKey } from "../../keys";
28
+ import { type KeyId, matchesKey } from "../../keys";
29
29
  import { parseSgrMouse } from "../../mouse";
30
30
  import { SplitPane, type SplitPaneHit } from "../../components/layout/split-pane";
31
31
  import { Stack } from "../../components/layout/stack";
@@ -41,7 +41,7 @@ import {
41
41
  matchesSelectPageUp,
42
42
  } from "../../keybinding-matchers";
43
43
  import { expandKeyHint } from "../../render/render-utils";
44
- import { formatKeyHint, formatKeyHints } from "../../app-keybindings";
44
+ import { formatKeyHint, formatKeyHints, formatTooltipKey } from "../../app-keybindings";
45
45
  import { boundKeys, editorKeys, interruptKey } from "../../chrome/keybinding-hints";
46
46
  import { bottomBorder, divider, PanelRows, row, topBorder } from "../../chrome/overlay-box";
47
47
  import { ExtensionList, type ExtensionListSwitch } from "./extension-list";
@@ -694,7 +694,7 @@ export class ExtensionDashboard implements Component {
694
694
  ];
695
695
  }
696
696
 
697
- #expandKey(): string {
697
+ #expandKey(): KeyId {
698
698
  return boundKeys("app.tools.expand", ["ctrl+o"])[0] ?? "ctrl+o";
699
699
  }
700
700
 
@@ -714,7 +714,7 @@ export class ExtensionDashboard implements Component {
714
714
  node("icon", {
715
715
  name: "x",
716
716
  role: "zeta.app.ibtn",
717
- title: `Close ${interruptKey()}`,
717
+ title: `Close ${formatTooltipKey(boundKeys("app.interrupt", ["escape"])[0] ?? "escape")}`,
718
718
  aria: "Close",
719
719
  actions: { click: "close" },
720
720
  }),
@@ -1,5 +1,5 @@
1
1
  /** Pure dashboard tree, filtering, and selection state. */
2
- import { fuzzyMatch } from "../../fuzzy";
2
+ import { FuzzyQuery, FuzzyText } from "../../fuzzy";
3
3
  import {
4
4
  type DashboardState,
5
5
  type DisabledReason,
@@ -119,6 +119,50 @@ export function flattenTree(tree: TreeNode[]): FlatTreeItem[] {
119
119
  return flat;
120
120
  }
121
121
 
122
+ /** Searchable fields an extension's cached {@link FuzzyText} was built from. */
123
+ interface ExtensionSearchMemo {
124
+ name: string;
125
+ displayName: string;
126
+ description: string | undefined;
127
+ trigger: string | undefined;
128
+ providerName: string;
129
+ kind: string;
130
+ text: FuzzyText;
131
+ }
132
+
133
+ /** Prepared search text per extension; rebuilt if any searchable field changes. */
134
+ const extensionSearchCache = new WeakMap<Extension, ExtensionSearchMemo>();
135
+
136
+ function extensionSearchText(ext: Extension): FuzzyText {
137
+ const memo = extensionSearchCache.get(ext);
138
+ if (
139
+ memo !== undefined &&
140
+ memo.name === ext.name &&
141
+ memo.displayName === ext.displayName &&
142
+ memo.description === ext.description &&
143
+ memo.trigger === ext.trigger &&
144
+ memo.providerName === ext.source.providerName &&
145
+ memo.kind === ext.kind
146
+ ) {
147
+ return memo.text;
148
+ }
149
+ const text = new FuzzyText(
150
+ [ext.name, ext.displayName, ext.description || "", ext.trigger || "", ext.source.providerName, ext.kind].join(
151
+ " ",
152
+ ),
153
+ );
154
+ extensionSearchCache.set(ext, {
155
+ name: ext.name,
156
+ displayName: ext.displayName,
157
+ description: ext.description,
158
+ trigger: ext.trigger,
159
+ providerName: ext.source.providerName,
160
+ kind: ext.kind,
161
+ text,
162
+ });
163
+ return text;
164
+ }
165
+
122
166
  /**
123
167
  * Apply fuzzy filter to extensions.
124
168
  */
@@ -127,22 +171,18 @@ export function applyFilter(extensions: Extension[], query: string): Extension[]
127
171
  return extensions;
128
172
  }
129
173
 
130
- const tokens = query.toLowerCase().split(/\s+/).filter(Boolean);
174
+ const tokens = query
175
+ .toLowerCase()
176
+ .split(/\s+/)
177
+ .filter(Boolean)
178
+ .map(token => new FuzzyQuery(token));
131
179
  if (tokens.length === 0) {
132
180
  return extensions;
133
181
  }
134
182
 
135
183
  return extensions.filter(ext => {
136
- const searchable = [
137
- ext.name,
138
- ext.displayName,
139
- ext.description || "",
140
- ext.trigger || "",
141
- ext.source.providerName,
142
- ext.kind,
143
- ].join(" ");
144
-
145
- return tokens.every(token => fuzzyMatch(token, searchable).matches);
184
+ const searchable = extensionSearchText(ext);
185
+ return tokens.every(token => token.match(searchable).matches);
146
186
  });
147
187
  }
148
188