@linxiraos/pi-tui 1.1.27 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (373) hide show
  1. package/CHANGELOG.md +47 -463
  2. package/README.md +7 -5
  3. package/dist/types/app-keybindings.d.ts +1 -1
  4. package/dist/types/apps/cleanse-board.d.ts +1 -1
  5. package/dist/types/apps/debug/log-viewer.d.ts +4 -0
  6. package/dist/types/apps/debug/raw-sse-buffer.d.ts +2 -0
  7. package/dist/types/apps/git/git-tui.d.ts +2 -2
  8. package/dist/types/apps/git/help.d.ts +3 -1
  9. package/dist/types/apps/if-bench-board.d.ts +2 -2
  10. package/dist/types/apps/setup-model-picker.d.ts +1 -1
  11. package/dist/types/autocomplete.d.ts +5 -2
  12. package/dist/types/charts/chart-plan.d.ts +674 -0
  13. package/dist/types/charts/chart-svg.d.ts +59 -0
  14. package/dist/types/charts/table-data.d.ts +175 -0
  15. package/dist/types/chat/assistant-message.d.ts +5 -0
  16. package/dist/types/chat/chat-transcript-builder.d.ts +2 -0
  17. package/dist/types/chat/compaction-summary-message.d.ts +1 -0
  18. package/dist/types/chat/execution-shared.d.ts +6 -5
  19. package/dist/types/chat/fence-figure.d.ts +42 -0
  20. package/dist/types/chat/figure-markdown.d.ts +42 -0
  21. package/dist/types/chat/messages.d.ts +1 -1
  22. package/dist/types/chat/svg-figure.d.ts +33 -0
  23. package/dist/types/chat/svg-source.d.ts +49 -0
  24. package/dist/types/chat/table-chart.d.ts +89 -0
  25. package/dist/types/chat/tool-execution.d.ts +2 -0
  26. package/dist/types/chat/user-message.d.ts +15 -2
  27. package/dist/types/chrome/shared.d.ts +9 -0
  28. package/dist/types/chrome/transcript-container.d.ts +6 -0
  29. package/dist/types/components/box.d.ts +1 -0
  30. package/dist/types/components/disclosure.d.ts +1 -0
  31. package/dist/types/components/editor.d.ts +15 -4
  32. package/dist/types/components/form.d.ts +1 -0
  33. package/dist/types/components/image.d.ts +12 -44
  34. package/dist/types/components/key-value-list.d.ts +1 -0
  35. package/dist/types/components/layout/row.d.ts +1 -0
  36. package/dist/types/components/layout/stack.d.ts +1 -0
  37. package/dist/types/components/markdown.d.ts +36 -3
  38. package/dist/types/components/scroll-view.d.ts +1 -0
  39. package/dist/types/components/section.d.ts +1 -0
  40. package/dist/types/components/table.d.ts +1 -0
  41. package/dist/types/components/text.d.ts +1 -0
  42. package/dist/types/components/tree-view.d.ts +7 -0
  43. package/dist/types/components/truncated-text.d.ts +1 -0
  44. package/dist/types/fuzzy.d.ts +29 -1
  45. package/dist/types/key-hint-format.d.ts +1 -1
  46. package/dist/types/keybinding-matchers.d.ts +3 -0
  47. package/dist/types/keybindings.d.ts +1 -1
  48. package/dist/types/kitty-graphics.d.ts +2 -2
  49. package/dist/types/loop-watchdog.d.ts +8 -7
  50. package/dist/types/multiplexers/cmux.d.ts +16 -0
  51. package/dist/types/multiplexers/herdr.d.ts +26 -0
  52. package/dist/types/multiplexers/notify.d.ts +13 -0
  53. package/dist/types/multiplexers/orca.d.ts +12 -0
  54. package/dist/types/multiplexers/screen.d.ts +9 -0
  55. package/dist/types/multiplexers/tmux.d.ts +29 -0
  56. package/dist/types/multiplexers/types.d.ts +100 -0
  57. package/dist/types/multiplexers/wmux.d.ts +12 -0
  58. package/dist/types/multiplexers/zellij.d.ts +12 -0
  59. package/dist/types/native/backend.d.ts +3 -2
  60. package/dist/types/native/blobs.d.ts +7 -3
  61. package/dist/types/native/encode.d.ts +3 -4
  62. package/dist/types/native/overlay.d.ts +5 -0
  63. package/dist/types/overlays/agent-hub-projection.d.ts +6 -5
  64. package/dist/types/overlays/agent-hub-types.d.ts +9 -0
  65. package/dist/types/overlays/agent-hub.d.ts +2 -0
  66. package/dist/types/overlays/agent-transcript-viewer.d.ts +11 -2
  67. package/dist/types/overlays/agents-hub.d.ts +9 -0
  68. package/dist/types/overlays/annotation-overlay.d.ts +19 -2
  69. package/dist/types/overlays/annotation-types.d.ts +2 -0
  70. package/dist/types/overlays/ask-dialog.d.ts +5 -2
  71. package/dist/types/overlays/history-search.d.ts +10 -0
  72. package/dist/types/overlays/hook-selector.d.ts +9 -4
  73. package/dist/types/overlays/jobs-panel.d.ts +1 -7
  74. package/dist/types/overlays/model-browser.d.ts +54 -7
  75. package/dist/types/overlays/model-hub.d.ts +23 -0
  76. package/dist/types/overlays/oauth-selector.d.ts +0 -1
  77. package/dist/types/overlays/plan-review-overlay.d.ts +3 -2
  78. package/dist/types/overlays/session-observer-registry.d.ts +2 -0
  79. package/dist/types/overlays/session-selector.d.ts +6 -2
  80. package/dist/types/overlays/settings-defs.d.ts +1 -0
  81. package/dist/types/overlays/usage-dashboard.d.ts +8 -4
  82. package/dist/types/overlays/usage-display.d.ts +3 -1
  83. package/dist/types/prompt/composer.d.ts +1 -1
  84. package/dist/types/prompt/custom-editor.d.ts +11 -8
  85. package/dist/types/prompt/image-source.d.ts +2 -1
  86. package/dist/types/prompt/interactive-context-helpers.d.ts +4 -1
  87. package/dist/types/prompt/model-mention-autocomplete.d.ts +1 -1
  88. package/dist/types/prompt/welcome.d.ts +6 -1
  89. package/dist/types/render/composite.d.ts +2 -0
  90. package/dist/types/render/output-pane.d.ts +1 -0
  91. package/dist/types/render/render-utils.d.ts +1 -0
  92. package/dist/types/render/tool-card.d.ts +6 -0
  93. package/dist/types/render/width-aware-text.d.ts +2 -0
  94. package/dist/types/setup/scenes/types.d.ts +1 -0
  95. package/dist/types/status-line/component.d.ts +3 -0
  96. package/dist/types/status-line/host.d.ts +4 -1
  97. package/dist/types/status-line/segments.d.ts +2 -0
  98. package/dist/types/status-line/types.d.ts +14 -3
  99. package/dist/types/terminal-capabilities.d.ts +27 -35
  100. package/dist/types/terminal-multiplexer.d.ts +127 -14
  101. package/dist/types/terminal.d.ts +22 -6
  102. package/dist/types/theme/active-symbols.d.ts +1 -1
  103. package/dist/types/theme/schema.d.ts +11 -2
  104. package/dist/types/theme/symbols.d.ts +1 -1
  105. package/dist/types/theme/theme-class.d.ts +7 -0
  106. package/dist/types/theme/theme.d.ts +1 -1
  107. package/dist/types/theme/tui-adapters.d.ts +3 -0
  108. package/dist/types/tmux.d.ts +11 -4
  109. package/dist/types/tools/daemon.d.ts +1 -1
  110. package/dist/types/tools/eval.d.ts +22 -3
  111. package/dist/types/tools/glob.d.ts +4 -0
  112. package/dist/types/tools/native-view.d.ts +47 -0
  113. package/dist/types/tools/output-meta.d.ts +11 -1
  114. package/dist/types/tools/renderer.d.ts +37 -2
  115. package/dist/types/tools/streaming-output.d.ts +20 -1
  116. package/dist/types/tools/task.d.ts +0 -4
  117. package/dist/types/tools/write.d.ts +9 -2
  118. package/dist/types/tools/yield.d.ts +22 -0
  119. package/dist/types/tui.d.ts +31 -0
  120. package/dist/types/utils.d.ts +6 -0
  121. package/dist/types/vim.d.ts +19 -5
  122. package/package.json +13 -9
  123. package/src/app-keybindings.d.ts +1 -1
  124. package/src/app-keybindings.ts +1 -1
  125. package/src/apps/autoresearch-dashboard.ts +7 -3
  126. package/src/apps/cleanse-board.ts +2 -2
  127. package/src/apps/cleanse-picker.ts +1 -1
  128. package/src/apps/debug/log-viewer.ts +188 -72
  129. package/src/apps/debug/raw-sse-buffer.ts +8 -0
  130. package/src/apps/debug/raw-sse.ts +39 -5
  131. package/src/apps/git/diff-pane.ts +26 -5
  132. package/src/apps/git/git-tui.ts +2 -2
  133. package/src/apps/git/help.ts +29 -20
  134. package/src/apps/if-bench-board.ts +2 -2
  135. package/src/apps/ps-top.ts +2 -2
  136. package/src/apps/setup-model-picker.ts +1 -1
  137. package/src/autocomplete.d.ts +5 -2
  138. package/src/autocomplete.ts +67 -43
  139. package/src/charts/chart-plan.d.ts +674 -0
  140. package/src/charts/chart-plan.ts +5081 -0
  141. package/src/charts/chart-svg.d.ts +59 -0
  142. package/src/charts/chart-svg.ts +4657 -0
  143. package/src/charts/table-data.d.ts +175 -0
  144. package/src/charts/table-data.ts +1478 -0
  145. package/src/chat/advisor-message.ts +11 -0
  146. package/src/chat/assistant-message.d.ts +5 -0
  147. package/src/chat/assistant-message.ts +219 -50
  148. package/src/chat/chat-transcript-builder.d.ts +2 -0
  149. package/src/chat/chat-transcript-builder.ts +5 -3
  150. package/src/chat/compaction-summary-message.d.ts +1 -0
  151. package/src/chat/compaction-summary-message.ts +8 -0
  152. package/src/chat/execution-shared.d.ts +6 -5
  153. package/src/chat/execution-shared.ts +42 -25
  154. package/src/chat/fence-figure.d.ts +42 -0
  155. package/src/chat/fence-figure.ts +80 -0
  156. package/src/chat/figure-markdown.d.ts +42 -0
  157. package/src/chat/figure-markdown.ts +174 -0
  158. package/src/chat/messages.d.ts +1 -1
  159. package/src/chat/messages.ts +1 -1
  160. package/src/chat/read-tool-group.ts +6 -4
  161. package/src/chat/skill-message.ts +6 -0
  162. package/src/chat/svg-figure.d.ts +33 -0
  163. package/src/chat/svg-figure.ts +399 -0
  164. package/src/chat/svg-source.d.ts +49 -0
  165. package/src/chat/svg-source.ts +188 -0
  166. package/src/chat/table-chart.d.ts +89 -0
  167. package/src/chat/table-chart.ts +294 -0
  168. package/src/chat/tool-execution.d.ts +2 -0
  169. package/src/chat/tool-execution.ts +80 -6
  170. package/src/chat/user-message.d.ts +15 -2
  171. package/src/chat/user-message.ts +55 -3
  172. package/src/chrome/shared.ts +53 -0
  173. package/src/chrome/transcript-container.d.ts +6 -0
  174. package/src/chrome/transcript-container.ts +93 -13
  175. package/src/components/box.d.ts +1 -0
  176. package/src/components/box.ts +5 -0
  177. package/src/components/disclosure.d.ts +1 -0
  178. package/src/components/disclosure.ts +8 -0
  179. package/src/components/editor.d.ts +15 -4
  180. package/src/components/editor.ts +294 -46
  181. package/src/components/form.d.ts +1 -0
  182. package/src/components/form.ts +8 -0
  183. package/src/components/image.d.ts +12 -44
  184. package/src/components/image.ts +47 -151
  185. package/src/components/key-value-list.d.ts +1 -0
  186. package/src/components/key-value-list.ts +4 -0
  187. package/src/components/layout/row.d.ts +1 -0
  188. package/src/components/layout/row.ts +7 -0
  189. package/src/components/layout/stack.d.ts +1 -0
  190. package/src/components/layout/stack.ts +7 -0
  191. package/src/components/markdown.d.ts +36 -3
  192. package/src/components/markdown.ts +433 -318
  193. package/src/components/scroll-view.d.ts +1 -0
  194. package/src/components/scroll-view.ts +5 -0
  195. package/src/components/section.d.ts +1 -0
  196. package/src/components/section.ts +5 -0
  197. package/src/components/settings-list.ts +2 -0
  198. package/src/components/table.d.ts +1 -0
  199. package/src/components/table.ts +4 -0
  200. package/src/components/text.d.ts +1 -0
  201. package/src/components/text.ts +7 -0
  202. package/src/components/tree-view.d.ts +7 -0
  203. package/src/components/tree-view.ts +38 -8
  204. package/src/components/truncated-text.d.ts +1 -0
  205. package/src/components/truncated-text.ts +5 -0
  206. package/src/desktop-notify.ts +1 -1
  207. package/src/fuzzy.d.ts +29 -1
  208. package/src/fuzzy.ts +115 -30
  209. package/src/key-hint-format.d.ts +1 -1
  210. package/src/key-hint-format.ts +1 -1
  211. package/src/keybinding-matchers.d.ts +3 -0
  212. package/src/keybinding-matchers.ts +6 -2
  213. package/src/keybindings.d.ts +1 -1
  214. package/src/keybindings.ts +1 -1
  215. package/src/kitty-graphics.d.ts +2 -2
  216. package/src/kitty-graphics.ts +4 -4
  217. package/src/loop-watchdog.d.ts +8 -7
  218. package/src/loop-watchdog.ts +18 -13
  219. package/src/multiplexers/cmux.d.ts +16 -0
  220. package/src/multiplexers/cmux.ts +29 -0
  221. package/src/multiplexers/herdr.d.ts +26 -0
  222. package/src/multiplexers/herdr.ts +64 -0
  223. package/src/multiplexers/notify.d.ts +13 -0
  224. package/src/multiplexers/notify.ts +24 -0
  225. package/src/multiplexers/orca.d.ts +12 -0
  226. package/src/multiplexers/orca.ts +16 -0
  227. package/src/multiplexers/screen.d.ts +9 -0
  228. package/src/multiplexers/screen.ts +15 -0
  229. package/src/multiplexers/tmux.d.ts +29 -0
  230. package/src/multiplexers/tmux.ts +40 -0
  231. package/src/multiplexers/types.d.ts +100 -0
  232. package/src/multiplexers/types.ts +110 -0
  233. package/src/multiplexers/wmux.d.ts +12 -0
  234. package/src/multiplexers/wmux.ts +16 -0
  235. package/src/multiplexers/zellij.d.ts +12 -0
  236. package/src/multiplexers/zellij.ts +24 -0
  237. package/src/native/backend.d.ts +3 -2
  238. package/src/native/backend.ts +174 -18
  239. package/src/native/blobs.d.ts +7 -3
  240. package/src/native/blobs.ts +47 -12
  241. package/src/native/encode.d.ts +3 -4
  242. package/src/native/encode.ts +58 -31
  243. package/src/native/overlay.d.ts +5 -0
  244. package/src/native/overlay.ts +13 -0
  245. package/src/native/reconcile.ts +4 -0
  246. package/src/overlays/agent-hub-projection.d.ts +6 -5
  247. package/src/overlays/agent-hub-projection.ts +124 -13
  248. package/src/overlays/agent-hub-types.d.ts +9 -0
  249. package/src/overlays/agent-hub-types.ts +6 -0
  250. package/src/overlays/agent-hub.ts +44 -29
  251. package/src/overlays/agent-transcript-viewer.ts +91 -23
  252. package/src/overlays/agents-hub.ts +28 -8
  253. package/src/overlays/annotation-overlay.ts +293 -90
  254. package/src/overlays/annotation-types.ts +2 -0
  255. package/src/overlays/ask-dialog.ts +7 -10
  256. package/src/overlays/btw-history-panel.ts +12 -1
  257. package/src/overlays/copy-selector.ts +30 -8
  258. package/src/overlays/extensions/state-manager.ts +52 -12
  259. package/src/overlays/history-search.ts +100 -7
  260. package/src/overlays/hook-selector.ts +12 -5
  261. package/src/overlays/jobs-panel.ts +28 -46
  262. package/src/overlays/model-browser.ts +341 -110
  263. package/src/overlays/model-hub.ts +300 -49
  264. package/src/overlays/move-overlay.ts +28 -14
  265. package/src/overlays/oauth-selector.ts +54 -70
  266. package/src/overlays/plan-review-overlay.ts +194 -70
  267. package/src/overlays/plugin-settings.ts +3 -3
  268. package/src/overlays/report-panel.ts +2 -2
  269. package/src/overlays/rewind-selector.ts +86 -17
  270. package/src/overlays/session-observer-registry.d.ts +2 -0
  271. package/src/overlays/session-observer-registry.ts +10 -2
  272. package/src/overlays/session-selector.ts +64 -26
  273. package/src/overlays/settings-defs.ts +4 -3
  274. package/src/overlays/settings-selector.ts +33 -8
  275. package/src/overlays/snapcompact-shape-preview.ts +2 -3
  276. package/src/overlays/stats-notice.ts +1 -1
  277. package/src/overlays/tree-selector.ts +41 -20
  278. package/src/overlays/usage-dashboard.ts +100 -50
  279. package/src/overlays/usage-display.ts +10 -4
  280. package/src/prompt/attachment-chips.ts +15 -11
  281. package/src/prompt/composer-attachments.ts +6 -4
  282. package/src/prompt/composer.ts +4 -15
  283. package/src/prompt/custom-editor.ts +54 -26
  284. package/src/prompt/image-source.ts +2 -1
  285. package/src/prompt/interactive-context-helpers.ts +30 -18
  286. package/src/prompt/macos-spelling.ts +7 -0
  287. package/src/prompt/magic-keywords.ts +11 -2
  288. package/src/prompt/model-mention-autocomplete.ts +14 -14
  289. package/src/prompt/tips.txt +6 -5
  290. package/src/prompt/welcome.ts +55 -12
  291. package/src/prompt/word-completion.ts +38 -5
  292. package/src/render/composite.d.ts +2 -0
  293. package/src/render/composite.ts +64 -0
  294. package/src/render/output-pane.d.ts +1 -0
  295. package/src/render/output-pane.ts +6 -0
  296. package/src/render/render-utils.d.ts +1 -0
  297. package/src/render/render-utils.ts +11 -0
  298. package/src/render/tool-card.d.ts +6 -0
  299. package/src/render/tool-card.ts +15 -0
  300. package/src/render/width-aware-text.d.ts +2 -0
  301. package/src/render/width-aware-text.ts +5 -0
  302. package/src/setup/scenes/model.ts +2 -1
  303. package/src/setup/scenes/types.ts +1 -0
  304. package/src/status-line/component.ts +292 -78
  305. package/src/status-line/host.ts +4 -1
  306. package/src/status-line/segments.ts +40 -22
  307. package/src/status-line/types.ts +15 -3
  308. package/src/terminal-capabilities.d.ts +27 -35
  309. package/src/terminal-capabilities.ts +75 -189
  310. package/src/terminal-multiplexer.d.ts +127 -14
  311. package/src/terminal-multiplexer.ts +112 -33
  312. package/src/terminal.d.ts +22 -6
  313. package/src/terminal.ts +139 -37
  314. package/src/theme/active-symbols.d.ts +1 -1
  315. package/src/theme/active-symbols.ts +1 -1
  316. package/src/theme/schema-validation.ts +1 -0
  317. package/src/theme/schema.d.ts +11 -2
  318. package/src/theme/schema.ts +14 -1
  319. package/src/theme/session-color.ts +1 -7
  320. package/src/theme/symbols.d.ts +1 -1
  321. package/src/theme/symbols.ts +6 -1
  322. package/src/theme/theme-class.d.ts +7 -0
  323. package/src/theme/theme-class.ts +26 -1
  324. package/src/theme/theme-schema.json +5 -1
  325. package/src/theme/theme.d.ts +1 -1
  326. package/src/theme/theme.ts +8 -4
  327. package/src/theme/tui-adapters.d.ts +3 -0
  328. package/src/theme/tui-adapters.ts +68 -0
  329. package/src/tmux.d.ts +11 -4
  330. package/src/tmux.ts +47 -11
  331. package/src/tools/ast-edit.ts +59 -35
  332. package/src/tools/ast-grep.ts +35 -16
  333. package/src/tools/bash.ts +92 -42
  334. package/src/tools/daemon.d.ts +1 -1
  335. package/src/tools/daemon.ts +1 -1
  336. package/src/tools/default-renderer.ts +35 -19
  337. package/src/tools/edit.ts +142 -120
  338. package/src/tools/eval-format/index.ts +28 -2
  339. package/src/tools/eval-format/javascript.ts +7 -5
  340. package/src/tools/eval.d.ts +22 -3
  341. package/src/tools/eval.ts +364 -208
  342. package/src/tools/fetch.ts +5 -6
  343. package/src/tools/glob.d.ts +4 -0
  344. package/src/tools/glob.ts +33 -65
  345. package/src/tools/grep.ts +81 -48
  346. package/src/tools/index.ts +2 -0
  347. package/src/tools/lsp.ts +81 -59
  348. package/src/tools/mcp.ts +111 -68
  349. package/src/tools/native-view.d.ts +47 -0
  350. package/src/tools/native-view.ts +102 -1
  351. package/src/tools/output-meta.d.ts +11 -1
  352. package/src/tools/output-meta.ts +23 -3
  353. package/src/tools/read.ts +7 -5
  354. package/src/tools/renderer.d.ts +37 -2
  355. package/src/tools/renderer.ts +42 -2
  356. package/src/tools/streaming-output.d.ts +20 -1
  357. package/src/tools/streaming-output.ts +149 -50
  358. package/src/tools/task.d.ts +0 -4
  359. package/src/tools/task.ts +112 -59
  360. package/src/tools/todo.ts +119 -30
  361. package/src/tools/wait.ts +34 -24
  362. package/src/tools/web-search.ts +1 -1
  363. package/src/tools/write.d.ts +9 -2
  364. package/src/tools/write.ts +147 -95
  365. package/src/tools/yield.d.ts +22 -0
  366. package/src/tools/yield.ts +225 -0
  367. package/src/tui.d.ts +31 -0
  368. package/src/tui.ts +309 -160
  369. package/src/utils.d.ts +6 -0
  370. package/src/utils.ts +19 -0
  371. package/src/vim.d.ts +19 -5
  372. package/src/vim.ts +333 -18
  373. package/src/windows-input-mode.ts +8 -0
@@ -13,13 +13,15 @@
13
13
  * Focus regions (`toc`/`body`/`actions`) cycle with Tab/Shift+Tab; arrows move
14
14
  * within the focused region and step left into the sidebar. The default focus is
15
15
  * `actions`, so the muscle memory of the old single-target overlay carries over:
16
- * ↑/↓ select options, Enter confirms, ←/→ drives the slider when there is no
17
- * sidebar, g/G + PgUp/PgDn scroll, and the external-editor key opens the plan.
16
+ * ↑/↓ select options, Enter confirms, ←/→ drives the slider, g/G + PgUp/PgDn
17
+ * scroll, and the external-editor key opens the plan. Under Tern the options
18
+ * are a horizontal bar, so ←/→ select options and Shift+←/→ drive the slider.
18
19
  */
19
20
  import {
20
21
  type Component,
21
22
  Ellipsis,
22
23
  Editor,
24
+ getWidthConfigEpoch,
23
25
  Markdown,
24
26
  type MarkdownTheme,
25
27
  matchesKey,
@@ -31,7 +33,7 @@ import {
31
33
  } from "../index";
32
34
  import { sanitizeText } from "@linxiraos/pi-utils";
33
35
  import { sanitizeStatusText } from "../chrome/shared";
34
- import { getEditorTheme, getMarkdownTheme, theme } from "../theme/theme";
36
+ import { getEditorTheme, getMarkdownTheme, getThemeEpoch, theme } from "../theme/theme";
35
37
  import {
36
38
  matchesAppExternalEditor,
37
39
  matchesSelectCancel,
@@ -58,6 +60,7 @@ import type { KeyName } from "../key-hint-format";
58
60
  import { col, item, keyed, md, node, row as rowNode, span, text } from "../native/describe";
59
61
  import { leafKey, type NativeChild, type NativeNode, type NativeUiEvent } from "../native/node";
60
62
  import { actionButton, actionHint, hintsRow, itemIndex, type NativeHint, selectList } from "../native/overlay";
63
+ import { isNativeRendering } from "../native/state";
61
64
  import { getKeybindings } from "../keybindings";
62
65
 
63
66
  /** Title shown in the overlay's top border. */
@@ -74,6 +77,8 @@ const SIDEBAR_MIN_TOTAL_WIDTH = 64;
74
77
  const SIDEBAR_MIN_BODY_WIDTH = 40;
75
78
  /** Persisted line-context cap; render-time captions clamp again to the viewport. */
76
79
  const MAX_ANNOTATION_CONTEXT_WIDTH = 120;
80
+ /** Oldest delete/annotate undo snapshots are dropped past this depth (each holds the full plan text). */
81
+ const MAX_UNDO_ENTRIES = 100;
77
82
 
78
83
  type Focus = "toc" | "body" | "actions";
79
84
 
@@ -146,6 +151,78 @@ interface UndoEntry {
146
151
  deleted: string[];
147
152
  }
148
153
 
154
+ /** Concatenated plan body rows and their anchors, reused while every render input is unchanged. */
155
+ interface BodyLayout {
156
+ width: number;
157
+ annotationRev: number;
158
+ themeEpoch: number;
159
+ widthEpoch: number;
160
+ /** Each section's `Markdown.render` result the layout was built from (compared by identity). */
161
+ rendered: readonly (readonly string[])[];
162
+ lines: readonly string[];
163
+ anchors: BodyRowAnchor[];
164
+ offsets: number[];
165
+ }
166
+
167
+ /** Line contexts per rendered-lines array; `Markdown.render` returns a stable array per (text, width). */
168
+ const lineContextCache = new WeakMap<readonly string[], readonly LineAnchorContext[]>();
169
+ /** Match keys per context list, so resolving several annotations normalizes each row once. */
170
+ const normalizedContextCache = new WeakMap<readonly LineAnchorContext[], readonly string[]>();
171
+
172
+ function lineContexts(rendered: readonly string[]): readonly LineAnchorContext[] {
173
+ let contexts = lineContextCache.get(rendered);
174
+ if (!contexts) {
175
+ contexts = rendered.map(line => {
176
+ const sanitized = sanitizeStatusText(line);
177
+ const truncated = visibleWidth(sanitized) > MAX_ANNOTATION_CONTEXT_WIDTH;
178
+ const text = truncateToWidth(sanitized, MAX_ANNOTATION_CONTEXT_WIDTH, Ellipsis.Unicode);
179
+ return { text: text || "(blank line)", truncated };
180
+ });
181
+ lineContextCache.set(rendered, contexts);
182
+ }
183
+ return contexts;
184
+ }
185
+
186
+ function normalizeLineContext(context: LineAnchorContext): string {
187
+ const normalized = sanitizeStatusText(context.text).replace(/\s+/g, " ").trim();
188
+ return context.truncated && normalized.endsWith("…") ? normalized.slice(0, -1) : normalized;
189
+ }
190
+
191
+ /** Row in `contexts` nearest `storedRow` whose text matches the stored context, or -1. */
192
+ function resolveLineRow(
193
+ storedRow: number,
194
+ storedContext: LineAnchorContext,
195
+ contexts: readonly LineAnchorContext[],
196
+ ): number {
197
+ if (contexts.length === 0) return -1;
198
+ const targetRow = Math.max(0, Math.floor(storedRow));
199
+ const normalizedStoredContext = normalizeLineContext(storedContext);
200
+ if (!normalizedStoredContext) return -1;
201
+ let normalizedContexts = normalizedContextCache.get(contexts);
202
+ if (!normalizedContexts) {
203
+ normalizedContexts = contexts.map(normalizeLineContext);
204
+ normalizedContextCache.set(contexts, normalizedContexts);
205
+ }
206
+ let best = -1;
207
+ let bestDistance = Number.POSITIVE_INFINITY;
208
+ for (let row = 0; row < normalizedContexts.length; row++) {
209
+ const normalizedContext = normalizedContexts[row]!;
210
+ if (
211
+ normalizedContext !== normalizedStoredContext &&
212
+ !normalizedContext.includes(normalizedStoredContext) &&
213
+ !normalizedStoredContext.includes(normalizedContext)
214
+ ) {
215
+ continue;
216
+ }
217
+ const distance = Math.abs(row - targetRow);
218
+ if (distance < bestDistance) {
219
+ best = row;
220
+ bestDistance = distance;
221
+ }
222
+ }
223
+ return best;
224
+ }
225
+
149
226
  export interface PlanReviewOverlayCallbacks {
150
227
  /** Invoked with the chosen option label (never a disabled one). */
151
228
  onPick: (label: string) => void;
@@ -197,6 +274,10 @@ export class PlanReviewOverlay implements Component {
197
274
  #sectionOffsets: number[] = [];
198
275
  /** Rendered body row to underlying plan row; callouts retain their owner's anchor. */
199
276
  #bodyRowAnchors: BodyRowAnchor[] = [];
277
+ /** Memoized {@link #buildBody} result. */
278
+ #bodyLayout: BodyLayout | undefined;
279
+ /** Lines last handed to the scroll view; unchanged layouts skip the copy and keep its render cache. */
280
+ #scrollLines: readonly string[] | undefined;
200
281
  #undo: UndoEntry[] = [];
201
282
  /** Titles of sections deleted in the overlay, surfaced as Refine feedback. */
202
283
  #deleted: string[] = [];
@@ -442,8 +523,8 @@ export class PlanReviewOverlay implements Component {
442
523
  ) {
443
524
  continue;
444
525
  }
445
- const contexts = section.md.render(MAX_ANNOTATION_CONTEXT_WIDTH).map(line => this.#lineContext(line));
446
- const row = this.#resolveLineRow(
526
+ const contexts = lineContexts(section.md.render(MAX_ANNOTATION_CONTEXT_WIDTH));
527
+ const row = resolveLineRow(
447
528
  entry.target.row,
448
529
  { text: entry.target.context, truncated: entry.target.contextTruncated === true },
449
530
  contexts,
@@ -666,6 +747,10 @@ export class PlanReviewOverlay implements Component {
666
747
  }
667
748
 
668
749
  #handleActions(data: string): void {
750
+ if (isNativeRendering()) {
751
+ this.#handleNativeActions(data);
752
+ return;
753
+ }
669
754
  // Left/right always drive the slider. The sidebar sits beside the body
670
755
  // (above this row), not the slider, so stealing left for it would strand
671
756
  // the operator unable to step the model tier back — reach the ToC via Tab.
@@ -695,6 +780,37 @@ export class PlanReviewOverlay implements Component {
695
780
  this.#handleBodyScroll(data);
696
781
  }
697
782
 
783
+ /** Tern lays the options out as a horizontal decision bar: ←/→ step through
784
+ * it, ↑ returns to the body above, and the model slider moves to Shift+←/→. */
785
+ #handleNativeActions(data: string): void {
786
+ if (matchesKey(data, "shift+left")) {
787
+ this.#moveSlider(-1);
788
+ return;
789
+ }
790
+ if (matchesKey(data, "shift+right")) {
791
+ this.#moveSlider(1);
792
+ return;
793
+ }
794
+ if (matchesKey(data, "left") || matchesKey(data, "h")) {
795
+ this.#moveSelection(-1);
796
+ return;
797
+ }
798
+ if (matchesKey(data, "right") || matchesKey(data, "l")) {
799
+ this.#moveSelection(1);
800
+ return;
801
+ }
802
+ if (matchesSelectUp(data) || matchesKey(data, "k")) {
803
+ this.#setFocus("body");
804
+ return;
805
+ }
806
+ if (matchesSelectDown(data) || matchesKey(data, "j")) return;
807
+ if (matchesKey(data, "enter") || matchesKey(data, "return") || data === "\n") {
808
+ this.#confirmSelection();
809
+ return;
810
+ }
811
+ this.#handleBodyScroll(data);
812
+ }
813
+
698
814
  #handleBody(data: string): void {
699
815
  if (data === "a") {
700
816
  this.#startBodyAnnotate();
@@ -829,6 +945,7 @@ export class PlanReviewOverlay implements Component {
829
945
  }
830
946
 
831
947
  #pushUndo(): void {
948
+ if (this.#undo.length >= MAX_UNDO_ENTRIES) this.#undo.shift();
832
949
  this.#undo.push({
833
950
  text: joinPlanSections(this.#sections),
834
951
  annotations: this.#sections.map(section =>
@@ -922,7 +1039,7 @@ export class PlanReviewOverlay implements Component {
922
1039
  seenRows.add(candidate.row);
923
1040
  sectionAnchors.push({ text: candidate.context, truncated: candidate.contextTruncated });
924
1041
  }
925
- const resolvedRow = this.#resolveLineRow(
1042
+ const resolvedRow = resolveLineRow(
926
1043
  annotation.target.row,
927
1044
  { text: annotation.target.context, truncated: annotation.target.contextTruncated },
928
1045
  sectionAnchors,
@@ -1150,23 +1267,64 @@ export class PlanReviewOverlay implements Component {
1150
1267
 
1151
1268
  #layoutBody(lines: readonly string[], height: number): void {
1152
1269
  this.#captureScrollProgress();
1153
- this.#scrollView.setLines(lines);
1270
+ if (lines !== this.#scrollLines) {
1271
+ this.#scrollView.setLines(lines);
1272
+ this.#scrollLines = lines;
1273
+ }
1154
1274
  this.#scrollView.setHeight(height);
1155
1275
  const maxOffset = this.#scrollView.getMaxScrollOffset();
1156
1276
  if (maxOffset > 0) this.#scrollView.setScrollOffset(Math.round(this.#scrollProgress * maxOffset));
1157
1277
  }
1158
1278
 
1159
- /** Build the concatenated body lines and map each rendered row back to a plan anchor. */
1160
- #buildBody(bodyContentWidth: number): string[] {
1279
+ /**
1280
+ * Build the concatenated body lines and map each rendered row back to a plan
1281
+ * anchor. Memoized on width, annotation revision, theme/width epochs and each
1282
+ * section's rendered-lines identity, so steady frames and scrolling reuse it.
1283
+ */
1284
+ #buildBody(bodyContentWidth: number): readonly string[] {
1285
+ const themeEpoch = getThemeEpoch();
1286
+ const widthEpoch = getWidthConfigEpoch();
1287
+ const cached = this.#bodyLayout;
1288
+ if (
1289
+ cached?.width === bodyContentWidth &&
1290
+ cached.annotationRev === this.#annotationRev &&
1291
+ cached.themeEpoch === themeEpoch &&
1292
+ cached.widthEpoch === widthEpoch &&
1293
+ cached.rendered.length === this.#sections.length &&
1294
+ this.#sections.every((section, i) => section.md.render(bodyContentWidth) === cached.rendered[i])
1295
+ ) {
1296
+ this.#sectionOffsets = cached.offsets;
1297
+ this.#bodyRowAnchors = cached.anchors;
1298
+ return cached.lines;
1299
+ }
1161
1300
  const lines: string[] = [];
1162
1301
  const anchors: BodyRowAnchor[] = [];
1302
+ const renderedSections: (readonly string[])[] = [];
1163
1303
  // oxlint-disable-next-line unicorn/no-new-array -- length preallocation
1164
1304
  const offsets: number[] = new Array(this.#sections.length);
1165
1305
  for (let sectionIndex = 0; sectionIndex < this.#sections.length; sectionIndex++) {
1166
1306
  const section = this.#sections[sectionIndex]!;
1167
1307
  offsets[sectionIndex] = lines.length;
1168
1308
  const rendered = section.md.render(bodyContentWidth);
1169
- const contexts = rendered.map(line => this.#lineContext(line));
1309
+ renderedSections.push(rendered);
1310
+ const contexts = lineContexts(rendered);
1311
+ // Resolve each note's row once; notes on one row keep their array order.
1312
+ let notesByRow: Map<number, string[]> | undefined;
1313
+ for (const annotation of section.annotations) {
1314
+ const annotationRow =
1315
+ annotation.target.kind === "section"
1316
+ ? 0
1317
+ : resolveLineRow(
1318
+ annotation.target.row,
1319
+ { text: annotation.target.context, truncated: annotation.target.contextTruncated },
1320
+ contexts,
1321
+ );
1322
+ if (annotationRow < 0) continue;
1323
+ notesByRow ??= new Map();
1324
+ const notes = notesByRow.get(annotationRow);
1325
+ if (notes) notes.push(annotation.note);
1326
+ else notesByRow.set(annotationRow, [annotation.note]);
1327
+ }
1170
1328
  for (let row = 0; row < rendered.length; row++) {
1171
1329
  const context = contexts[row]!;
1172
1330
  const anchor = {
@@ -1177,67 +1335,24 @@ export class PlanReviewOverlay implements Component {
1177
1335
  };
1178
1336
  lines.push(rendered[row]!);
1179
1337
  anchors.push(anchor);
1180
- for (const annotation of section.annotations) {
1181
- const annotationRow =
1182
- annotation.target.kind === "section"
1183
- ? 0
1184
- : this.#resolveLineRow(
1185
- annotation.target.row,
1186
- {
1187
- text: annotation.target.context,
1188
- truncated: annotation.target.contextTruncated,
1189
- },
1190
- contexts,
1191
- );
1192
- if (annotationRow === row) {
1193
- this.#appendAnnotationCallout(lines, anchors, annotation.note, anchor, bodyContentWidth);
1194
- }
1195
- }
1338
+ const notes = notesByRow?.get(row);
1339
+ if (!notes) continue;
1340
+ for (const note of notes) this.#appendAnnotationCallout(lines, anchors, note, anchor, bodyContentWidth);
1196
1341
  }
1197
1342
  }
1198
1343
  this.#sectionOffsets = offsets;
1199
1344
  this.#bodyRowAnchors = anchors;
1200
- return lines;
1201
- }
1202
-
1203
- #lineContext(line: string): LineAnchorContext {
1204
- const sanitized = sanitizeStatusText(line);
1205
- const truncated = visibleWidth(sanitized) > MAX_ANNOTATION_CONTEXT_WIDTH;
1206
- const text = truncateToWidth(sanitized, MAX_ANNOTATION_CONTEXT_WIDTH, Ellipsis.Unicode);
1207
- return { text: text || "(blank line)", truncated };
1208
- }
1209
-
1210
- #resolveLineRow(
1211
- storedRow: number,
1212
- storedContext: LineAnchorContext,
1213
- contexts: readonly LineAnchorContext[],
1214
- ): number {
1215
- if (contexts.length === 0) return -1;
1216
- const targetRow = Math.max(0, Math.floor(storedRow));
1217
- const normalize = (context: LineAnchorContext): string => {
1218
- const normalized = sanitizeStatusText(context.text).replace(/\s+/g, " ").trim();
1219
- return context.truncated && normalized.endsWith("…") ? normalized.slice(0, -1) : normalized;
1345
+ this.#bodyLayout = {
1346
+ width: bodyContentWidth,
1347
+ annotationRev: this.#annotationRev,
1348
+ themeEpoch,
1349
+ widthEpoch,
1350
+ rendered: renderedSections,
1351
+ lines,
1352
+ anchors,
1353
+ offsets,
1220
1354
  };
1221
- const normalizedStoredContext = normalize(storedContext);
1222
- if (!normalizedStoredContext) return -1;
1223
- let best = -1;
1224
- let bestDistance = Number.POSITIVE_INFINITY;
1225
- for (let row = 0; row < contexts.length; row++) {
1226
- const normalizedContext = normalize(contexts[row]!);
1227
- if (
1228
- normalizedContext !== normalizedStoredContext &&
1229
- !normalizedContext.includes(normalizedStoredContext) &&
1230
- !normalizedStoredContext.includes(normalizedContext)
1231
- ) {
1232
- continue;
1233
- }
1234
- const distance = Math.abs(row - targetRow);
1235
- if (distance < bestDistance) {
1236
- best = row;
1237
- bestDistance = distance;
1238
- }
1239
- }
1240
- return best;
1355
+ return lines;
1241
1356
  }
1242
1357
 
1243
1358
  #appendAnnotationCallout(
@@ -1492,7 +1607,7 @@ export class PlanReviewOverlay implements Component {
1492
1607
  return this.#nativeRoot;
1493
1608
  }
1494
1609
 
1495
- /** Head tools: Copy `c` and the external editor, mirroring their keys. */
1610
+ /** Head tools: Copy `c`, the external editor and Cancel, mirroring their keys. */
1496
1611
  #describeTools(): NativeNode | undefined {
1497
1612
  if (this.#committed) return undefined;
1498
1613
  const buttons: NativeNode[] = [];
@@ -1503,6 +1618,12 @@ export class PlanReviewOverlay implements Component {
1503
1618
  if (this.callbacks.onExternalEditor) {
1504
1619
  buttons.push(actionButton("Edit in $EDITOR", "externalEditor", editorKeyId ? { keys: editorKeyId } : {}));
1505
1620
  }
1621
+ // Cancel the review the way the cancel key does; while annotating that key
1622
+ // only leaves the draft/chooser, so the button is hidden there.
1623
+ if (!this.#annotating && !this.#annotationChooser) {
1624
+ const cancelKeyId = getKeybindings().getKeys("tui.select.cancel")[0];
1625
+ buttons.push(actionButton("Cancel", "cancel", cancelKeyId ? { keys: cancelKeyId } : {}));
1626
+ }
1506
1627
  if (buttons.length === 0) return undefined;
1507
1628
  return node("row", { role: "zeta.plan.tools", gap: "sm", align: "center", justify: "end" }, buttons, "tools");
1508
1629
  }
@@ -1510,12 +1631,15 @@ export class PlanReviewOverlay implements Component {
1510
1631
  handleNativeEvent(event: NativeUiEvent): void {
1511
1632
  if (this.#committed) return;
1512
1633
  if (event.type === "action") {
1513
- // Head tools run exactly what `c` and the external-editor key run.
1634
+ // Head tools run exactly what `c`, the external-editor key and Esc run.
1514
1635
  if (this.#annotating || this.#annotationChooser) return;
1515
1636
  if (event.act === "copyPlan" && this.callbacks.onCopyPlan) {
1516
1637
  void this.callbacks.onCopyPlan(joinPlanSections(this.#sections));
1517
1638
  } else if (event.act === "externalEditor") {
1518
1639
  this.callbacks.onExternalEditor?.();
1640
+ } else if (event.act === "cancel") {
1641
+ this.#committed = true;
1642
+ this.callbacks.onCancel();
1519
1643
  }
1520
1644
  return;
1521
1645
  }
@@ -1686,8 +1810,8 @@ export class PlanReviewOverlay implements Component {
1686
1810
  const hints: (NativeHint | undefined)[] = [];
1687
1811
  switch (this.#focus) {
1688
1812
  case "actions":
1689
- hints.push(upDown("select"), key("enter", "confirm"));
1690
- if (this.#slider) hints.push(key(["left", "right"], "model"));
1813
+ hints.push(key(["left", "right"], "select"), key("enter", "confirm"));
1814
+ if (this.#slider) hints.push(key(["shift+left", "shift+right"], "model"));
1691
1815
  break;
1692
1816
  case "toc":
1693
1817
  hints.push(
@@ -378,7 +378,7 @@ export class PluginListComponent extends OverlayPanel {
378
378
  lead:
379
379
  rows.length > 0
380
380
  ? "Plugins installed for you and this project. Configure one to turn it or its features on and off."
381
- : "No plugins installed. Install one with omp plugin install <package>, or <name>@<marketplace>.",
381
+ : "No plugins installed. Install one with zetacode plugin install <package>, or <name>@<marketplace>.",
382
382
  sections: rows.length > 0 ? [{ id: "installed", title: "Installed", rows }] : [],
383
383
  focus: this.#selectList.getSelectedItem()?.value ?? null,
384
384
  editing: null,
@@ -403,10 +403,10 @@ export class PluginListComponent extends OverlayPanel {
403
403
  text([span("No plugins installed", "muted")]),
404
404
  node("kv", {
405
405
  items: [
406
- { k: "Install npm plugins", v: [span("omp plugin install <package>", "code")] },
406
+ { k: "Install npm plugins", v: [span("zetacode plugin install <package>", "code")] },
407
407
  {
408
408
  k: "Install marketplace plugins",
409
- v: [span("omp plugin install <name>@<marketplace>", "code")],
409
+ v: [span("zetacode plugin install <name>@<marketplace>", "code")],
410
410
  },
411
411
  ],
412
412
  }),
@@ -72,9 +72,9 @@ export class ReportPanel extends OverlayPanel {
72
72
  #native: { scroll: NativeScroll | undefined; node: NativeNode } | undefined;
73
73
 
74
74
  constructor(options: ReportPanelOptions) {
75
- super(options.title, "omp.overlay.report");
75
+ super(options.title, "zeta.overlay.report");
76
76
  this.nativeOverlay = {
77
- role: "omp.overlay.report",
77
+ role: "zeta.overlay.report",
78
78
  size: "lg",
79
79
  anchor: "center",
80
80
  head: options.head ?? options.title,
@@ -158,6 +158,14 @@ const STRIP_GAP = 2;
158
158
  /** Duration of the branch-swap camera slide. */
159
159
  const SLIDE_MS = 160;
160
160
 
161
+ /** Same length and identical elements (or both undefined). */
162
+ function sameElements<T>(a: readonly T[] | undefined, b: readonly T[] | undefined): boolean {
163
+ if (a === b) return true;
164
+ if (a === undefined || b === undefined || a.length !== b.length) return false;
165
+ for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;
166
+ return true;
167
+ }
168
+
161
169
  export class RewindSelectorComponent implements Component {
162
170
  #builder: ChatTranscriptBuilder;
163
171
  #browser: TranscriptBrowser;
@@ -196,6 +204,24 @@ export class RewindSelectorComponent implements Component {
196
204
  #rowText = new WeakMap<readonly string[], string>();
197
205
  /** Native filter haystack: lowercased turn text per main target (no rendered rows under TSP). */
198
206
  #nativeTexts: { targets: OutlineTarget[]; texts: string[] } | undefined;
207
+ /** Compiled word patterns of the last filter query. */
208
+ #filterPatterns: { query: string; patterns: RegExp[] } | undefined;
209
+ /**
210
+ * Last {@link #filterMatches} result and its inputs. Frames hand back fresh
211
+ * outer `mainRows`/`mainVisible` arrays over cached per-child rows, so those
212
+ * compare element-wise.
213
+ */
214
+ #filterMemo:
215
+ | {
216
+ query: string;
217
+ targets: OutlineTarget[];
218
+ expanded: boolean;
219
+ native: boolean;
220
+ mainRows: readonly (readonly string[])[];
221
+ mainVisible: readonly boolean[] | undefined;
222
+ matches: readonly number[];
223
+ }
224
+ | undefined;
199
225
  /** Last described bar and the state it was built from. */
200
226
  #bar: { memo: string; targets: OutlineTarget[]; node: NativeNode } | undefined;
201
227
 
@@ -529,14 +555,25 @@ export class RewindSelectorComponent implements Component {
529
555
  * Matching the rendered rows keeps results honest: collapsed tool output
530
556
  * only matches once Ctrl+O expands it.
531
557
  */
532
- #filterMatches(): number[] {
533
- const words = (this.#filter ?? "").toLowerCase().split(/\s+/).filter(Boolean);
534
- const patterns = words.map(word =>
535
- /^[\p{Script=Latin}\p{N}_]+$/u.test(word)
536
- ? new RegExp(`(?<![\\p{L}\\p{N}_])${RegExp.escape(word)}(?![\\p{L}\\p{N}_])`, "u")
537
- : new RegExp(RegExp.escape(word), "u"),
538
- );
539
- if (isNativeRendering() && this.#nativeTexts?.targets !== this.#targets) {
558
+ #filterMatches(): readonly number[] {
559
+ const query = this.#filter ?? "";
560
+ const native = isNativeRendering();
561
+ const memo = this.#filterMemo;
562
+ if (
563
+ memo &&
564
+ memo.query === query &&
565
+ memo.targets === this.#targets &&
566
+ memo.expanded === this.#expanded &&
567
+ memo.native === native &&
568
+ sameElements(memo.mainVisible, this.#mainVisible) &&
569
+ (native || sameElements(memo.mainRows, this.#mainRows))
570
+ ) {
571
+ memo.mainRows = this.#mainRows;
572
+ memo.mainVisible = this.#mainVisible;
573
+ return memo.matches;
574
+ }
575
+ const patterns = this.#compileFilter(query);
576
+ if (native && this.#nativeTexts?.targets !== this.#targets) {
540
577
  this.#nativeTexts = {
541
578
  targets: this.#targets,
542
579
  // Turn text plus its commands and tool output, like the expanded rendered rows.
@@ -551,21 +588,53 @@ export class RewindSelectorComponent implements Component {
551
588
  for (let index = 0; index < this.#targets.length; index++) {
552
589
  if (!this.#isMainSelectable(index)) continue;
553
590
  const target = this.#targets[index]!;
554
- const texts = isNativeRendering()
555
- ? [this.#nativeTexts?.texts[index] ?? ""]
556
- : this.#mainRows.slice(target.start, target.end).map(rows => {
557
- let text = this.#rowText.get(rows);
558
- if (text === undefined) {
559
- text = Bun.stripANSI(rows.join("\n")).toLowerCase();
560
- this.#rowText.set(rows, text);
591
+ const matched = native
592
+ ? patterns.every(pattern => pattern.test(this.#nativeTexts?.texts[index] ?? ""))
593
+ : patterns.every(pattern => {
594
+ for (let child = target.start; child < target.end; child++) {
595
+ const rows = this.#mainRows[child];
596
+ if (rows !== undefined && pattern.test(this.#plainRowText(rows))) return true;
561
597
  }
562
- return text;
598
+ return false;
563
599
  });
564
- if (patterns.every(pattern => texts.some(text => pattern.test(text)))) matches.push(index);
600
+ if (matched) matches.push(index);
565
601
  }
602
+ this.#filterMemo = {
603
+ query,
604
+ targets: this.#targets,
605
+ expanded: this.#expanded,
606
+ native,
607
+ mainRows: this.#mainRows,
608
+ mainVisible: this.#mainVisible,
609
+ matches,
610
+ };
566
611
  return matches;
567
612
  }
568
613
 
614
+ /** Word patterns of `query`, compiled once per distinct query. */
615
+ #compileFilter(query: string): RegExp[] {
616
+ const cached = this.#filterPatterns;
617
+ if (cached?.query === query) return cached.patterns;
618
+ const words = query.toLowerCase().split(/\s+/).filter(Boolean);
619
+ const patterns = words.map(word =>
620
+ /^[\p{Script=Latin}\p{N}_]+$/u.test(word)
621
+ ? new RegExp(`(?<![\\p{L}\\p{N}_])${RegExp.escape(word)}(?![\\p{L}\\p{N}_])`, "u")
622
+ : new RegExp(RegExp.escape(word), "u"),
623
+ );
624
+ this.#filterPatterns = { query, patterns };
625
+ return patterns;
626
+ }
627
+
628
+ /** Lowercased plain text of one rendered child row array, cached by array identity. */
629
+ #plainRowText(rows: readonly string[]): string {
630
+ let text = this.#rowText.get(rows);
631
+ if (text === undefined) {
632
+ text = Bun.stripANSI(rows.join("\n")).toLowerCase();
633
+ this.#rowText.set(rows, text);
634
+ }
635
+ return text;
636
+ }
637
+
569
638
  /** Up/Down: step within the active column; leaving a sibling column's top exits the strip. */
570
639
  #moveVertical(delta: -1 | 1): void {
571
640
  if (this.#activeVariant > 0) {
@@ -58,6 +58,8 @@ export interface ObservableSession {
58
58
  lastUpdate: number;
59
59
  /** Latest progress snapshot from the subagent executor */
60
60
  progress?: AgentProgress;
61
+ /** `Date.now()` when {@link progress} arrived; its `durationMs` was current then. */
62
+ progressAt?: number;
61
63
  }
62
64
  /** Coarse source of an observer change; callers use it to separate lifecycle work from high-frequency progress. */
63
65
  export type SessionObserverChangeKind = "main" | "reset" | "lifecycle" | "progress";
@@ -64,6 +64,8 @@ export interface ObservableSession {
64
64
  lastUpdate: number;
65
65
  /** Latest progress snapshot from the subagent executor */
66
66
  progress?: AgentProgress;
67
+ /** `Date.now()` when {@link progress} arrived; its `durationMs` was current then. */
68
+ progressAt?: number;
67
69
  }
68
70
 
69
71
  /** Coarse source of an observer change; callers use it to separate lifecycle work from high-frequency progress. */
@@ -225,6 +227,9 @@ export class SessionObserverRegistry {
225
227
  existing.detached = payload.detached ?? existing.detached;
226
228
  if (payload.description) existing.description = payload.description;
227
229
  if (payload.sessionFile) existing.sessionFile = payload.sessionFile;
230
+ // A kept-alive or parked agent's next turn restarts its clock; the prior turn's
231
+ // snapshot must not be aged across the idle gap before the new turn's first frame.
232
+ if (payload.status === "started") existing.progressAt = undefined;
228
233
  } else {
229
234
  this.#sessions.set(payload.id, {
230
235
  id: payload.id,
@@ -256,12 +261,14 @@ export class SessionObserverRegistry {
256
261
 
257
262
  const sortOrder = this.#ensureSortOrder(id);
258
263
  this.#ensureParentSortOrder(payload.parentToolCallId, sortOrder);
264
+ const now = Date.now();
259
265
  if (existing) {
260
- existing.lastUpdate = Date.now();
266
+ existing.lastUpdate = now;
261
267
  existing.index = payload.index;
262
268
  existing.parentToolCallId = payload.parentToolCallId ?? existing.parentToolCallId;
263
269
  existing.detached = payload.detached ?? existing.detached;
264
270
  existing.progress = progress;
271
+ existing.progressAt = now;
265
272
  if (progress.description) existing.description = progress.description;
266
273
  if (payload.sessionFile) existing.sessionFile = payload.sessionFile;
267
274
  } else {
@@ -276,8 +283,9 @@ export class SessionObserverRegistry {
276
283
  parentToolCallId: payload.parentToolCallId,
277
284
  detached: payload.detached,
278
285
  index: payload.index,
279
- lastUpdate: Date.now(),
286
+ lastUpdate: now,
280
287
  progress,
288
+ progressAt: now,
281
289
  });
282
290
  }
283
291
  this.#notifyListeners("progress");