@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
@@ -8,8 +8,8 @@
8
8
  * This is a usable subset of Vim, not a reimplementation of it (see issue #3299): Normal and Visual
9
9
  * modes, the common motions, and operators built from those motions.
10
10
  */
11
- export type VimMode = "insert" | "normal" | "visual" | "visual-line";
12
- export type VimOperator = "d" | "y" | "c";
11
+ export type VimMode = "insert" | "normal" | "visual" | "visual-line" | "replace";
12
+ export type VimOperator = "d" | "y" | "c" | ">" | "<";
13
13
  export interface VimPosition {
14
14
  line: number;
15
15
  col: number;
@@ -50,6 +50,20 @@ export type VimCommand = {
50
50
  count: number;
51
51
  } | {
52
52
  kind: "undo";
53
+ } | {
54
+ kind: "replace";
55
+ from: VimPosition;
56
+ to: VimPosition;
57
+ text: string;
58
+ } | {
59
+ kind: "join";
60
+ fromLine: number;
61
+ lines: number;
62
+ } | {
63
+ kind: "indent";
64
+ fromLine: number;
65
+ toLine: number;
66
+ out: boolean;
53
67
  };
54
68
  /** Start offset of the grapheme after `col`, clamped to `text.length`. */
55
69
  export declare function nextGraphemeStart(text: string, col: number): number;
@@ -62,11 +76,11 @@ export declare class VimState {
62
76
  mode: VimMode;
63
77
  /** Fixed end of a Visual selection; the cursor is the moving end. */
64
78
  anchor: VimPosition | null;
65
- /** True while a count, operator, `g`, or text-object prefix is half-typed — Escape cancels it. */
79
+ /** True while a count, operator, `g`, text-object, find, or `r` prefix is half-typed — Escape cancels it. */
66
80
  get pending(): boolean;
67
81
  /**
68
- * The half-typed command as Vim would echo it (`"2"`, `"d"`, `"2d"`, `"di"`) — empty when
69
- * nothing is pending. Hosts render this next to the mode so a partially entered operator is
82
+ * The half-typed command as Vim would echo it (`"2"`, `"d"`, `"2d"`, `"di"`, `"df"`, `"r"`) — empty
83
+ * when nothing is pending. Hosts render this next to the mode so a partially entered operator is
70
84
  * visible instead of silently swallowing the next keystroke.
71
85
  */
72
86
  get pendingText(): string;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@linxiraos/pi-tui",
4
- "version": "1.1.27",
4
+ "version": "1.2.0",
5
5
  "description": "Terminal User Interface library with differential rendering for efficient text-based applications",
6
6
  "homepage": "https://linxira-os.github.io/zeta/",
7
7
  "author": "Linxira-OS",
@@ -38,14 +38,14 @@
38
38
  "gen:glyphs": "bun scripts/gen-glyphs.ts"
39
39
  },
40
40
  "dependencies": {
41
- "@linxiraos/pi-omptype": "1.1.27",
42
- "@linxiraos/pi-agent-core": "1.1.27",
43
- "@linxiraos/pi-ai": "1.1.27",
44
- "@linxiraos/pi-catalog": "1.1.27",
45
- "@linxiraos/pi-natives": "1.1.27",
46
- "@linxiraos/pi-utils": "1.1.27",
47
- "@linxiraos/pi-wire": "1.1.27",
48
- "@linxiraos/pi-snapcompact": "1.1.27"
41
+ "@linxiraos/pi-omptype": "1.2.0",
42
+ "@linxiraos/pi-agent-core": "1.2.0",
43
+ "@linxiraos/pi-ai": "1.2.0",
44
+ "@linxiraos/pi-catalog": "1.2.0",
45
+ "@linxiraos/pi-natives": "1.2.0",
46
+ "@linxiraos/pi-utils": "1.2.0",
47
+ "@linxiraos/pi-wire": "1.2.0",
48
+ "@linxiraos/pi-snapcompact": "1.2.0"
49
49
  },
50
50
  "devDependencies": {
51
51
  "kitty-vt-wasm": "^0.2.0"
@@ -102,6 +102,10 @@
102
102
  "types": "./dist/types/components/*.d.ts",
103
103
  "import": "./src/components/*.ts"
104
104
  },
105
+ "./native/*": {
106
+ "types": "./dist/types/native/*.d.ts",
107
+ "import": "./src/native/*.ts"
108
+ },
105
109
  "./*.js": "./src/*.ts"
106
110
  }
107
111
  }
@@ -119,7 +119,7 @@ export declare const KEYBINDINGS: {
119
119
  readonly description: "Delete word backward";
120
120
  };
121
121
  readonly "tui.editor.deleteWordForward": {
122
- readonly defaultKeys: ["alt+delete", "alt+d", "super+alt+delete", "super+alt+d"];
122
+ readonly defaultKeys: ["alt+delete", "alt+d", "ctrl+delete", "super+alt+delete", "super+alt+d"];
123
123
  readonly description: "Delete word forward";
124
124
  };
125
125
  readonly "tui.editor.deleteToLineStart": {
@@ -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";
@@ -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