@linxiraos/pi-tui 1.1.26 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (423) hide show
  1. package/CHANGELOG.md +51 -491
  2. package/README.md +13 -7
  3. package/dist/types/active-terminal.d.ts +30 -0
  4. package/dist/types/app-keybindings.d.ts +7 -2
  5. package/dist/types/apps/cleanse-board.d.ts +1 -1
  6. package/dist/types/apps/debug/log-viewer.d.ts +4 -0
  7. package/dist/types/apps/debug/raw-sse-buffer.d.ts +2 -0
  8. package/dist/types/apps/git/git-tui.d.ts +5 -4
  9. package/dist/types/apps/git/help.d.ts +3 -1
  10. package/dist/types/apps/if-bench-board.d.ts +2 -2
  11. package/dist/types/apps/setup-model-picker.d.ts +1 -1
  12. package/dist/types/autocomplete.d.ts +5 -2
  13. package/dist/types/charts/chart-plan.d.ts +674 -0
  14. package/dist/types/charts/chart-svg.d.ts +59 -0
  15. package/dist/types/charts/table-data.d.ts +175 -0
  16. package/dist/types/chat/assistant-message.d.ts +11 -1
  17. package/dist/types/chat/chat-transcript-builder.d.ts +3 -0
  18. package/dist/types/chat/compaction-summary-message.d.ts +1 -0
  19. package/dist/types/chat/execution-shared.d.ts +6 -5
  20. package/dist/types/chat/fence-figure.d.ts +42 -0
  21. package/dist/types/chat/figure-markdown.d.ts +42 -0
  22. package/dist/types/chat/messages.d.ts +1 -1
  23. package/dist/types/chat/svg-figure.d.ts +33 -0
  24. package/dist/types/chat/svg-source.d.ts +49 -0
  25. package/dist/types/chat/table-chart.d.ts +89 -0
  26. package/dist/types/chat/tool-execution.d.ts +2 -0
  27. package/dist/types/chat/user-message.d.ts +17 -4
  28. package/dist/types/chrome/shared.d.ts +9 -0
  29. package/dist/types/chrome/status-notice.d.ts +15 -3
  30. package/dist/types/chrome/transcript-container.d.ts +6 -0
  31. package/dist/types/components/box.d.ts +1 -0
  32. package/dist/types/components/disclosure.d.ts +1 -0
  33. package/dist/types/components/editor.d.ts +23 -5
  34. package/dist/types/components/form.d.ts +1 -0
  35. package/dist/types/components/image.d.ts +26 -44
  36. package/dist/types/components/input.d.ts +2 -1
  37. package/dist/types/components/key-value-list.d.ts +1 -0
  38. package/dist/types/components/layout/row.d.ts +1 -0
  39. package/dist/types/components/layout/stack.d.ts +1 -0
  40. package/dist/types/components/loader.d.ts +5 -6
  41. package/dist/types/components/markdown.d.ts +47 -5
  42. package/dist/types/components/scroll-view.d.ts +1 -0
  43. package/dist/types/components/section.d.ts +1 -0
  44. package/dist/types/components/table.d.ts +1 -0
  45. package/dist/types/components/text.d.ts +1 -0
  46. package/dist/types/components/tree-view.d.ts +7 -0
  47. package/dist/types/components/truncated-text.d.ts +1 -0
  48. package/dist/types/fuzzy.d.ts +29 -1
  49. package/dist/types/key-hint-format.d.ts +9 -1
  50. package/dist/types/keybinding-matchers.d.ts +3 -0
  51. package/dist/types/keybindings.d.ts +1 -1
  52. package/dist/types/kitty-graphics.d.ts +2 -2
  53. package/dist/types/loop-watchdog.d.ts +8 -7
  54. package/dist/types/multiplexers/cmux.d.ts +16 -0
  55. package/dist/types/multiplexers/herdr.d.ts +26 -0
  56. package/dist/types/multiplexers/notify.d.ts +13 -0
  57. package/dist/types/multiplexers/orca.d.ts +12 -0
  58. package/dist/types/multiplexers/screen.d.ts +9 -0
  59. package/dist/types/multiplexers/tmux.d.ts +29 -0
  60. package/dist/types/multiplexers/types.d.ts +100 -0
  61. package/dist/types/multiplexers/wmux.d.ts +12 -0
  62. package/dist/types/multiplexers/zellij.d.ts +12 -0
  63. package/dist/types/native/apply.d.ts +4 -3
  64. package/dist/types/native/backend.d.ts +5 -4
  65. package/dist/types/native/blobs.d.ts +7 -3
  66. package/dist/types/native/encode.d.ts +8 -6
  67. package/dist/types/native/node.d.ts +36 -5
  68. package/dist/types/native/overlay.d.ts +8 -3
  69. package/dist/types/native/settle.d.ts +4 -3
  70. package/dist/types/overlays/agent-hub-projection.d.ts +6 -5
  71. package/dist/types/overlays/agent-hub-types.d.ts +9 -0
  72. package/dist/types/overlays/agent-hub.d.ts +3 -0
  73. package/dist/types/overlays/agent-transcript-viewer.d.ts +12 -2
  74. package/dist/types/overlays/agents-hub.d.ts +9 -0
  75. package/dist/types/overlays/annotation-overlay.d.ts +19 -2
  76. package/dist/types/overlays/annotation-types.d.ts +2 -0
  77. package/dist/types/overlays/ask-dialog.d.ts +5 -2
  78. package/dist/types/overlays/btw-history-panel.d.ts +13 -1
  79. package/dist/types/overlays/copy-selector.d.ts +1 -0
  80. package/dist/types/overlays/history-search.d.ts +10 -0
  81. package/dist/types/overlays/hook-selector.d.ts +9 -4
  82. package/dist/types/overlays/hub-frame.d.ts +4 -1
  83. package/dist/types/overlays/jobs-panel.d.ts +1 -7
  84. package/dist/types/overlays/model-browser.d.ts +54 -7
  85. package/dist/types/overlays/model-hub.d.ts +37 -0
  86. package/dist/types/overlays/oauth-selector.d.ts +0 -1
  87. package/dist/types/overlays/plan-review-overlay.d.ts +3 -2
  88. package/dist/types/overlays/report-panel.d.ts +68 -0
  89. package/dist/types/overlays/rewind-selector.d.ts +1 -0
  90. package/dist/types/overlays/session-observer-registry.d.ts +2 -0
  91. package/dist/types/overlays/session-selector.d.ts +6 -2
  92. package/dist/types/overlays/settings-defs.d.ts +1 -0
  93. package/dist/types/overlays/thinking-selector.d.ts +2 -2
  94. package/dist/types/overlays/usage-dashboard.d.ts +12 -8
  95. package/dist/types/overlays/usage-display.d.ts +3 -1
  96. package/dist/types/prompt/composer-cache.d.ts +0 -16
  97. package/dist/types/prompt/composer.d.ts +18 -3
  98. package/dist/types/prompt/custom-editor.d.ts +25 -13
  99. package/dist/types/prompt/image-source.d.ts +2 -1
  100. package/dist/types/prompt/interactive-context-helpers.d.ts +5 -1
  101. package/dist/types/prompt/model-mention-autocomplete.d.ts +1 -1
  102. package/dist/types/prompt/welcome.d.ts +7 -2
  103. package/dist/types/render/composite.d.ts +2 -0
  104. package/dist/types/render/output-pane.d.ts +1 -0
  105. package/dist/types/render/render-utils.d.ts +1 -0
  106. package/dist/types/render/tool-card.d.ts +6 -0
  107. package/dist/types/render/width-aware-text.d.ts +2 -0
  108. package/dist/types/setup/scenes/types.d.ts +1 -0
  109. package/dist/types/space-hold.d.ts +40 -25
  110. package/dist/types/status-line/component.d.ts +3 -0
  111. package/dist/types/status-line/context-usage.d.ts +7 -5
  112. package/dist/types/status-line/host.d.ts +4 -1
  113. package/dist/types/status-line/segments.d.ts +2 -0
  114. package/dist/types/status-line/types.d.ts +14 -3
  115. package/dist/types/terminal-capabilities.d.ts +41 -36
  116. package/dist/types/terminal-multiplexer.d.ts +127 -14
  117. package/dist/types/terminal.d.ts +23 -16
  118. package/dist/types/theme/active-symbols.d.ts +1 -1
  119. package/dist/types/theme/schema.d.ts +11 -2
  120. package/dist/types/theme/symbols.d.ts +1 -1
  121. package/dist/types/theme/theme-class.d.ts +7 -0
  122. package/dist/types/theme/theme.d.ts +1 -1
  123. package/dist/types/theme/tui-adapters.d.ts +3 -0
  124. package/dist/types/thinking.d.ts +1 -1
  125. package/dist/types/tmux.d.ts +11 -4
  126. package/dist/types/tools/bash.d.ts +7 -2
  127. package/dist/types/tools/daemon.d.ts +1 -1
  128. package/dist/types/tools/eval.d.ts +22 -3
  129. package/dist/types/tools/glob.d.ts +4 -0
  130. package/dist/types/tools/native-view.d.ts +47 -0
  131. package/dist/types/tools/output-meta.d.ts +11 -1
  132. package/dist/types/tools/renderer.d.ts +37 -2
  133. package/dist/types/tools/streaming-output.d.ts +20 -1
  134. package/dist/types/tools/task.d.ts +2 -3
  135. package/dist/types/tools/write.d.ts +9 -2
  136. package/dist/types/tools/yield.d.ts +22 -0
  137. package/dist/types/tui.d.ts +36 -0
  138. package/dist/types/utils.d.ts +6 -0
  139. package/dist/types/vim.d.ts +19 -5
  140. package/dist/types/windows-input-mode.d.ts +33 -1
  141. package/package.json +13 -9
  142. package/src/active-terminal.d.ts +30 -0
  143. package/src/active-terminal.ts +80 -0
  144. package/src/app-keybindings.d.ts +7 -2
  145. package/src/app-keybindings.ts +7 -2
  146. package/src/apps/autoresearch-dashboard.ts +7 -3
  147. package/src/apps/cleanse-board.ts +2 -2
  148. package/src/apps/cleanse-picker.ts +1 -1
  149. package/src/apps/debug/log-viewer.ts +188 -72
  150. package/src/apps/debug/raw-sse-buffer.ts +8 -0
  151. package/src/apps/debug/raw-sse.ts +39 -5
  152. package/src/apps/git/diff-pane.ts +96 -23
  153. package/src/apps/git/git-tui.ts +23 -16
  154. package/src/apps/git/help.ts +29 -20
  155. package/src/apps/git/sidebar.ts +8 -3
  156. package/src/apps/if-bench-board.ts +2 -2
  157. package/src/apps/ps-top.ts +2 -2
  158. package/src/apps/setup-model-picker.ts +1 -1
  159. package/src/autocomplete.d.ts +5 -2
  160. package/src/autocomplete.ts +67 -43
  161. package/src/bracketed-paste.ts +10 -3
  162. package/src/charts/chart-plan.d.ts +674 -0
  163. package/src/charts/chart-plan.ts +5081 -0
  164. package/src/charts/chart-svg.d.ts +59 -0
  165. package/src/charts/chart-svg.ts +4657 -0
  166. package/src/charts/table-data.d.ts +175 -0
  167. package/src/charts/table-data.ts +1478 -0
  168. package/src/chat/advisor-message.ts +11 -0
  169. package/src/chat/assistant-message.d.ts +11 -1
  170. package/src/chat/assistant-message.ts +245 -54
  171. package/src/chat/chat-transcript-builder.d.ts +3 -0
  172. package/src/chat/chat-transcript-builder.ts +9 -3
  173. package/src/chat/compaction-summary-message.d.ts +1 -0
  174. package/src/chat/compaction-summary-message.ts +8 -0
  175. package/src/chat/execution-shared.d.ts +6 -5
  176. package/src/chat/execution-shared.ts +42 -25
  177. package/src/chat/fence-figure.d.ts +42 -0
  178. package/src/chat/fence-figure.ts +80 -0
  179. package/src/chat/figure-markdown.d.ts +42 -0
  180. package/src/chat/figure-markdown.ts +174 -0
  181. package/src/chat/messages.d.ts +1 -1
  182. package/src/chat/messages.ts +1 -1
  183. package/src/chat/read-tool-group.ts +6 -4
  184. package/src/chat/skill-message.ts +6 -0
  185. package/src/chat/svg-figure.d.ts +33 -0
  186. package/src/chat/svg-figure.ts +399 -0
  187. package/src/chat/svg-source.d.ts +49 -0
  188. package/src/chat/svg-source.ts +188 -0
  189. package/src/chat/table-chart.d.ts +89 -0
  190. package/src/chat/table-chart.ts +294 -0
  191. package/src/chat/tool-execution.d.ts +2 -0
  192. package/src/chat/tool-execution.ts +86 -7
  193. package/src/chat/user-message.d.ts +17 -4
  194. package/src/chat/user-message.ts +71 -8
  195. package/src/chrome/shared.ts +53 -0
  196. package/src/chrome/status-notice.ts +25 -7
  197. package/src/chrome/transcript-container.d.ts +6 -0
  198. package/src/chrome/transcript-container.ts +162 -19
  199. package/src/components/box.d.ts +1 -0
  200. package/src/components/box.ts +5 -0
  201. package/src/components/disclosure.d.ts +1 -0
  202. package/src/components/disclosure.ts +8 -0
  203. package/src/components/editor.d.ts +23 -5
  204. package/src/components/editor.ts +316 -47
  205. package/src/components/form.d.ts +1 -0
  206. package/src/components/form.ts +8 -0
  207. package/src/components/image.d.ts +26 -44
  208. package/src/components/image.ts +115 -152
  209. package/src/components/input.d.ts +2 -1
  210. package/src/components/input.ts +17 -7
  211. package/src/components/key-value-list.d.ts +1 -0
  212. package/src/components/key-value-list.ts +4 -0
  213. package/src/components/layout/row.d.ts +1 -0
  214. package/src/components/layout/row.ts +7 -0
  215. package/src/components/layout/stack.d.ts +1 -0
  216. package/src/components/layout/stack.ts +7 -0
  217. package/src/components/loader.d.ts +5 -6
  218. package/src/components/loader.ts +10 -17
  219. package/src/components/markdown.d.ts +47 -5
  220. package/src/components/markdown.ts +786 -303
  221. package/src/components/scroll-view.d.ts +1 -0
  222. package/src/components/scroll-view.ts +5 -0
  223. package/src/components/section.d.ts +1 -0
  224. package/src/components/section.ts +5 -0
  225. package/src/components/settings-list.ts +2 -0
  226. package/src/components/table.d.ts +1 -0
  227. package/src/components/table.ts +4 -0
  228. package/src/components/text.d.ts +1 -0
  229. package/src/components/text.ts +7 -0
  230. package/src/components/tree-view.d.ts +7 -0
  231. package/src/components/tree-view.ts +38 -8
  232. package/src/components/truncated-text.d.ts +1 -0
  233. package/src/components/truncated-text.ts +5 -0
  234. package/src/desktop-notify.ts +3 -2
  235. package/src/fuzzy.d.ts +29 -1
  236. package/src/fuzzy.ts +115 -30
  237. package/src/hotkeys-markdown.ts +9 -2
  238. package/src/key-hint-format.d.ts +9 -1
  239. package/src/key-hint-format.ts +26 -11
  240. package/src/keybinding-matchers.d.ts +3 -0
  241. package/src/keybinding-matchers.ts +6 -2
  242. package/src/keybindings.d.ts +1 -1
  243. package/src/keybindings.ts +1 -1
  244. package/src/kitty-graphics.d.ts +2 -2
  245. package/src/kitty-graphics.ts +11 -5
  246. package/src/loop-watchdog.d.ts +8 -7
  247. package/src/loop-watchdog.ts +18 -13
  248. package/src/multiplexers/cmux.d.ts +16 -0
  249. package/src/multiplexers/cmux.ts +29 -0
  250. package/src/multiplexers/herdr.d.ts +26 -0
  251. package/src/multiplexers/herdr.ts +64 -0
  252. package/src/multiplexers/notify.d.ts +13 -0
  253. package/src/multiplexers/notify.ts +24 -0
  254. package/src/multiplexers/orca.d.ts +12 -0
  255. package/src/multiplexers/orca.ts +16 -0
  256. package/src/multiplexers/screen.d.ts +9 -0
  257. package/src/multiplexers/screen.ts +15 -0
  258. package/src/multiplexers/tmux.d.ts +29 -0
  259. package/src/multiplexers/tmux.ts +40 -0
  260. package/src/multiplexers/types.d.ts +100 -0
  261. package/src/multiplexers/types.ts +110 -0
  262. package/src/multiplexers/wmux.d.ts +12 -0
  263. package/src/multiplexers/wmux.ts +16 -0
  264. package/src/multiplexers/zellij.d.ts +12 -0
  265. package/src/multiplexers/zellij.ts +24 -0
  266. package/src/native/apply.d.ts +4 -3
  267. package/src/native/apply.ts +4 -3
  268. package/src/native/backend.d.ts +5 -4
  269. package/src/native/backend.ts +199 -22
  270. package/src/native/blobs.d.ts +7 -3
  271. package/src/native/blobs.ts +47 -12
  272. package/src/native/encode.d.ts +8 -6
  273. package/src/native/encode.ts +76 -34
  274. package/src/native/node.d.ts +36 -5
  275. package/src/native/node.ts +31 -5
  276. package/src/native/overlay.d.ts +8 -3
  277. package/src/native/overlay.ts +17 -4
  278. package/src/native/reconcile.ts +16 -4
  279. package/src/native/settle.d.ts +4 -3
  280. package/src/native/settle.ts +4 -3
  281. package/src/overlays/agent-hub-projection.d.ts +6 -5
  282. package/src/overlays/agent-hub-projection.ts +124 -13
  283. package/src/overlays/agent-hub-types.d.ts +9 -0
  284. package/src/overlays/agent-hub-types.ts +6 -0
  285. package/src/overlays/agent-hub.ts +48 -29
  286. package/src/overlays/agent-transcript-viewer.ts +93 -23
  287. package/src/overlays/agents-hub.ts +28 -8
  288. package/src/overlays/annotation-overlay.ts +293 -90
  289. package/src/overlays/annotation-types.ts +2 -0
  290. package/src/overlays/ask-dialog.ts +10 -17
  291. package/src/overlays/btw-history-panel.ts +136 -39
  292. package/src/overlays/copy-selector.d.ts +1 -0
  293. package/src/overlays/copy-selector.ts +32 -8
  294. package/src/overlays/extensions/extension-dashboard.ts +4 -4
  295. package/src/overlays/extensions/state-manager.ts +52 -12
  296. package/src/overlays/history-search.ts +100 -7
  297. package/src/overlays/hook-selector.ts +12 -5
  298. package/src/overlays/hub-frame.ts +19 -1
  299. package/src/overlays/jobs-panel.ts +33 -52
  300. package/src/overlays/model-browser.ts +367 -122
  301. package/src/overlays/model-hub.ts +435 -59
  302. package/src/overlays/move-overlay.ts +28 -14
  303. package/src/overlays/oauth-selector.ts +54 -70
  304. package/src/overlays/pause-screen.ts +2 -2
  305. package/src/overlays/plan-review-overlay.ts +208 -70
  306. package/src/overlays/plugin-settings.ts +3 -3
  307. package/src/overlays/report-panel.ts +191 -0
  308. package/src/overlays/rewind-selector.ts +88 -17
  309. package/src/overlays/session-observer-registry.d.ts +2 -0
  310. package/src/overlays/session-observer-registry.ts +10 -2
  311. package/src/overlays/session-selector.ts +64 -26
  312. package/src/overlays/settings-defs.ts +5 -4
  313. package/src/overlays/settings-selector.ts +33 -8
  314. package/src/overlays/snapcompact-shape-preview.ts +2 -3
  315. package/src/overlays/stats-notice.ts +1 -1
  316. package/src/overlays/thinking-selector.ts +11 -10
  317. package/src/overlays/tree-selector.ts +41 -20
  318. package/src/overlays/usage-dashboard.ts +117 -58
  319. package/src/overlays/usage-display.ts +10 -4
  320. package/src/overlays/usage-row.ts +22 -8
  321. package/src/prompt/attachment-chips.ts +15 -11
  322. package/src/prompt/composer-attachments.ts +6 -4
  323. package/src/prompt/composer-cache.ts +7 -86
  324. package/src/prompt/composer.ts +37 -26
  325. package/src/prompt/custom-editor.ts +282 -105
  326. package/src/prompt/image-source.ts +2 -1
  327. package/src/prompt/interactive-context-helpers.ts +32 -18
  328. package/src/prompt/macos-spelling.ts +7 -0
  329. package/src/prompt/magic-keywords.ts +11 -2
  330. package/src/prompt/model-mention-autocomplete.ts +14 -14
  331. package/src/prompt/queued-messages.ts +6 -4
  332. package/src/prompt/tips.txt +13 -9
  333. package/src/prompt/welcome.ts +64 -46
  334. package/src/prompt/word-completion.ts +38 -5
  335. package/src/render/composite.d.ts +2 -0
  336. package/src/render/composite.ts +64 -0
  337. package/src/render/output-pane.d.ts +1 -0
  338. package/src/render/output-pane.ts +6 -0
  339. package/src/render/render-utils.d.ts +1 -0
  340. package/src/render/render-utils.ts +16 -1
  341. package/src/render/tool-card.d.ts +6 -0
  342. package/src/render/tool-card.ts +15 -0
  343. package/src/render/width-aware-text.d.ts +2 -0
  344. package/src/render/width-aware-text.ts +5 -0
  345. package/src/setup/scenes/model.ts +2 -1
  346. package/src/setup/scenes/types.ts +1 -0
  347. package/src/space-hold.d.ts +40 -25
  348. package/src/space-hold.ts +156 -52
  349. package/src/status-line/component.ts +292 -78
  350. package/src/status-line/context-usage.ts +21 -46
  351. package/src/status-line/host.ts +4 -1
  352. package/src/status-line/segments.ts +40 -22
  353. package/src/status-line/types.ts +15 -3
  354. package/src/terminal-capabilities.d.ts +41 -36
  355. package/src/terminal-capabilities.ts +137 -213
  356. package/src/terminal-multiplexer.d.ts +127 -14
  357. package/src/terminal-multiplexer.ts +112 -33
  358. package/src/terminal.d.ts +23 -16
  359. package/src/terminal.ts +203 -99
  360. package/src/theme/active-symbols.d.ts +1 -1
  361. package/src/theme/active-symbols.ts +1 -1
  362. package/src/theme/schema-validation.ts +1 -0
  363. package/src/theme/schema.d.ts +11 -2
  364. package/src/theme/schema.ts +14 -1
  365. package/src/theme/session-color.ts +1 -7
  366. package/src/theme/symbols.d.ts +1 -1
  367. package/src/theme/symbols.ts +11 -4
  368. package/src/theme/theme-class.d.ts +7 -0
  369. package/src/theme/theme-class.ts +26 -1
  370. package/src/theme/theme-schema.json +5 -1
  371. package/src/theme/theme.d.ts +1 -1
  372. package/src/theme/theme.ts +8 -4
  373. package/src/theme/tui-adapters.d.ts +3 -0
  374. package/src/theme/tui-adapters.ts +68 -0
  375. package/src/thinking.d.ts +1 -1
  376. package/src/thinking.ts +1 -1
  377. package/src/tmux.d.ts +11 -4
  378. package/src/tmux.ts +47 -11
  379. package/src/tools/ast-edit.ts +59 -35
  380. package/src/tools/ast-grep.ts +35 -16
  381. package/src/tools/bash.d.ts +7 -2
  382. package/src/tools/bash.ts +116 -47
  383. package/src/tools/daemon.d.ts +1 -1
  384. package/src/tools/daemon.ts +1 -1
  385. package/src/tools/default-renderer.ts +35 -19
  386. package/src/tools/edit.ts +142 -120
  387. package/src/tools/eval-format/index.ts +28 -2
  388. package/src/tools/eval-format/javascript.ts +7 -5
  389. package/src/tools/eval.d.ts +22 -3
  390. package/src/tools/eval.ts +364 -208
  391. package/src/tools/fetch.ts +5 -6
  392. package/src/tools/glob.d.ts +4 -0
  393. package/src/tools/glob.ts +33 -65
  394. package/src/tools/grep.ts +81 -48
  395. package/src/tools/index.ts +2 -0
  396. package/src/tools/lsp.ts +81 -59
  397. package/src/tools/mcp.ts +111 -68
  398. package/src/tools/native-view.d.ts +47 -0
  399. package/src/tools/native-view.ts +102 -1
  400. package/src/tools/output-meta.d.ts +11 -1
  401. package/src/tools/output-meta.ts +23 -3
  402. package/src/tools/read.ts +7 -5
  403. package/src/tools/renderer.d.ts +37 -2
  404. package/src/tools/renderer.ts +42 -2
  405. package/src/tools/streaming-output.d.ts +20 -1
  406. package/src/tools/streaming-output.ts +154 -50
  407. package/src/tools/task.d.ts +2 -3
  408. package/src/tools/task.ts +115 -62
  409. package/src/tools/todo.ts +119 -30
  410. package/src/tools/wait.ts +34 -24
  411. package/src/tools/web-search.ts +1 -1
  412. package/src/tools/write.d.ts +9 -2
  413. package/src/tools/write.ts +147 -95
  414. package/src/tools/yield.d.ts +22 -0
  415. package/src/tools/yield.ts +225 -0
  416. package/src/tui.d.ts +36 -0
  417. package/src/tui.ts +666 -217
  418. package/src/utils.d.ts +6 -0
  419. package/src/utils.ts +19 -0
  420. package/src/vim.d.ts +19 -5
  421. package/src/vim.ts +333 -18
  422. package/src/windows-input-mode.d.ts +33 -1
  423. package/src/windows-input-mode.ts +212 -6
@@ -2,7 +2,7 @@ import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
3
  // Subpaths, not the `@linxiraos/pi-utils` barrel: the barrel loads the native
4
4
  // addon (file-lock), and key-hint formatting runs on addon-free CLI paths
5
- // (`omp --version`, help) through cli/command-help.ts.
5
+ // (`zetacode --version`, help) through cli/command-help.ts.
6
6
  import { getActiveProfile, getAgentDir, getProfileRootDir } from "@linxiraos/pi-utils/dirs";
7
7
  import { isEnoent } from "@linxiraos/pi-utils/fs-error";
8
8
  import * as logger from "@linxiraos/pi-utils/logger";
@@ -60,6 +60,7 @@ interface AppKeybindings {
60
60
  "app.tree.unfoldOrDown": true;
61
61
  "app.plan.toggle": true;
62
62
  "app.history.search": true;
63
+ "app.stt.pushToTalk": true;
63
64
  "app.stt.toggle": true;
64
65
  "app.live.toggle": true;
65
66
  }
@@ -237,9 +238,13 @@ export const KEYBINDINGS = {
237
238
  defaultKeys: "ctrl+r",
238
239
  description: "Search history",
239
240
  },
241
+ "app.stt.pushToTalk": {
242
+ defaultKeys: "space",
243
+ description: "Hold to record speech-to-text",
244
+ },
240
245
  "app.stt.toggle": {
241
246
  defaultKeys: [],
242
- description: "Toggle speech-to-text (default gesture: hold Space)",
247
+ description: "Toggle speech-to-text recording",
243
248
  },
244
249
  "app.live.toggle": {
245
250
  defaultKeys: "ctrl+l",
@@ -17,8 +17,8 @@ import {
17
17
  import { formatNum, type ExperimentResult, type ExperimentState } from "../tools/autoresearch";
18
18
  import type { TspSpan, TspTableColumn, TspText } from "@linxiraos/pi-wire";
19
19
  import { card, col, elapsed, keyed, node, row, span, text } from "../native/describe";
20
- import type { DescribeContext, NativeNode } from "../native/node";
21
- import { hintsRow } from "../native/overlay";
20
+ import type { DescribeContext, NativeNode, NativeUiEvent } from "../native/node";
21
+ import { actionBar, actionButton, hintsRow } from "../native/overlay";
22
22
  import { Memo } from "../native/memo";
23
23
  import { isNativeRendering } from "../native/state";
24
24
 
@@ -190,9 +190,9 @@ export function createDashboardController(): DashboardController {
190
190
  { keys: ["up", "down", "j", "k"], label: "scroll" },
191
191
  { keys: ["pageUp", "pageDown"], label: "page" },
192
192
  { keys: ["g", "shift+g"], label: "top/bottom" },
193
- { keys: ["escape"], label: "close" },
194
193
  ]),
195
194
  );
195
+ children.push(actionBar([null, actionButton("Close", "close", { keys: "escape" })]));
196
196
  return col(children, { gap: "md", role: "zeta.app.autoresearch" });
197
197
  },
198
198
  );
@@ -214,6 +214,10 @@ export function createDashboardController(): DashboardController {
214
214
  scrollView.setHeight(viewportRows);
215
215
  return [header, ...scrollView.render(width), renderOverlayFooter(width, theme)];
216
216
  },
217
+ /** The Close button runs Esc's path. */
218
+ handleNativeEvent(event: NativeUiEvent): void {
219
+ if (event.type === "action" && event.act === "close") done(undefined);
220
+ },
217
221
  handleInput(data: string): void {
218
222
  if (matchesKey(data, "escape") || matchesKey(data, "esc") || data === "q") {
219
223
  done(undefined);
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Live status board for `omp cleanse`.
2
+ * Live status board for `zetacode cleanse`.
3
3
  *
4
4
  * Interactive terminals get a transient board repainted in place: a phase
5
5
  * spinner (model resolution, checker discovery), one row per running checker,
@@ -59,7 +59,7 @@ const REPAIR_BAR_STYLE: ProgressBarStyle = {
59
59
  styleEmpty: text => fgOrPlain("dim", text),
60
60
  };
61
61
 
62
- /** Rendering surface for one `omp cleanse` run. */
62
+ /** Rendering surface for one `zetacode cleanse` run. */
63
63
  export interface CleanseStatusBoard {
64
64
  readonly interactive: boolean;
65
65
  /** Print a permanent line above the live area (plain write when non-TTY). */
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Standalone TUI pickers for `omp cleanse`.
2
+ * Standalone TUI pickers for `zetacode cleanse`.
3
3
  *
4
4
  * One-shot {@link selectStandaloneItem} and {@link promptStandaloneText}
5
5
  * prompts, resolved on select/submit/cancel so the command can keep writing
@@ -10,7 +10,7 @@ import { Input } from "../../components/input";
10
10
  import { routeSgrMouseInput, type SgrMouseEvent } from "../../mouse";
11
11
  import { padding, truncateToWidth, visibleWidth } from "../../utils";
12
12
  import { isRecord, sanitizeText } from "@linxiraos/pi-utils";
13
- import { theme } from "../../theme/theme";
13
+ import { getThemeEpoch, theme } from "../../theme/theme";
14
14
  import { sanitizeDisplayText } from "../../overlays/extensions/display-text";
15
15
  import { DebugViewerFrame, type DebugViewerFrameContent, type DebugViewerFrameContext } from "./viewer-frame";
16
16
  import { formatDebugLogExpandedLines, parseDebugLogPid, parseDebugLogTimestampMs } from "./log-formatting";
@@ -28,6 +28,12 @@ export const LOAD_OLDER_LABEL = "### MOVE UP TO LOAD MORE...";
28
28
 
29
29
  const INITIAL_LOG_CHUNK = 50;
30
30
  const LOAD_OLDER_CHUNK = 50;
31
+ /**
32
+ * Default for {@link DebugLogViewerModelOptions.maxLogEntries}. Once this many
33
+ * entries are loaded, older history stops loading; the newest entries (the
34
+ * current session) are never evicted.
35
+ */
36
+ const MAX_LOG_ENTRIES = 50_000;
31
37
  const MIN_LOG_VIEWER_WIDTH = 48;
32
38
  /** Picker item id of the "load older entries" row. */
33
39
  const OLDER_ITEM = "older";
@@ -43,6 +49,24 @@ type LogEntry = {
43
49
  rawLine: string;
44
50
  timestampMs: number | undefined;
45
51
  pid: number | undefined;
52
+ /** Lowercased {@link rawLine}, filled on first filter match. */
53
+ lowerLine?: string;
54
+ };
55
+
56
+ type SelectionState = {
57
+ rows: readonly ViewerRow[];
58
+ cursor: number;
59
+ anchor: number | undefined;
60
+ indices: number[];
61
+ set: Set<number>;
62
+ };
63
+
64
+ type CachedLogRow = {
65
+ width: number;
66
+ selected: boolean;
67
+ active: boolean;
68
+ expanded: boolean;
69
+ lines: string[];
46
70
  };
47
71
 
48
72
  type CursorToken = { kind: "log"; logIndex: number } | { kind: "load-older" };
@@ -52,6 +76,8 @@ type DebugLogViewerModelOptions = {
52
76
  processPid?: number;
53
77
  hasOlderLogs?: () => boolean;
54
78
  loadOlderLogs?: (limitDays?: number) => Promise<string>;
79
+ /** Most entries kept; older history beyond it is not loaded. Defaults to {@link MAX_LOG_ENTRIES}. */
80
+ maxLogEntries?: number;
55
81
  };
56
82
 
57
83
  /**
@@ -111,6 +137,17 @@ function getProcessStartMs(): number {
111
137
  return Date.now() - process.uptime() * 1000;
112
138
  }
113
139
 
140
+ function parseLogEntries(logText: string): LogEntry[] {
141
+ return logText
142
+ .split("\n")
143
+ .filter(line => line.length > 0)
144
+ .map(rawLine => ({
145
+ rawLine,
146
+ timestampMs: parseDebugLogTimestampMs(rawLine),
147
+ pid: parseDebugLogPid(rawLine),
148
+ }));
149
+ }
150
+
114
151
  /** Split raw log text into nonempty entries. */
115
152
  export function splitLogText(logText: string): string[] {
116
153
  return logText.split("\n").filter(line => line.length > 0);
@@ -126,12 +163,18 @@ export function buildLogCopyPayload(lines: string[]): string {
126
163
 
127
164
  /** Filter, paginate, expand, and select captured log entries. */
128
165
  export class DebugLogViewerModel {
166
+ /**
167
+ * Loaded entries, newest first, so loading older history appends instead of
168
+ * copying the whole array. Log index `i` (0 = oldest) lives at `length - 1 - i`.
169
+ */
129
170
  #entries: LogEntry[];
171
+ readonly #maxLogEntries: number;
130
172
  #rows: ViewerRow[];
131
173
  #visibleLogIndices: number[];
132
174
  #selectableRowIndices: number[];
133
175
  #cursorSelectableIndex = 0;
134
176
  #selectionAnchorSelectableIndex: number | undefined;
177
+ #selection: SelectionState | undefined;
135
178
  #expandedLogIndices = new Set<number>();
136
179
  #filterQuery = "";
137
180
  #processStartMs: number;
@@ -142,12 +185,17 @@ export class DebugLogViewerModel {
142
185
  #loadOlderLogs?: (limitDays?: number) => Promise<string>;
143
186
 
144
187
  constructor(logText: string, options: DebugLogViewerModelOptions = {}) {
145
- const { processStartMs = getProcessStartMs(), processPid = process.pid, hasOlderLogs, loadOlderLogs } = options;
146
- this.#entries = splitLogText(logText).map(rawLine => ({
147
- rawLine,
148
- timestampMs: parseDebugLogTimestampMs(rawLine),
149
- pid: parseDebugLogPid(rawLine),
150
- }));
188
+ const {
189
+ processStartMs = getProcessStartMs(),
190
+ processPid = process.pid,
191
+ hasOlderLogs,
192
+ loadOlderLogs,
193
+ maxLogEntries = MAX_LOG_ENTRIES,
194
+ } = options;
195
+ this.#maxLogEntries = Math.max(1, maxLogEntries);
196
+ this.#entries = parseLogEntries(logText).reverse();
197
+ // Newest first: truncating keeps the newest entries and drops the oldest.
198
+ if (this.#entries.length > this.#maxLogEntries) this.#entries.length = this.#maxLogEntries;
151
199
  this.#processStartMs = processStartMs;
152
200
  this.#processPid = processPid;
153
201
  this.#hasOlderLogs = hasOlderLogs;
@@ -163,6 +211,11 @@ export class DebugLogViewerModel {
163
211
  return this.#entries.length;
164
212
  }
165
213
 
214
+ /** True once the entry cap is reached, so older history no longer loads. */
215
+ get historyLimitReached(): boolean {
216
+ return this.#entries.length >= this.#maxLogEntries;
217
+ }
218
+
166
219
  get visibleLogCount(): number {
167
220
  return this.#visibleLogIndices.length;
168
221
  }
@@ -201,7 +254,7 @@ export class DebugLogViewerModel {
201
254
  }
202
255
 
203
256
  getRawLine(logIndex: number): string {
204
- return this.#entries[logIndex]?.rawLine ?? "";
257
+ return this.#entryAt(logIndex)?.rawLine ?? "";
205
258
  }
206
259
 
207
260
  setFilterQuery(query: string): void {
@@ -269,38 +322,15 @@ export class DebugLogViewerModel {
269
322
  }
270
323
 
271
324
  getSelectedLogIndices(): number[] {
272
- if (this.#selectableRowIndices.length === 0) {
273
- return [];
274
- }
275
-
276
- const cursorRow = this.#getCursorRow();
277
- if (this.#selectionAnchorSelectableIndex === undefined) {
278
- if (cursorRow?.kind !== "log") {
279
- return [];
280
- }
281
- return [cursorRow.logIndex];
282
- }
283
-
284
- const min = Math.min(this.#selectionAnchorSelectableIndex, this.#cursorSelectableIndex);
285
- const max = Math.max(this.#selectionAnchorSelectableIndex, this.#cursorSelectableIndex);
286
- const selected: number[] = [];
287
- for (let i = min; i <= max; i++) {
288
- const rowIndex = this.#selectableRowIndices[i];
289
- const row = rowIndex === undefined ? undefined : this.#rows[rowIndex];
290
- if (row?.kind === "log") {
291
- selected.push(row.logIndex);
292
- }
293
- }
294
- return selected;
325
+ return this.#selectionState().indices.slice();
295
326
  }
296
327
 
297
328
  getSelectedCount(): number {
298
- return this.getSelectedLogIndices().length;
329
+ return this.#selectionState().indices.length;
299
330
  }
300
331
 
301
332
  isSelected(logIndex: number): boolean {
302
- const selected = this.getSelectedLogIndices();
303
- return selected.includes(logIndex);
333
+ return this.#selectionState().set.has(logIndex);
304
334
  }
305
335
 
306
336
  isExpanded(logIndex: number): boolean {
@@ -320,8 +350,7 @@ export class DebugLogViewerModel {
320
350
  }
321
351
 
322
352
  getSelectedRawLines(): string[] {
323
- const selectedIndices = this.getSelectedLogIndices();
324
- return selectedIndices.map(index => this.getRawLine(index));
353
+ return this.#selectionState().indices.map(index => this.getRawLine(index));
325
354
  }
326
355
 
327
356
  selectAllVisible(): void {
@@ -381,16 +410,17 @@ export class DebugLogViewerModel {
381
410
  prependLogs(logText: string): number {
382
411
  const previousCursor = this.#getCursorToken();
383
412
  const previousAnchorLogIndex = this.#getAnchorLogIndex();
384
- const newEntries = splitLogText(logText).map(rawLine => ({
385
- rawLine,
386
- timestampMs: parseDebugLogTimestampMs(rawLine),
387
- pid: parseDebugLogPid(rawLine),
388
- }));
413
+ const room = this.#maxLogEntries - this.#entries.length;
414
+ const parsed = room > 0 ? parseLogEntries(logText) : [];
415
+ // Chronological order: past the cap, keep the chunk's newest entries (nearest the loaded history).
416
+ const newEntries = parsed.length > room ? parsed.slice(parsed.length - room) : parsed;
389
417
  if (newEntries.length === 0) {
390
418
  return 0;
391
419
  }
392
420
  const offset = newEntries.length;
393
- this.#entries = [...newEntries, ...this.#entries];
421
+ for (let i = offset - 1; i >= 0; i--) {
422
+ this.#entries.push(newEntries[i]!);
423
+ }
394
424
  this.#loadedStartIndex += offset;
395
425
  this.#expandedLogIndices = new Set([...this.#expandedLogIndices].map(logIndex => logIndex + offset));
396
426
  const adjustedCursor: CursorToken | undefined =
@@ -421,7 +451,7 @@ export class DebugLogViewerModel {
421
451
  const query = this.#filterQuery.toLowerCase();
422
452
  const visible: number[] = [];
423
453
  for (let i = this.#loadedStartIndex; i < this.#entries.length; i++) {
424
- const entry = this.#entries[i];
454
+ const entry = this.#entryAt(i);
425
455
  if (!entry) {
426
456
  continue;
427
457
  }
@@ -438,7 +468,7 @@ export class DebugLogViewerModel {
438
468
  let olderSeen = false;
439
469
  let warningInserted = false;
440
470
  for (const logIndex of visible) {
441
- const timestampMs = this.#entries[logIndex]?.timestampMs;
471
+ const timestampMs = this.#entryAt(logIndex)?.timestampMs;
442
472
  if (timestampMs !== undefined) {
443
473
  if (timestampMs < this.#processStartMs) {
444
474
  olderSeen = true;
@@ -486,7 +516,7 @@ export class DebugLogViewerModel {
486
516
  }
487
517
 
488
518
  #matchesFilters(entry: LogEntry, query: string): boolean {
489
- if (query.length > 0 && !entry.rawLine.toLowerCase().includes(query)) {
519
+ if (query.length > 0 && !(entry.lowerLine ??= entry.rawLine.toLowerCase()).includes(query)) {
490
520
  return false;
491
521
  }
492
522
  if (!this.#processFilterEnabled) {
@@ -503,7 +533,7 @@ export class DebugLogViewerModel {
503
533
  return false;
504
534
  }
505
535
  for (let i = 0; i < this.#loadedStartIndex; i++) {
506
- const entry = this.#entries[i];
536
+ const entry = this.#entryAt(i);
507
537
  if (entry && this.#matchesFilters(entry, query)) {
508
538
  return true;
509
539
  }
@@ -512,7 +542,60 @@ export class DebugLogViewerModel {
512
542
  }
513
543
 
514
544
  #hasExternalOlderLogs(): boolean {
515
- return this.#hasOlderLogs?.() ?? false;
545
+ return !this.historyLimitReached && (this.#hasOlderLogs?.() ?? false);
546
+ }
547
+
548
+ #entryAt(logIndex: number): LogEntry | undefined {
549
+ return this.#entries[this.#entries.length - 1 - logIndex];
550
+ }
551
+
552
+ /** Selected log indices, memoized until the rows, cursor or anchor change. */
553
+ #selectionState(): SelectionState {
554
+ const cached = this.#selection;
555
+ if (
556
+ cached &&
557
+ cached.rows === this.#rows &&
558
+ cached.cursor === this.#cursorSelectableIndex &&
559
+ cached.anchor === this.#selectionAnchorSelectableIndex
560
+ ) {
561
+ return cached;
562
+ }
563
+ const indices = this.#computeSelectedLogIndices();
564
+ const state: SelectionState = {
565
+ rows: this.#rows,
566
+ cursor: this.#cursorSelectableIndex,
567
+ anchor: this.#selectionAnchorSelectableIndex,
568
+ indices,
569
+ set: new Set(indices),
570
+ };
571
+ this.#selection = state;
572
+ return state;
573
+ }
574
+
575
+ #computeSelectedLogIndices(): number[] {
576
+ if (this.#selectableRowIndices.length === 0) {
577
+ return [];
578
+ }
579
+
580
+ const cursorRow = this.#getCursorRow();
581
+ if (this.#selectionAnchorSelectableIndex === undefined) {
582
+ if (cursorRow?.kind !== "log") {
583
+ return [];
584
+ }
585
+ return [cursorRow.logIndex];
586
+ }
587
+
588
+ const min = Math.min(this.#selectionAnchorSelectableIndex, this.#cursorSelectableIndex);
589
+ const max = Math.max(this.#selectionAnchorSelectableIndex, this.#cursorSelectableIndex);
590
+ const selected: number[] = [];
591
+ for (let i = min; i <= max; i++) {
592
+ const rowIndex = this.#selectableRowIndices[i];
593
+ const row = rowIndex === undefined ? undefined : this.#rows[rowIndex];
594
+ if (row?.kind === "log") {
595
+ selected.push(row.logIndex);
596
+ }
597
+ }
598
+ return selected;
516
599
  }
517
600
 
518
601
  #getCursorRow(): ViewerRow | undefined {
@@ -572,9 +655,13 @@ export class DebugLogViewerComponent implements Component {
572
655
  #loadingOlder = false;
573
656
  #bodyLineToRowIndex: Array<number | undefined> = [];
574
657
  readonly #native = new Memo();
575
- /** Picker rows by log index, valid while {@link #nativeItemsCount} entries are loaded (a prepend shifts indices). */
658
+ /** Picker rows by log index, valid for {@link #nativeItemsGeneration} (a prepend shifts indices). */
576
659
  #nativeItems = new Map<number, TspPickerItem>();
577
- #nativeItemsCount = 0;
660
+ #nativeItemsGeneration = 0;
661
+ /** Formatted ANSI log rows by log index, valid for {@link #rowCacheGeneration} and {@link #rowCacheThemeEpoch}. */
662
+ #rowCache = new Map<number, CachedLogRow>();
663
+ #rowCacheGeneration = 0;
664
+ #rowCacheThemeEpoch = -1;
578
665
 
579
666
  constructor(options: DebugLogViewerComponentOptions) {
580
667
  this.#deps = options.deps;
@@ -744,6 +831,7 @@ export class DebugLogViewerComponent implements Component {
744
831
  invalidate(): void {
745
832
  this.#frame.invalidate();
746
833
  this.#native.clear();
834
+ this.#rowCache.clear();
747
835
  }
748
836
 
749
837
  dispose(): void {
@@ -774,6 +862,7 @@ export class DebugLogViewerComponent implements Component {
774
862
  model.cursorRowIndex,
775
863
  selected.join(","),
776
864
  model.logCount,
865
+ model.historyLimitReached,
777
866
  model.isProcessFilterEnabled(),
778
867
  model.canLoadOlder(),
779
868
  query,
@@ -782,9 +871,11 @@ export class DebugLogViewerComponent implements Component {
782
871
  this.#statusMessage,
783
872
  ],
784
873
  () => {
785
- if (this.#nativeItemsCount !== model.logCount) {
874
+ // Entries only grow, and every prepend shifts log indices, so the count keys the item cache.
875
+ const generation = model.logCount;
876
+ if (this.#nativeItemsGeneration !== generation) {
786
877
  this.#nativeItems = new Map();
787
- this.#nativeItemsCount = model.logCount;
878
+ this.#nativeItemsGeneration = generation;
788
879
  }
789
880
  const items: TspPickerItem[] = [];
790
881
  const order: (string | TspPickerGroup)[] = [];
@@ -817,6 +908,7 @@ export class DebugLogViewerComponent implements Component {
817
908
  const subtitle = [
818
909
  `${model.visibleLogCount}/${model.logCount} entries`,
819
910
  selected.length > 1 ? `${selected.length} selected` : undefined,
911
+ model.historyLimitReached ? "history limit reached" : undefined,
820
912
  this.#loadingOlder ? "loading older…" : undefined,
821
913
  ]
822
914
  .filter(part => part !== undefined)
@@ -853,7 +945,11 @@ export class DebugLogViewerComponent implements Component {
853
945
  pickerAction("all", "Select all", "ctrl+a"),
854
946
  pickerAction("pid", "This process", "ctrl+p", { on: model.isProcessFilterEnabled() }),
855
947
  pickerAction("older", "Load older", "ctrl+o", {
856
- disabled: model.canLoadOlder() ? undefined : "No older log entries",
948
+ disabled: model.canLoadOlder()
949
+ ? undefined
950
+ : model.historyLimitReached
951
+ ? "Log history limit reached"
952
+ : "No older log entries",
857
953
  }),
858
954
  CLOSE_ACTION,
859
955
  ],
@@ -945,7 +1041,8 @@ export class DebugLogViewerComponent implements Component {
945
1041
  #summaryText(): string {
946
1042
  const selected = this.#model.getSelectedCount();
947
1043
  const expanded = this.#model.expandedCount;
948
- return `${theme.fg("muted", "showing")} ${theme.fg("accent", `${this.#model.visibleLogCount}/${this.#model.logCount}`)} ${theme.fg("muted", "selected")} ${theme.fg(selected > 0 ? "accent" : "muted", String(selected))} ${theme.fg("muted", "expanded")} ${theme.fg(expanded > 0 ? "accent" : "muted", String(expanded))}`;
1044
+ const limitText = this.#model.historyLimitReached ? ` ${theme.fg("warning", "history limit reached")}` : "";
1045
+ return `${theme.fg("muted", "showing")} ${theme.fg("accent", `${this.#model.visibleLogCount}/${this.#model.logCount}`)} ${theme.fg("muted", "selected")} ${theme.fg(selected > 0 ? "accent" : "muted", String(selected))} ${theme.fg("muted", "expanded")} ${theme.fg(expanded > 0 ? "accent" : "muted", String(expanded))}${limitText}`;
949
1046
  }
950
1047
 
951
1048
  #controlsText(): string {
@@ -1017,10 +1114,21 @@ export class DebugLogViewerComponent implements Component {
1017
1114
  }
1018
1115
 
1019
1116
  #renderRows(innerWidth: number): Array<{ lines: string[]; rowIndex: number }> {
1117
+ const model = this.#model;
1020
1118
  const rendered: Array<{ lines: string[]; rowIndex: number }> = [];
1021
-
1022
- for (let rowIndex = 0; rowIndex < this.#model.rows.length; rowIndex++) {
1023
- const row = this.#model.rows[rowIndex];
1119
+ // Entries only grow, and every prepend shifts log indices, so the count keys the row cache.
1120
+ const generation = model.logCount;
1121
+ const themeEpoch = getThemeEpoch();
1122
+ if (this.#rowCacheGeneration !== generation || this.#rowCacheThemeEpoch !== themeEpoch) {
1123
+ this.#rowCache.clear();
1124
+ this.#rowCacheGeneration = generation;
1125
+ this.#rowCacheThemeEpoch = themeEpoch;
1126
+ }
1127
+ const cursorRowIndex = model.cursorRowIndex;
1128
+ const cursorLogIndex = model.cursorLogIndex;
1129
+
1130
+ for (let rowIndex = 0; rowIndex < model.rows.length; rowIndex++) {
1131
+ const row = model.rows[rowIndex];
1024
1132
  if (!row) {
1025
1133
  continue;
1026
1134
  }
@@ -1034,7 +1142,7 @@ export class DebugLogViewerComponent implements Component {
1034
1142
  }
1035
1143
 
1036
1144
  if (row.kind === "load-older") {
1037
- const active = this.#model.cursorRowIndex === rowIndex;
1145
+ const active = cursorRowIndex === rowIndex;
1038
1146
  const marker = active ? theme.fg("accent", "❯") : " ";
1039
1147
  const prefix = `${marker} `;
1040
1148
  const contentWidth = Math.max(1, innerWidth - visibleWidth(prefix));
@@ -1047,32 +1155,40 @@ export class DebugLogViewerComponent implements Component {
1047
1155
  }
1048
1156
 
1049
1157
  const logIndex = row.logIndex;
1050
- const selected = this.#model.isSelected(logIndex);
1051
- const cursorLogIndex = this.#model.cursorLogIndex;
1158
+ const selected = model.isSelected(logIndex);
1052
1159
  const active = cursorLogIndex !== undefined && cursorLogIndex === logIndex;
1053
- const expanded = this.#model.isExpanded(logIndex);
1160
+ const expanded = model.isExpanded(logIndex);
1161
+ const cached = this.#rowCache.get(logIndex);
1162
+ if (
1163
+ cached &&
1164
+ cached.width === innerWidth &&
1165
+ cached.selected === selected &&
1166
+ cached.active === active &&
1167
+ cached.expanded === expanded
1168
+ ) {
1169
+ rendered.push({ rowIndex, lines: cached.lines });
1170
+ continue;
1171
+ }
1172
+
1054
1173
  const marker = active ? theme.fg("accent", "❯") : selected ? theme.fg("accent", "•") : " ";
1055
1174
  const fold = expanded ? theme.fg("accent", "▾") : theme.fg("muted", "▸");
1056
1175
  const prefix = `${marker}${fold} `;
1057
1176
  const contentWidth = Math.max(1, innerWidth - visibleWidth(prefix));
1058
-
1177
+ let lines: string[];
1059
1178
  if (expanded) {
1060
- const wrapped = formatDebugLogExpandedLines(this.#model.getRawLine(logIndex), contentWidth);
1179
+ const wrapped = formatDebugLogExpandedLines(model.getRawLine(logIndex), contentWidth);
1061
1180
  const indent = padding(visibleWidth(prefix));
1062
- const lines = wrapped.map((segment, index) => {
1181
+ lines = wrapped.map((segment, index) => {
1063
1182
  const content = selected ? theme.bold(segment) : segment;
1064
1183
  return truncateToWidth(`${index === 0 ? prefix : indent}${content}`, innerWidth);
1065
1184
  });
1066
- rendered.push({ rowIndex, lines });
1067
- continue;
1185
+ } else {
1186
+ const preview = truncateToWidth(sanitizeDisplayText(model.getRawLine(logIndex)), contentWidth);
1187
+ const content = selected ? theme.bold(preview) : preview;
1188
+ lines = [truncateToWidth(`${prefix}${content}`, innerWidth)];
1068
1189
  }
1069
-
1070
- const preview = truncateToWidth(
1071
- sanitizeDisplayText(this.#model.getRawLine(logIndex)),
1072
- Math.max(1, contentWidth),
1073
- );
1074
- const content = selected ? theme.bold(preview) : preview;
1075
- rendered.push({ rowIndex, lines: [truncateToWidth(`${prefix}${content}`, innerWidth)] });
1190
+ this.#rowCache.set(logIndex, { width: innerWidth, selected, active, expanded, lines });
1191
+ rendered.push({ rowIndex, lines });
1076
1192
  }
1077
1193
 
1078
1194
  return rendered;
@@ -271,9 +271,15 @@ export class RawSseDebugBuffer {
271
271
  #totalEvents = 0;
272
272
  #lastUpdatedAt: number | undefined;
273
273
  #nextSequence = 1;
274
+ #revision = 0;
274
275
  #listeners = new Set<() => void>();
275
276
  #emitScheduled = false;
276
277
 
278
+ /** Bumped on every change to the captured window (append, eviction, clear); equal revisions mean equal snapshots. */
279
+ get revision(): number {
280
+ return this.#revision;
281
+ }
282
+
277
283
  subscribe(listener: () => void): () => void {
278
284
  this.#listeners.add(listener);
279
285
  return () => this.#listeners.delete(listener);
@@ -359,6 +365,7 @@ export class RawSseDebugBuffer {
359
365
  this.#droppedChars = 0;
360
366
  this.#totalEvents = 0;
361
367
  this.#lastUpdatedAt = undefined;
368
+ this.#revision += 1;
362
369
  this.#emit();
363
370
  }
364
371
 
@@ -368,6 +375,7 @@ export class RawSseDebugBuffer {
368
375
  this.#totalChars += chars;
369
376
  this.#lastUpdatedAt = record.timestamp;
370
377
  this.#enforceLimits();
378
+ this.#revision += 1;
371
379
  this.#emit();
372
380
  }
373
381
 
@@ -9,7 +9,7 @@ import { actionBar, actionButton } from "../../native/overlay";
9
9
  import { routeSgrMouseInput, type SgrMouseEvent } from "../../mouse";
10
10
  import { truncateToWidth } from "../../utils";
11
11
  import { sanitizeDisplayText } from "../../overlays/extensions/display-text";
12
- import { theme } from "../../theme/theme";
12
+ import { getThemeEpoch, theme } from "../../theme/theme";
13
13
  import { DebugViewerFrame } from "./viewer-frame";
14
14
  import {
15
15
  formatRawSseIsoTime,
@@ -106,6 +106,14 @@ export class RawSseViewerComponent implements Component {
106
106
  // Sequences are monotonic; we prune entries below the oldest live record
107
107
  // after each render so the cache tracks the buffer's eviction window.
108
108
  readonly #prettyLinesCache = new Map<number, string[]>();
109
+ /** Buffer snapshot for {@link #snapshotRevision}; the buffer's revision bumps on every change. */
110
+ #snapshot: RawSseDebugSnapshot | undefined;
111
+ #snapshotRevision = -1;
112
+ /** Rendered body for (buffer revision, width, theme epoch). */
113
+ #bodyLines: string[] = [];
114
+ #bodyRevision = -1;
115
+ #bodyWidth = -1;
116
+ #bodyThemeEpoch = -1;
109
117
 
110
118
  constructor(options: RawSseViewerOptions) {
111
119
  this.#deps = options.deps;
@@ -227,6 +235,7 @@ export class RawSseViewerComponent implements Component {
227
235
  this.#native.clear();
228
236
  this.#nativeHead.clear();
229
237
  this.#nativeBody.clear();
238
+ this.#bodyRevision = -1;
230
239
  }
231
240
 
232
241
  render(width: number): readonly string[] {
@@ -249,7 +258,7 @@ export class RawSseViewerComponent implements Component {
249
258
  * so they never scroll away.
250
259
  */
251
260
  describeScreen(): NativeScreen {
252
- const snapshot = this.#buffer.snapshot();
261
+ const snapshot = this.#currentSnapshot();
253
262
  const stream = this.#feedNative(snapshot);
254
263
  const head = this.#nativeHead.get(
255
264
  [snapshot.totalEvents, snapshot.records.length, snapshot.droppedRecords, snapshot.lastUpdatedAt],
@@ -372,15 +381,40 @@ export class RawSseViewerComponent implements Component {
372
381
  this.#onExit();
373
382
  }
374
383
 
384
+ /** The buffer's snapshot, re-taken only when its revision moved. */
385
+ #currentSnapshot(): RawSseDebugSnapshot {
386
+ const revision = this.#buffer.revision;
387
+ if (!this.#snapshot || this.#snapshotRevision !== revision) {
388
+ this.#snapshot = this.#buffer.snapshot();
389
+ this.#snapshotRevision = revision;
390
+ }
391
+ return this.#snapshot;
392
+ }
393
+
375
394
  #renderRawLines(innerWidth: number): string[] {
376
- const snapshot = this.#buffer.snapshot();
395
+ const snapshot = this.#currentSnapshot();
396
+ const themeEpoch = getThemeEpoch();
397
+ if (
398
+ this.#bodyRevision === this.#snapshotRevision &&
399
+ this.#bodyWidth === innerWidth &&
400
+ this.#bodyThemeEpoch === themeEpoch
401
+ ) {
402
+ return this.#bodyLines;
403
+ }
404
+ this.#bodyRevision = this.#snapshotRevision;
405
+ this.#bodyWidth = innerWidth;
406
+ this.#bodyThemeEpoch = themeEpoch;
407
+ this.#bodyLines = this.#formatRawLines(snapshot, innerWidth);
408
+ return this.#bodyLines;
409
+ }
410
+
411
+ #formatRawLines(snapshot: RawSseDebugSnapshot, innerWidth: number): string[] {
377
412
  if (snapshot.records.length === 0) {
378
413
  return [
379
414
  theme.fg("muted", "No raw SSE frames captured yet."),
380
415
  theme.fg("muted", "HTTP SSE providers populate this view while a model response is streaming."),
381
416
  ];
382
417
  }
383
-
384
418
  const lines: string[] = [];
385
419
  if (snapshot.droppedRecords > 0) {
386
420
  lines.push(
@@ -421,7 +455,7 @@ export class RawSseViewerComponent implements Component {
421
455
  }
422
456
  }
423
457
  #summaryText(): string {
424
- const snapshot = this.#buffer.snapshot();
458
+ const snapshot = this.#currentSnapshot();
425
459
  const last = snapshot.lastUpdatedAt
426
460
  ? `${theme.fg("muted", "last")} ${theme.fg("accent", formatRawSseIsoTime(snapshot.lastUpdatedAt))}`
427
461
  : theme.fg("muted", "waiting for first frame");