@linxiraos/pi-tui 1.1.27 → 1.2.1

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 (376) hide show
  1. package/CHANGELOG.md +50 -461
  2. package/README.md +7 -5
  3. package/dist/types/app-keybindings.d.ts +1 -1
  4. package/dist/types/apps/cleanse-board.d.ts +1 -1
  5. package/dist/types/apps/debug/log-viewer.d.ts +4 -0
  6. package/dist/types/apps/debug/raw-sse-buffer.d.ts +2 -0
  7. package/dist/types/apps/git/git-tui.d.ts +2 -2
  8. package/dist/types/apps/git/help.d.ts +3 -1
  9. package/dist/types/apps/if-bench-board.d.ts +2 -2
  10. package/dist/types/apps/setup-model-picker.d.ts +1 -1
  11. package/dist/types/autocomplete.d.ts +5 -2
  12. package/dist/types/charts/chart-plan.d.ts +674 -0
  13. package/dist/types/charts/chart-svg.d.ts +59 -0
  14. package/dist/types/charts/table-data.d.ts +175 -0
  15. package/dist/types/chat/assistant-message.d.ts +5 -0
  16. package/dist/types/chat/chat-transcript-builder.d.ts +2 -0
  17. package/dist/types/chat/compaction-summary-message.d.ts +1 -0
  18. package/dist/types/chat/execution-shared.d.ts +6 -5
  19. package/dist/types/chat/fence-figure.d.ts +42 -0
  20. package/dist/types/chat/figure-markdown.d.ts +42 -0
  21. package/dist/types/chat/messages.d.ts +1 -1
  22. package/dist/types/chat/svg-figure.d.ts +33 -0
  23. package/dist/types/chat/svg-source.d.ts +49 -0
  24. package/dist/types/chat/table-chart.d.ts +89 -0
  25. package/dist/types/chat/tool-execution.d.ts +2 -0
  26. package/dist/types/chat/user-message.d.ts +15 -2
  27. package/dist/types/chrome/shared.d.ts +9 -0
  28. package/dist/types/chrome/transcript-container.d.ts +6 -0
  29. package/dist/types/components/box.d.ts +1 -0
  30. package/dist/types/components/disclosure.d.ts +1 -0
  31. package/dist/types/components/editor.d.ts +15 -4
  32. package/dist/types/components/form.d.ts +1 -0
  33. package/dist/types/components/image.d.ts +12 -44
  34. package/dist/types/components/key-value-list.d.ts +1 -0
  35. package/dist/types/components/layout/row.d.ts +1 -0
  36. package/dist/types/components/layout/stack.d.ts +1 -0
  37. package/dist/types/components/markdown.d.ts +36 -3
  38. package/dist/types/components/scroll-view.d.ts +1 -0
  39. package/dist/types/components/section.d.ts +1 -0
  40. package/dist/types/components/table.d.ts +1 -0
  41. package/dist/types/components/text.d.ts +1 -0
  42. package/dist/types/components/tree-view.d.ts +7 -0
  43. package/dist/types/components/truncated-text.d.ts +1 -0
  44. package/dist/types/fuzzy.d.ts +29 -1
  45. package/dist/types/key-hint-format.d.ts +1 -1
  46. package/dist/types/keybinding-matchers.d.ts +3 -0
  47. package/dist/types/keybindings.d.ts +1 -1
  48. package/dist/types/kitty-graphics.d.ts +2 -2
  49. package/dist/types/loop-watchdog.d.ts +8 -7
  50. package/dist/types/multiplexers/cmux.d.ts +16 -0
  51. package/dist/types/multiplexers/herdr.d.ts +26 -0
  52. package/dist/types/multiplexers/notify.d.ts +13 -0
  53. package/dist/types/multiplexers/orca.d.ts +12 -0
  54. package/dist/types/multiplexers/screen.d.ts +9 -0
  55. package/dist/types/multiplexers/tmux.d.ts +29 -0
  56. package/dist/types/multiplexers/types.d.ts +100 -0
  57. package/dist/types/multiplexers/wmux.d.ts +12 -0
  58. package/dist/types/multiplexers/zellij.d.ts +12 -0
  59. package/dist/types/native/backend.d.ts +3 -2
  60. package/dist/types/native/blobs.d.ts +7 -3
  61. package/dist/types/native/encode.d.ts +3 -4
  62. package/dist/types/native/overlay.d.ts +5 -0
  63. package/dist/types/overlays/agent-hub-projection.d.ts +6 -5
  64. package/dist/types/overlays/agent-hub-types.d.ts +9 -0
  65. package/dist/types/overlays/agent-hub.d.ts +2 -0
  66. package/dist/types/overlays/agent-transcript-viewer.d.ts +11 -2
  67. package/dist/types/overlays/agents-hub.d.ts +9 -0
  68. package/dist/types/overlays/annotation-overlay.d.ts +19 -2
  69. package/dist/types/overlays/annotation-types.d.ts +2 -0
  70. package/dist/types/overlays/ask-dialog.d.ts +5 -2
  71. package/dist/types/overlays/history-search.d.ts +10 -0
  72. package/dist/types/overlays/hook-selector.d.ts +9 -4
  73. package/dist/types/overlays/jobs-panel.d.ts +1 -7
  74. package/dist/types/overlays/model-browser.d.ts +54 -7
  75. package/dist/types/overlays/model-hub.d.ts +23 -0
  76. package/dist/types/overlays/oauth-selector.d.ts +0 -1
  77. package/dist/types/overlays/plan-review-overlay.d.ts +3 -2
  78. package/dist/types/overlays/session-observer-registry.d.ts +2 -0
  79. package/dist/types/overlays/session-selector.d.ts +6 -2
  80. package/dist/types/overlays/settings-defs.d.ts +1 -0
  81. package/dist/types/overlays/usage-dashboard.d.ts +8 -4
  82. package/dist/types/overlays/usage-display.d.ts +3 -1
  83. package/dist/types/prompt/composer.d.ts +2 -7
  84. package/dist/types/prompt/custom-editor.d.ts +11 -8
  85. package/dist/types/prompt/image-source.d.ts +2 -1
  86. package/dist/types/prompt/interactive-context-helpers.d.ts +4 -1
  87. package/dist/types/prompt/model-mention-autocomplete.d.ts +1 -1
  88. package/dist/types/prompt/welcome.d.ts +19 -42
  89. package/dist/types/render/composite.d.ts +2 -0
  90. package/dist/types/render/output-pane.d.ts +1 -0
  91. package/dist/types/render/render-utils.d.ts +1 -0
  92. package/dist/types/render/tool-card.d.ts +6 -0
  93. package/dist/types/render/width-aware-text.d.ts +2 -0
  94. package/dist/types/setup/scenes/splash.d.ts +1 -1
  95. package/dist/types/setup/scenes/types.d.ts +1 -0
  96. package/dist/types/status-line/component.d.ts +3 -0
  97. package/dist/types/status-line/host.d.ts +4 -1
  98. package/dist/types/status-line/segments.d.ts +2 -0
  99. package/dist/types/status-line/types.d.ts +14 -3
  100. package/dist/types/terminal-capabilities.d.ts +27 -35
  101. package/dist/types/terminal-multiplexer.d.ts +127 -14
  102. package/dist/types/terminal.d.ts +22 -6
  103. package/dist/types/theme/active-symbols.d.ts +1 -1
  104. package/dist/types/theme/schema.d.ts +11 -2
  105. package/dist/types/theme/symbols.d.ts +1 -1
  106. package/dist/types/theme/theme-class.d.ts +7 -0
  107. package/dist/types/theme/theme.d.ts +1 -1
  108. package/dist/types/theme/tui-adapters.d.ts +3 -0
  109. package/dist/types/tmux.d.ts +11 -4
  110. package/dist/types/tools/daemon.d.ts +1 -1
  111. package/dist/types/tools/eval.d.ts +22 -3
  112. package/dist/types/tools/glob.d.ts +4 -0
  113. package/dist/types/tools/native-view.d.ts +47 -0
  114. package/dist/types/tools/output-meta.d.ts +11 -1
  115. package/dist/types/tools/renderer.d.ts +37 -2
  116. package/dist/types/tools/streaming-output.d.ts +20 -1
  117. package/dist/types/tools/task.d.ts +0 -4
  118. package/dist/types/tools/write.d.ts +9 -2
  119. package/dist/types/tools/yield.d.ts +22 -0
  120. package/dist/types/tui.d.ts +31 -0
  121. package/dist/types/utils.d.ts +6 -0
  122. package/dist/types/vim.d.ts +19 -5
  123. package/package.json +13 -9
  124. package/src/app-keybindings.d.ts +1 -1
  125. package/src/app-keybindings.ts +1 -1
  126. package/src/apps/autoresearch-dashboard.ts +7 -3
  127. package/src/apps/cleanse-board.ts +2 -2
  128. package/src/apps/cleanse-picker.ts +1 -1
  129. package/src/apps/debug/log-viewer.ts +188 -72
  130. package/src/apps/debug/raw-sse-buffer.ts +8 -0
  131. package/src/apps/debug/raw-sse.ts +39 -5
  132. package/src/apps/git/diff-pane.ts +26 -5
  133. package/src/apps/git/git-tui.ts +2 -2
  134. package/src/apps/git/help.ts +29 -20
  135. package/src/apps/if-bench-board.ts +2 -2
  136. package/src/apps/ps-top.ts +2 -2
  137. package/src/apps/setup-model-picker.ts +1 -1
  138. package/src/autocomplete.d.ts +5 -2
  139. package/src/autocomplete.ts +67 -43
  140. package/src/charts/chart-plan.d.ts +674 -0
  141. package/src/charts/chart-plan.ts +5081 -0
  142. package/src/charts/chart-svg.d.ts +59 -0
  143. package/src/charts/chart-svg.ts +4657 -0
  144. package/src/charts/table-data.d.ts +175 -0
  145. package/src/charts/table-data.ts +1478 -0
  146. package/src/chat/advisor-message.ts +11 -0
  147. package/src/chat/assistant-message.d.ts +5 -0
  148. package/src/chat/assistant-message.ts +219 -50
  149. package/src/chat/chat-transcript-builder.d.ts +2 -0
  150. package/src/chat/chat-transcript-builder.ts +5 -3
  151. package/src/chat/compaction-summary-message.d.ts +1 -0
  152. package/src/chat/compaction-summary-message.ts +8 -0
  153. package/src/chat/execution-shared.d.ts +6 -5
  154. package/src/chat/execution-shared.ts +42 -25
  155. package/src/chat/fence-figure.d.ts +42 -0
  156. package/src/chat/fence-figure.ts +80 -0
  157. package/src/chat/figure-markdown.d.ts +42 -0
  158. package/src/chat/figure-markdown.ts +174 -0
  159. package/src/chat/messages.d.ts +1 -1
  160. package/src/chat/messages.ts +1 -1
  161. package/src/chat/read-tool-group.ts +6 -4
  162. package/src/chat/skill-message.ts +6 -0
  163. package/src/chat/svg-figure.d.ts +33 -0
  164. package/src/chat/svg-figure.ts +399 -0
  165. package/src/chat/svg-source.d.ts +49 -0
  166. package/src/chat/svg-source.ts +188 -0
  167. package/src/chat/table-chart.d.ts +89 -0
  168. package/src/chat/table-chart.ts +294 -0
  169. package/src/chat/tool-execution.d.ts +2 -0
  170. package/src/chat/tool-execution.ts +80 -6
  171. package/src/chat/user-message.d.ts +15 -2
  172. package/src/chat/user-message.ts +55 -3
  173. package/src/chrome/shared.ts +53 -0
  174. package/src/chrome/transcript-container.d.ts +6 -0
  175. package/src/chrome/transcript-container.ts +93 -13
  176. package/src/components/box.d.ts +1 -0
  177. package/src/components/box.ts +5 -0
  178. package/src/components/disclosure.d.ts +1 -0
  179. package/src/components/disclosure.ts +8 -0
  180. package/src/components/editor.d.ts +15 -4
  181. package/src/components/editor.ts +294 -46
  182. package/src/components/form.d.ts +1 -0
  183. package/src/components/form.ts +8 -0
  184. package/src/components/image.d.ts +12 -44
  185. package/src/components/image.ts +47 -151
  186. package/src/components/key-value-list.d.ts +1 -0
  187. package/src/components/key-value-list.ts +4 -0
  188. package/src/components/layout/row.d.ts +1 -0
  189. package/src/components/layout/row.ts +7 -0
  190. package/src/components/layout/stack.d.ts +1 -0
  191. package/src/components/layout/stack.ts +7 -0
  192. package/src/components/markdown.d.ts +36 -3
  193. package/src/components/markdown.ts +433 -318
  194. package/src/components/scroll-view.d.ts +1 -0
  195. package/src/components/scroll-view.ts +5 -0
  196. package/src/components/section.d.ts +1 -0
  197. package/src/components/section.ts +5 -0
  198. package/src/components/settings-list.ts +2 -0
  199. package/src/components/table.d.ts +1 -0
  200. package/src/components/table.ts +4 -0
  201. package/src/components/text.d.ts +1 -0
  202. package/src/components/text.ts +7 -0
  203. package/src/components/tree-view.d.ts +7 -0
  204. package/src/components/tree-view.ts +38 -8
  205. package/src/components/truncated-text.d.ts +1 -0
  206. package/src/components/truncated-text.ts +5 -0
  207. package/src/desktop-notify.ts +1 -1
  208. package/src/fuzzy.d.ts +29 -1
  209. package/src/fuzzy.ts +115 -30
  210. package/src/key-hint-format.d.ts +1 -1
  211. package/src/key-hint-format.ts +1 -1
  212. package/src/keybinding-matchers.d.ts +3 -0
  213. package/src/keybinding-matchers.ts +6 -2
  214. package/src/keybindings.d.ts +1 -1
  215. package/src/keybindings.ts +1 -1
  216. package/src/kitty-graphics.d.ts +2 -2
  217. package/src/kitty-graphics.ts +4 -4
  218. package/src/loop-watchdog.d.ts +8 -7
  219. package/src/loop-watchdog.ts +18 -13
  220. package/src/multiplexers/cmux.d.ts +16 -0
  221. package/src/multiplexers/cmux.ts +29 -0
  222. package/src/multiplexers/herdr.d.ts +26 -0
  223. package/src/multiplexers/herdr.ts +64 -0
  224. package/src/multiplexers/notify.d.ts +13 -0
  225. package/src/multiplexers/notify.ts +24 -0
  226. package/src/multiplexers/orca.d.ts +12 -0
  227. package/src/multiplexers/orca.ts +16 -0
  228. package/src/multiplexers/screen.d.ts +9 -0
  229. package/src/multiplexers/screen.ts +15 -0
  230. package/src/multiplexers/tmux.d.ts +29 -0
  231. package/src/multiplexers/tmux.ts +40 -0
  232. package/src/multiplexers/types.d.ts +100 -0
  233. package/src/multiplexers/types.ts +110 -0
  234. package/src/multiplexers/wmux.d.ts +12 -0
  235. package/src/multiplexers/wmux.ts +16 -0
  236. package/src/multiplexers/zellij.d.ts +12 -0
  237. package/src/multiplexers/zellij.ts +24 -0
  238. package/src/native/backend.d.ts +3 -2
  239. package/src/native/backend.ts +174 -18
  240. package/src/native/blobs.d.ts +7 -3
  241. package/src/native/blobs.ts +47 -12
  242. package/src/native/encode.d.ts +3 -4
  243. package/src/native/encode.ts +58 -31
  244. package/src/native/overlay.d.ts +5 -0
  245. package/src/native/overlay.ts +13 -0
  246. package/src/native/reconcile.ts +4 -0
  247. package/src/overlays/agent-hub-projection.d.ts +6 -5
  248. package/src/overlays/agent-hub-projection.ts +124 -13
  249. package/src/overlays/agent-hub-types.d.ts +9 -0
  250. package/src/overlays/agent-hub-types.ts +6 -0
  251. package/src/overlays/agent-hub.ts +46 -31
  252. package/src/overlays/agent-transcript-viewer.ts +91 -23
  253. package/src/overlays/agents-hub.ts +28 -8
  254. package/src/overlays/annotation-overlay.ts +293 -90
  255. package/src/overlays/annotation-types.ts +2 -0
  256. package/src/overlays/ask-dialog.ts +7 -10
  257. package/src/overlays/btw-history-panel.ts +12 -1
  258. package/src/overlays/copy-selector.ts +30 -8
  259. package/src/overlays/extensions/state-manager.ts +52 -12
  260. package/src/overlays/history-search.ts +100 -7
  261. package/src/overlays/hook-selector.ts +12 -5
  262. package/src/overlays/jobs-panel.ts +28 -46
  263. package/src/overlays/model-browser.ts +341 -110
  264. package/src/overlays/model-hub.ts +300 -49
  265. package/src/overlays/move-overlay.ts +28 -14
  266. package/src/overlays/oauth-selector.ts +54 -70
  267. package/src/overlays/plan-review-overlay.ts +194 -70
  268. package/src/overlays/plugin-settings.ts +11 -5
  269. package/src/overlays/report-panel.ts +2 -2
  270. package/src/overlays/rewind-selector.ts +86 -17
  271. package/src/overlays/session-observer-registry.d.ts +2 -0
  272. package/src/overlays/session-observer-registry.ts +10 -2
  273. package/src/overlays/session-selector.ts +64 -26
  274. package/src/overlays/settings-defs.ts +4 -3
  275. package/src/overlays/settings-selector.ts +33 -8
  276. package/src/overlays/snapcompact-shape-preview.ts +2 -3
  277. package/src/overlays/stats-notice.ts +1 -1
  278. package/src/overlays/tree-selector.ts +41 -20
  279. package/src/overlays/usage-dashboard.ts +100 -50
  280. package/src/overlays/usage-display.ts +10 -4
  281. package/src/prompt/attachment-chips.ts +15 -11
  282. package/src/prompt/composer-attachments.ts +6 -4
  283. package/src/prompt/composer-cache.ts +4 -6
  284. package/src/prompt/composer.ts +6 -37
  285. package/src/prompt/custom-editor.ts +54 -26
  286. package/src/prompt/image-source.ts +2 -1
  287. package/src/prompt/interactive-context-helpers.ts +30 -18
  288. package/src/prompt/macos-spelling.ts +7 -0
  289. package/src/prompt/magic-keywords.ts +11 -2
  290. package/src/prompt/model-mention-autocomplete.ts +14 -14
  291. package/src/prompt/tips.txt +6 -5
  292. package/src/prompt/welcome.ts +235 -411
  293. package/src/prompt/word-completion.ts +38 -5
  294. package/src/render/composite.d.ts +2 -0
  295. package/src/render/composite.ts +64 -0
  296. package/src/render/output-pane.d.ts +1 -0
  297. package/src/render/output-pane.ts +6 -0
  298. package/src/render/render-utils.d.ts +1 -0
  299. package/src/render/render-utils.ts +11 -0
  300. package/src/render/tool-card.d.ts +6 -0
  301. package/src/render/tool-card.ts +15 -0
  302. package/src/render/width-aware-text.d.ts +2 -0
  303. package/src/render/width-aware-text.ts +5 -0
  304. package/src/setup/scenes/model.ts +2 -1
  305. package/src/setup/scenes/splash.ts +116 -53
  306. package/src/setup/scenes/types.ts +1 -0
  307. package/src/status-line/component.ts +292 -78
  308. package/src/status-line/host.ts +4 -1
  309. package/src/status-line/segments.ts +40 -22
  310. package/src/status-line/types.ts +15 -3
  311. package/src/terminal-capabilities.d.ts +27 -35
  312. package/src/terminal-capabilities.ts +75 -189
  313. package/src/terminal-multiplexer.d.ts +127 -14
  314. package/src/terminal-multiplexer.ts +112 -33
  315. package/src/terminal.d.ts +22 -6
  316. package/src/terminal.ts +139 -37
  317. package/src/theme/active-symbols.d.ts +1 -1
  318. package/src/theme/active-symbols.ts +1 -1
  319. package/src/theme/schema-validation.ts +1 -0
  320. package/src/theme/schema.d.ts +11 -2
  321. package/src/theme/schema.ts +14 -1
  322. package/src/theme/session-color.ts +1 -7
  323. package/src/theme/symbols.d.ts +1 -1
  324. package/src/theme/symbols.ts +6 -1
  325. package/src/theme/theme-class.d.ts +7 -0
  326. package/src/theme/theme-class.ts +26 -1
  327. package/src/theme/theme-schema.json +5 -1
  328. package/src/theme/theme.d.ts +1 -1
  329. package/src/theme/theme.ts +8 -4
  330. package/src/theme/tui-adapters.d.ts +3 -0
  331. package/src/theme/tui-adapters.ts +68 -0
  332. package/src/tmux.d.ts +11 -4
  333. package/src/tmux.ts +47 -11
  334. package/src/tools/ast-edit.ts +59 -35
  335. package/src/tools/ast-grep.ts +35 -16
  336. package/src/tools/bash.ts +92 -42
  337. package/src/tools/daemon.d.ts +1 -1
  338. package/src/tools/daemon.ts +1 -1
  339. package/src/tools/default-renderer.ts +35 -19
  340. package/src/tools/edit.ts +142 -120
  341. package/src/tools/eval-format/index.ts +28 -2
  342. package/src/tools/eval-format/javascript.ts +7 -5
  343. package/src/tools/eval.d.ts +22 -3
  344. package/src/tools/eval.ts +364 -208
  345. package/src/tools/fetch.ts +5 -6
  346. package/src/tools/glob.d.ts +4 -0
  347. package/src/tools/glob.ts +33 -65
  348. package/src/tools/grep.ts +81 -48
  349. package/src/tools/index.ts +2 -0
  350. package/src/tools/lsp.ts +81 -59
  351. package/src/tools/mcp.ts +111 -68
  352. package/src/tools/native-view.d.ts +47 -0
  353. package/src/tools/native-view.ts +102 -1
  354. package/src/tools/output-meta.d.ts +11 -1
  355. package/src/tools/output-meta.ts +23 -3
  356. package/src/tools/read.ts +7 -5
  357. package/src/tools/renderer.d.ts +37 -2
  358. package/src/tools/renderer.ts +42 -2
  359. package/src/tools/streaming-output.d.ts +20 -1
  360. package/src/tools/streaming-output.ts +149 -50
  361. package/src/tools/task.d.ts +0 -4
  362. package/src/tools/task.ts +112 -59
  363. package/src/tools/todo.ts +119 -30
  364. package/src/tools/wait.ts +34 -24
  365. package/src/tools/web-search.ts +1 -1
  366. package/src/tools/write.d.ts +9 -2
  367. package/src/tools/write.ts +147 -95
  368. package/src/tools/yield.d.ts +22 -0
  369. package/src/tools/yield.ts +225 -0
  370. package/src/tui.d.ts +31 -0
  371. package/src/tui.ts +309 -160
  372. package/src/utils.d.ts +6 -0
  373. package/src/utils.ts +19 -0
  374. package/src/vim.d.ts +19 -5
  375. package/src/vim.ts +333 -18
  376. package/src/windows-input-mode.ts +8 -0
@@ -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
  }
@@ -711,8 +711,11 @@ export class AskDialogComponent implements Component {
711
711
  }
712
712
 
713
713
  /**
714
- * A bottom-anchored glass sheet over the composer (the `overlay` hoists into
715
- * 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.
716
719
  */
717
720
  describe(cx: DescribeContext): NativeNode {
718
721
  const inputGuard = this.options.inputGuard;
@@ -728,13 +731,7 @@ export class AskDialogComponent implements Component {
728
731
  if (this.#isSubmitTab()) this.#describeSubmitBody(children);
729
732
  else this.#describeQuestionBody(children);
730
733
  children.push(this.#describeActions(blocked));
731
- const sheet = node(
732
- "overlay",
733
- { role: "zeta.overlay.ask", anchor: "bottom", size: "md", modal: true },
734
- [col(children, { gap: "md" })],
735
- "sheet",
736
- );
737
- this.#native = col([sheet]);
734
+ this.#native = col(children, { role: "zeta.editor", gap: "md" });
738
735
  this.#nativeBlocked = blocked;
739
736
  return this.#native;
740
737
  }
@@ -1137,7 +1134,7 @@ export class AskDialogComponent implements Component {
1137
1134
  const action = question?.multi
1138
1135
  ? `${formatKeyHint("space")} toggle · ${enter} ${enterAction}`
1139
1136
  : `${enter} select · ${formatKeyHint("n")} note`;
1140
- const tabs = this.#hasSubmitTab() ? ` · ${formatKeyHints(["tab", "left", "right"])}` : "";
1137
+ const tabs = this.#hasSubmitTab() ? ` · ${formatKeyHints(["tab", "left", "right"])} question` : "";
1141
1138
  const expand = this.#expandHint();
1142
1139
  if (this.#questionCanPage && indicator) {
1143
1140
  const pageKeys = editorKeys("tui.select.pageUp", "tui.select.pageDown");
@@ -37,7 +37,7 @@ import { clampSelection, contentRowWidth, padLinesToHeight, renderScrollableList
37
37
  import type { TspScrollBy, TspSpan } from "@linxiraos/pi-wire";
38
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[];
@@ -485,6 +485,13 @@ export class BtwHistoryPanel implements Component, Focusable {
485
485
  children.push(node("col", undefined, lines, "composer"));
486
486
  }
487
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
+ });
488
495
  // The sheet is the frame (`nativeOverlay`): a borderless column.
489
496
  const described = col(children, { gap: "md" });
490
497
  this.#native = {
@@ -504,6 +511,10 @@ export class BtwHistoryPanel implements Component, Focusable {
504
511
  }
505
512
 
506
513
  handleNativeEvent(event: NativeUiEvent): void {
514
+ if (event.type === "action" && event.act === "close") {
515
+ this.#options.onClose();
516
+ return;
517
+ }
507
518
  if ((event.type !== "select" && event.type !== "activate") || this.#composer) return;
508
519
  const index = this.#records.findIndex(record => record.id === event.item);
509
520
  if (index === -1) return;
@@ -327,6 +327,12 @@ interface ControlRegion {
327
327
  end: number;
328
328
  }
329
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
+
330
336
  export class CopySelectorComponent implements Component {
331
337
  #builder: ChatTranscriptBuilder;
332
338
  #browser: TranscriptBrowser;
@@ -348,6 +354,8 @@ export class CopySelectorComponent implements Component {
348
354
 
349
355
  /** Last described root and the state it was built from. */
350
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;
351
359
  /** Transcript list items, rebuilt only when the replayed targets change. */
352
360
  #nativeItems: { targets: OutlineTarget[]; truncated: boolean; items: NativeNode[] } | undefined;
353
361
  /** Timeline picker items of the replayed targets. */
@@ -414,6 +422,7 @@ export class CopySelectorComponent implements Component {
414
422
  }
415
423
 
416
424
  invalidate(): void {
425
+ this.#blockPreviews = undefined;
417
426
  this.#builder.container.invalidate();
418
427
  this.#browser.invalidate();
419
428
  }
@@ -945,18 +954,13 @@ export class CopySelectorComponent implements Component {
945
954
  */
946
955
  #composeBlocks(blocks: CopyBlock[], columnWidth: number, lineOffset: number): ComposedColumn {
947
956
  const inner = Math.max(10, columnWidth - 4);
957
+ const previews = this.#previewBlocks(blocks, inner);
948
958
  const lines: string[] = [];
949
959
  let selStart = -1;
950
960
  let selEnd = -1;
951
961
  for (let index = 0; index < blocks.length; index++) {
952
962
  const block = blocks[index]!;
953
- const raw = block.content.split("\n");
954
- const shown = raw.slice(0, BLOCK_PREVIEW_LINES);
955
- const styled = block.language ? highlightCode(shown.join("\n"), block.language) : shown;
956
- const rows = styled.map(row => truncateToWidth(replaceTabs(row), inner));
957
- if (raw.length > shown.length) {
958
- rows.push(theme.fg("dim", `… +${raw.length - shown.length} more lines`));
959
- }
963
+ const { rows, lineCount } = previews[index]!;
960
964
  const selected = index === this.#blockSelected;
961
965
  const captionColor: ThemeColor = selected ? OUTLINE_COLOR : "dim";
962
966
  const controls: Array<{ action: ControlRegion["action"]; text: string }> = [
@@ -965,7 +969,7 @@ export class CopySelectorComponent implements Component {
965
969
  if (block.href && this.deps.onOpen) controls.push({ action: "open", text: `${theme.cmd.share} open` });
966
970
  const controlsWidth = controls.reduce((sum, control) => sum + visibleWidth(control.text) + 2, 0);
967
971
  const summary = truncateToWidth(
968
- `${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"}`,
969
973
  Math.max(4, inner - controlsWidth),
970
974
  );
971
975
  // Caption: two-space gutter, summary, then the controls, each preceded by two spaces.
@@ -998,6 +1002,24 @@ export class CopySelectorComponent implements Component {
998
1002
  lines.push("");
999
1003
  return { lines, selStart, selEnd };
1000
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
+ }
1001
1023
  }
1002
1024
 
1003
1025
  /** Raw multi-line text of a user message (string or text blocks). */
@@ -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
 
@@ -65,6 +65,13 @@ const PICKER_KEY = "picker";
65
65
  /** Visible result rows; also the jump distance for PageUp/PageDown. */
66
66
  const MAX_VISIBLE = 10;
67
67
 
68
+ /**
69
+ * Quiet period after the last query edit before the synchronous storage
70
+ * search runs. Only applied when the host wires {@link HistorySearchComponent.setOnRequestRender},
71
+ * since the deferred results need a repaint to show up.
72
+ */
73
+ const SEARCH_DEBOUNCE_MS = 100;
74
+
68
75
  /** Split a query the same way `HistorySource` tokenizes it, so highlights align with matches. */
69
76
  function queryTokens(query: string): string[] {
70
77
  return query
@@ -106,6 +113,8 @@ class HistoryResultsList implements Component {
106
113
  #menu: MenuSelection<HistorySearchEntry>;
107
114
  #tokens: string[] = [];
108
115
  #maxVisible = MAX_VISIBLE;
116
+ /** Highlighted prompt text per entry, valid for one `(tokens, budget)` pair; reset on invalidate (theme). */
117
+ #highlightCache = new WeakMap<HistorySearchEntry, { tokens: string[]; budget: number; text: string }>();
109
118
 
110
119
  constructor(menu: MenuSelection<HistorySearchEntry>) {
111
120
  this.#menu = menu;
@@ -116,7 +125,17 @@ class HistoryResultsList implements Component {
116
125
  }
117
126
 
118
127
  invalidate(): void {
119
- // No cached state to invalidate currently
128
+ this.#highlightCache = new WeakMap();
129
+ }
130
+
131
+ /** Truncated, token-highlighted prompt of `entry` for `budget` cells. */
132
+ #highlightedPrompt(entry: HistorySearchEntry, budget: number): string {
133
+ const cached = this.#highlightCache.get(entry);
134
+ if (cached !== undefined && cached.tokens === this.#tokens && cached.budget === budget) return cached.text;
135
+ const normalized = entry.prompt.replace(/\s+/g, " ").trim();
136
+ const text = highlightTokens(truncateToWidth(normalized, budget), this.#tokens);
137
+ this.#highlightCache.set(entry, { tokens: this.#tokens, budget, text });
138
+ return text;
120
139
  }
121
140
 
122
141
  render(width: number): readonly string[] {
@@ -151,9 +170,7 @@ class HistoryResultsList implements Component {
151
170
  const showTime = rowWidth >= gutterWidth + 12 + timeWidth;
152
171
 
153
172
  const promptBudget = Math.max(4, rowWidth - gutterWidth - (showTime ? timeWidth + 1 : 0));
154
- const normalized = entry.prompt.replace(/\s+/g, " ").trim();
155
- const plain = truncateToWidth(normalized, promptBudget);
156
- const highlighted = highlightTokens(plain, this.#tokens);
173
+ const highlighted = this.#highlightedPrompt(entry, promptBudget);
157
174
 
158
175
  const cursor = isSelected ? theme.fg("accent", cursorSymbol) : padding(gutterWidth);
159
176
  let line = cursor + (isSelected ? theme.bold(highlighted) : highlighted);
@@ -186,12 +203,22 @@ export class HistorySearchComponent extends OverlayPanel {
186
203
  #nativeHints: NativeNode | undefined;
187
204
  #nativeMemo: HistoryNativeMemo | undefined;
188
205
  #pickerItems: { items: readonly HistorySearchEntry[]; rows: readonly TspPickerItem[] } | undefined;
206
+ /** Trimmed query the current results were fetched for; `undefined` before the first fetch. */
207
+ #resultsQuery: string | undefined;
208
+ #searchTimer: NodeJS.Timeout | undefined;
209
+ #onRequestRender?: () => void;
189
210
 
190
211
  constructor(historyStorage: HistorySource, onSelect: (prompt: string) => void, onCancel: () => void) {
191
212
  super("History", "zeta.overlay.history");
192
213
  this.#historyStorage = historyStorage;
193
- this.#onSelect = onSelect;
194
- this.#onCancel = onCancel;
214
+ this.#onSelect = prompt => {
215
+ this.#cancelPendingSearch();
216
+ onSelect(prompt);
217
+ };
218
+ this.#onCancel = () => {
219
+ this.#cancelPendingSearch();
220
+ onCancel();
221
+ };
195
222
 
196
223
  this.#menu = new MenuSelection<HistorySearchEntry>([], {
197
224
  getKey: entry => `${entry.created_at}:${entry.prompt}`,
@@ -199,6 +226,7 @@ export class HistorySearchComponent extends OverlayPanel {
199
226
  });
200
227
  this.#searchInput = new Input();
201
228
  this.#searchInput.onSubmit = () => {
229
+ this.#flushPendingSearch();
202
230
  const selected = this.#menu.selectedItem;
203
231
  if (selected) {
204
232
  this.#onSelect(selected.prompt);
@@ -225,38 +253,59 @@ export class HistorySearchComponent extends OverlayPanel {
225
253
  this.#updateResults();
226
254
  }
227
255
 
256
+ /**
257
+ * Repaint hook for debounced result updates. Once set, query edits search
258
+ * storage after a {@link SEARCH_DEBOUNCE_MS} quiet period instead of on
259
+ * every key; without it every edit searches synchronously.
260
+ */
261
+ setOnRequestRender(callback: () => void): void {
262
+ this.#onRequestRender = callback;
263
+ }
264
+
265
+ override dispose(): void {
266
+ this.#cancelPendingSearch();
267
+ super.dispose();
268
+ }
269
+
228
270
  handleInput(keyData: string): void {
229
271
  if (matchesSelectUp(keyData)) {
272
+ this.#flushPendingSearch();
230
273
  this.#menu.move(-1, false);
231
274
  return;
232
275
  }
233
276
 
234
277
  if (matchesSelectDown(keyData)) {
278
+ this.#flushPendingSearch();
235
279
  this.#menu.move(1, false);
236
280
  return;
237
281
  }
238
282
 
239
283
  if (matchesSelectPageUp(keyData)) {
284
+ this.#flushPendingSearch();
240
285
  this.#menu.move(-MAX_VISIBLE, false);
241
286
  return;
242
287
  }
243
288
 
244
289
  if (matchesSelectPageDown(keyData)) {
290
+ this.#flushPendingSearch();
245
291
  this.#menu.move(MAX_VISIBLE, false);
246
292
  return;
247
293
  }
248
294
 
249
295
  if (matchesKey(keyData, "home")) {
296
+ this.#flushPendingSearch();
250
297
  this.#menu.moveToBoundary("first");
251
298
  return;
252
299
  }
253
300
 
254
301
  if (matchesKey(keyData, "end")) {
302
+ this.#flushPendingSearch();
255
303
  this.#menu.moveToBoundary("last");
256
304
  return;
257
305
  }
258
306
 
259
307
  if (matchesKey(keyData, "enter") || matchesKey(keyData, "return") || keyData === "\n") {
308
+ this.#flushPendingSearch();
260
309
  const selected = this.#menu.selectedItem;
261
310
  if (selected) {
262
311
  this.#onSelect(selected.prompt);
@@ -270,7 +319,7 @@ export class HistorySearchComponent extends OverlayPanel {
270
319
  }
271
320
 
272
321
  this.#searchInput.handleInput(keyData);
273
- this.#updateResults();
322
+ this.#scheduleResults();
274
323
  }
275
324
 
276
325
  /**
@@ -372,6 +421,9 @@ export class HistorySearchComponent extends OverlayPanel {
372
421
  * Picker: a row click highlights, a second click or `Insert` inserts it
373
422
  * (Enter), `Close` cancels (Esc), `Clear search` empties the query.
374
423
  * List: picking a result does what highlighting it and pressing Enter does.
424
+ * A row picked while a debounced search is pending belongs to the previous
425
+ * query: results are refreshed first and the pick only lands if that entry
426
+ * is still among them.
375
427
  */
376
428
  handleNativeEvent(event: NativeUiEvent): void {
377
429
  const ev = pickerEvent(event, PICKER_KEY);
@@ -385,6 +437,7 @@ export class HistorySearchComponent extends OverlayPanel {
385
437
  }
386
438
  return;
387
439
  }
440
+ this.#flushPendingSearch();
388
441
  const index = this.#menu.visibleItems.findIndex(entry => nativeEntryKey(entry) === ev.item);
389
442
  if (index < 0) return;
390
443
  this.#menu.setSelectedIndex(index);
@@ -392,6 +445,7 @@ export class HistorySearchComponent extends OverlayPanel {
392
445
  return;
393
446
  }
394
447
  if ((event.type !== "select" && event.type !== "activate") || event.key !== "list") return;
448
+ this.#flushPendingSearch();
395
449
  const index = this.#menu.visibleItems.findIndex(entry => nativeEntryKey(entry) === event.item);
396
450
  const target = this.#menu.visibleItems[index];
397
451
  if (!target) return;
@@ -399,8 +453,47 @@ export class HistorySearchComponent extends OverlayPanel {
399
453
  this.#onSelect(target.prompt);
400
454
  }
401
455
 
456
+ /**
457
+ * Refresh results after a query edit: no-op when the trimmed query is
458
+ * unchanged (cursor moves, trailing spaces), immediate for the empty query
459
+ * or when no repaint hook is wired, debounced otherwise.
460
+ */
461
+ #scheduleResults(): void {
462
+ const query = this.#searchInput.getValue().trim();
463
+ if (query === this.#resultsQuery) {
464
+ this.#cancelPendingSearch();
465
+ return;
466
+ }
467
+ const requestRender = this.#onRequestRender;
468
+ if (!query || !requestRender) {
469
+ this.#updateResults();
470
+ return;
471
+ }
472
+ this.#cancelPendingSearch();
473
+ this.#searchTimer = setTimeout(() => {
474
+ this.#searchTimer = undefined;
475
+ this.#updateResults();
476
+ requestRender();
477
+ }, SEARCH_DEBOUNCE_MS);
478
+ }
479
+
480
+ /** Run a pending debounced search now, so selection acts on the current query's results. */
481
+ #flushPendingSearch(): void {
482
+ if (this.#searchTimer === undefined) return;
483
+ this.#cancelPendingSearch();
484
+ this.#updateResults();
485
+ }
486
+
487
+ #cancelPendingSearch(): void {
488
+ if (this.#searchTimer === undefined) return;
489
+ clearTimeout(this.#searchTimer);
490
+ this.#searchTimer = undefined;
491
+ }
492
+
402
493
  #updateResults(): void {
494
+ this.#cancelPendingSearch();
403
495
  const query = this.#searchInput.getValue().trim();
496
+ this.#resultsQuery = query;
404
497
  const results = query
405
498
  ? this.#historyStorage.search(query, this.#resultLimit)
406
499
  : this.#historyStorage.getRecent(this.#resultLimit);
@@ -94,6 +94,10 @@ export interface HookSelectorOptions {
94
94
  /** Number of leading options (original order) that receive a selection
95
95
  * marker. Defaults to every option when {@link selectionMarker} is set. */
96
96
  markableCount?: number;
97
+ /** Describe as a card in the dock (the composer slot) instead of a picker
98
+ * sheet, so dock content above it (e.g. a `/omfg` candidate rule) stays
99
+ * visible and the transcript stays scrollable on native surfaces. */
100
+ inline?: boolean;
97
101
  }
98
102
 
99
103
  export interface HookSelectorOption {
@@ -210,6 +214,7 @@ export class HookSelectorComponent extends OverlayPanel {
210
214
  #sliderComponent: Text | undefined;
211
215
  #lastRenderWidth: number | undefined;
212
216
  readonly #detailLines: readonly string[];
217
+ readonly #inline: boolean;
213
218
  readonly #helpText: string | undefined;
214
219
  /** Described option rows by original index; `marker` is the radio state baked into the label. */
215
220
  readonly #nativeItems = new Map<number, { marker: boolean; node: NativeNode }>();
@@ -264,6 +269,7 @@ export class HookSelectorComponent extends OverlayPanel {
264
269
  this.#baseTitle = this.title;
265
270
  this.#detailLines = title.split(/\r?\n/).slice(1);
266
271
  this.#helpText = opts?.helpText;
272
+ this.#inline = opts?.inline ?? false;
267
273
  this.#onLeftCallback = opts?.onLeft;
268
274
  this.#onRightCallback = opts?.onRight;
269
275
  this.#onExternalEditorCallback = opts?.onExternalEditor;
@@ -726,13 +732,14 @@ export class HookSelectorComponent extends OverlayPanel {
726
732
  }
727
733
 
728
734
  /**
729
- * A plain option list is a picker sheet. Otherwise a card headed by the
730
- * title (plus the countdown `elapsed` when a timeout runs), the extra title
731
- * lines, the slider as `tabs`, the options as a `list` keyed by original
732
- * option index, the search query while typing, and the key hints.
735
+ * A plain option list is a picker sheet. Otherwise (inline, slider, or
736
+ * countdown) a card headed by the title (plus the countdown `elapsed` when a
737
+ * timeout runs), the extra title lines, the slider as `tabs`, the options as
738
+ * a `list` keyed by original option index, the search query while typing,
739
+ * and the key hints.
733
740
  */
734
741
  override describe(cx: DescribeContext): NativeNode {
735
- if (cx.supports("picker") && !this.#slider && !this.#countdown) return this.#describePicker();
742
+ if (cx.supports("picker") && !this.#inline && !this.#slider && !this.#countdown) return this.#describePicker();
736
743
  const items = this.#menu.visibleItems;
737
744
  const selected = this.#menu.selectedKey;
738
745
  const query = this.#menu.query;
@@ -7,7 +7,7 @@
7
7
  */
8
8
  import type { TspAgentProps, TspSpan } from "@linxiraos/pi-wire";
9
9
  import { formatDuration } from "@linxiraos/pi-utils";
10
- import { type Component, Container } from "../tui";
10
+ import type { Component } from "../tui";
11
11
  import { matchesSelectCancel, matchesSelectDown, matchesSelectUp } from "../keybinding-matchers";
12
12
  import { matchesKey } from "../keys";
13
13
  import { truncateToWidth } from "../utils";
@@ -91,31 +91,6 @@ const DOT: Record<JobsPanelJob["status"], TspSpan> = {
91
91
  /** Output tail rows the terminal fallback shows under the selected job. */
92
92
  const FALLBACK_TAIL_LINES = 8;
93
93
 
94
- /** The `/jobs` block: `ansi` renders in the terminal, the snapshot describes the native frame. */
95
- export class JobsPanel extends Container {
96
- readonly #snapshot: JobsPanelSnapshot;
97
- readonly #nowMs: number;
98
- #native: { agent: boolean; node: NativeNode } | undefined;
99
-
100
- constructor(snapshot: JobsPanelSnapshot, nowMs: number, ansi: readonly Component[]) {
101
- super();
102
- this.#snapshot = snapshot;
103
- this.#nowMs = nowMs;
104
- for (const child of ansi) this.addChild(child);
105
- }
106
-
107
- override describe(cx: DescribeContext): NativeNode {
108
- const agent = cx.supports("agent");
109
- if (this.#native?.agent === agent) return this.#native.node;
110
- const head: TspSpan[] = [span("Background jobs", "strong")];
111
- const running = this.#snapshot.running.length;
112
- if (running > 0) head.push(span(` · ${running} running`, "muted"));
113
- const described = card({ role: "zeta.jobs", head }, describeJobs(this.#snapshot, this.#nowMs, agent));
114
- this.#native = { agent, node: described };
115
- return described;
116
- }
117
- }
118
-
119
94
  /**
120
95
  * The jobs pill's sheet: a centred `lg` glass sheet titled Background jobs.
121
96
  * A selectable list of every job (running first) sits over the selected
@@ -135,6 +110,8 @@ export class JobsSheet implements Component {
135
110
  readonly #source: JobsSheetSource;
136
111
  #selectedId: string | undefined;
137
112
  #native: { key: string; output: string | undefined; node: NativeNode } | undefined;
113
+ /** Fallback output tail, reused while the selected job's output is unchanged. */
114
+ #tail: { id: string; output: string; lines: readonly string[] } | undefined;
138
115
 
139
116
  constructor(source: JobsSheetSource) {
140
117
  this.#source = source;
@@ -202,8 +179,9 @@ export class JobsSheet implements Component {
202
179
  detail.artifactId && `artifact://${detail.artifactId}`,
203
180
  ]);
204
181
  if (facts.length > 0) lines.push(truncateToWidth(` ${facts.join(" · ")}`, width));
205
- const tail = (detail.output ?? "").trimEnd().split("\n").slice(-FALLBACK_TAIL_LINES);
206
- for (const line of tail) if (line) lines.push(truncateToWidth(` ${line}`, width));
182
+ for (const line of this.#tailLines(selected.id, detail.output ?? "")) {
183
+ if (line) lines.push(truncateToWidth(` ${line}`, width));
184
+ }
207
185
  }
208
186
  return lines;
209
187
  }
@@ -235,6 +213,14 @@ export class JobsSheet implements Component {
235
213
  return { jobs, selected };
236
214
  }
237
215
 
216
+ #tailLines(id: string, output: string): readonly string[] {
217
+ const cached = this.#tail;
218
+ if (cached?.id === id && cached.output === output) return cached.lines;
219
+ const lines = tailLines(output, FALLBACK_TAIL_LINES);
220
+ this.#tail = { id, output, lines };
221
+ return lines;
222
+ }
223
+
238
224
  #move(delta: number): void {
239
225
  const { jobs, selected } = this.#current();
240
226
  if (!selected) return;
@@ -253,25 +239,21 @@ function jobAge(job: JobsPanelJob, nowMs: number): number {
253
239
  return Math.max(0, (job.endTime ?? nowMs) - job.startTime);
254
240
  }
255
241
 
256
- /** The body the `/jobs` panel draws: running rows, then a Recent section; an empty note when there are none. */
257
- function describeJobs(snapshot: JobsPanelSnapshot, nowMs: number, agent: boolean): NativeChild[] {
258
- const { running, recent } = snapshot;
259
- const children: NativeChild[] = [];
260
- if (running.length === 0 && recent.length === 0) {
261
- children.push(node("text", { text: "No background jobs", role: "zeta.jobs.empty" }, undefined, "empty"));
262
- }
263
- for (const job of running) children.push(describeJob(job, nowMs, agent));
264
- if (recent.length > 0) {
265
- children.push(
266
- node(
267
- "section",
268
- { head: [span("Recent", "muted")] },
269
- recent.map(job => describeJob(job, nowMs, agent)),
270
- "recent",
271
- ),
272
- );
242
+ /**
243
+ * The last `count` pieces of `text.trimEnd().split("\n")`, found by scanning
244
+ * back from the end so a long running output is never split as a whole.
245
+ */
246
+ function tailLines(text: string, count: number): string[] {
247
+ const trimmed = text.trimEnd();
248
+ const lines: string[] = [];
249
+ let end = trimmed.length;
250
+ while (lines.length < count) {
251
+ const newline = end > 0 ? trimmed.lastIndexOf("\n", end - 1) : -1;
252
+ lines.push(trimmed.slice(newline + 1, end));
253
+ if (newline < 0) break;
254
+ end = newline;
273
255
  }
274
- return children;
256
+ return lines.reverse();
275
257
  }
276
258
 
277
259
  /** A task job as an `agent` node when the terminal draws them, else a one-line dot row. */