@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,8 @@ import { ThinkingLevel } from "@linxiraos/pi-agent-core";
2
2
  import {
3
3
  type Component,
4
4
  Container,
5
- fuzzyMatch,
5
+ FuzzyQuery,
6
+ FuzzyText,
6
7
  Input,
7
8
  matchesKey,
8
9
  Spacer,
@@ -53,7 +54,7 @@ import { shortenPath } from "../render/render-utils";
53
54
  import { canonicalizeMessage } from "../chat/thinking-display";
54
55
  import { resolveAssistantErrorPresentation } from "../chat/transcript-render-helpers";
55
56
  import { OverlayPanel, PanelDivider } from "../chrome/overlay-box";
56
- import { TreeView, type TreeRow } from "../components/tree-view";
57
+ import { TreeView, type TreeAncestor, type TreeRow } from "../components/tree-view";
57
58
  import { formatKeyHint, formatKeyHints } from "../app-keybindings";
58
59
  import { boundKeys, editorKeys, interruptKey } from "../chrome/keybinding-hints";
59
60
  import type { TspPickerItem, TspPickerProps, TspSpan, TspText } from "@linxiraos/pi-wire";
@@ -210,6 +211,8 @@ class TreeList implements Component {
210
211
  #multipleRoots = false;
211
212
  #activePathIds: Set<string> = new Set();
212
213
  #containsActive: Map<TreeSelectorNode, boolean> = new Map();
214
+ /** Prepared search text per node; rebuilt when the node's label changes. */
215
+ #searchTextCache = new WeakMap<TreeSelectorNode, { label: string | undefined; text: FuzzyText }>();
213
216
 
214
217
  onSelect?: (entryId: string, options: { summarize: boolean }) => void;
215
218
  onCancel?: () => void;
@@ -253,6 +256,7 @@ class TreeList implements Component {
253
256
  getChildren: node => this.#orderedChildren(node),
254
257
  getChildDepth: (_node, row, children) =>
255
258
  children.length > 1 || (this.#multipleRoots && row.parentKey === undefined) ? row.depth + 1 : row.depth,
259
+ compactSameDepthAncestors: true,
256
260
  theme,
257
261
  filter: this.#buildFilter(),
258
262
  maxRows: maxVisibleLines,
@@ -373,7 +377,11 @@ class TreeList implements Component {
373
377
  #buildFilter(): (node: TreeSelectorNode, row: TreeRow<TreeSelectorNode, string>) => boolean {
374
378
  const filterMode = this.#filterMode;
375
379
  const leafId = this.currentLeafId;
376
- const searchTokens = this.getSearchQuery().toLowerCase().split(/\s+/).filter(Boolean);
380
+ const searchTokens = this.getSearchQuery()
381
+ .toLowerCase()
382
+ .split(/\s+/)
383
+ .filter(Boolean)
384
+ .map(token => new FuzzyQuery(token));
377
385
  return node => {
378
386
  const entry = node.entry;
379
387
  const isCurrentLeaf = entry.id === leafId;
@@ -435,14 +443,23 @@ class TreeList implements Component {
435
443
 
436
444
  // Apply fuzzy search filter
437
445
  if (searchTokens.length > 0) {
438
- const nodeText = this.#getSearchableText(node);
439
- return searchTokens.every(token => fuzzyMatch(token, nodeText).matches);
446
+ const nodeText = this.#getFuzzyText(node);
447
+ return searchTokens.every(token => token.match(nodeText).matches);
440
448
  }
441
449
 
442
450
  return true;
443
451
  };
444
452
  }
445
453
 
454
+ /** Prepared fuzzy text for a node's searchable content, built once per label. */
455
+ #getFuzzyText(node: TreeSelectorNode): FuzzyText {
456
+ const cached = this.#searchTextCache.get(node);
457
+ if (cached !== undefined && cached.label === node.label) return cached.text;
458
+ const text = new FuzzyText(this.#getSearchableText(node));
459
+ this.#searchTextCache.set(node, { label: node.label, text });
460
+ return text;
461
+ }
462
+
446
463
  /** Get searchable text content from a node */
447
464
  #getSearchableText(node: TreeSelectorNode): string {
448
465
  const entry = node.entry;
@@ -877,23 +894,26 @@ class TreeList implements Component {
877
894
  // Linear rows reuse their branch head's depth. Existing sibling
878
895
  // gutters remain visible; terminal gutters remain terminated.
879
896
 
897
+ // Branch points that can draw a gutter, indexed by rendered level: the
898
+ // ancestor list is as deep as the conversation, so it is scanned once
899
+ // per row rather than once per prefix cell.
900
+ // oxlint-disable-next-line unicorn/no-new-array -- length preallocation
901
+ const gutterByLevel: (TreeAncestor<string> | undefined)[] = new Array(renderedIndent);
902
+ for (const ancestor of row.ancestors) {
903
+ if (ancestor.siblingCount <= 1) continue;
904
+ if (this.#multipleRoots && this.#rootIds.has(ancestor.key)) continue;
905
+ const level = ancestor.depth - 1 - scrollOffset;
906
+ if (level >= 0 && level < renderedIndent && gutterByLevel[level] === undefined) {
907
+ gutterByLevel[level] = ancestor;
908
+ }
909
+ }
880
910
  // Build prefix char by char, placing gutters and connector at their positions
881
911
  const totalChars = renderedIndent * 3;
882
912
  const prefixChars: string[] = [];
883
913
  for (let i = 0; i < totalChars; i++) {
884
914
  const level = Math.floor(i / 3);
885
- const originalDepth = level + scrollOffset;
886
915
  const posInLevel = i % 3;
887
-
888
- // An ancestor branch point draws its gutter at its own depth minus
889
- // one — the level its connector occupied. Session roots never emit
890
- // gutters; their connectors are suppressed.
891
- const gutterAncestor = row.ancestors.find(
892
- ancestor =>
893
- ancestor.depth - 1 === originalDepth &&
894
- ancestor.siblingCount > 1 &&
895
- !(this.#multipleRoots && this.#rootIds.has(ancestor.key)),
896
- );
916
+ const gutterAncestor = gutterByLevel[level];
897
917
  if (gutterAncestor) {
898
918
  // Gutters follow standard tree semantics: `│` only while more
899
919
  // siblings continue below, space below a `└─`.
@@ -1036,12 +1056,12 @@ class TreeList implements Component {
1036
1056
  }
1037
1057
 
1038
1058
  #extractContent(content: unknown): string {
1039
- return this.#joinTextContent(content).slice(0, SEARCH_TEXT_LIMIT);
1059
+ return this.#joinTextContent(content, SEARCH_TEXT_LIMIT);
1040
1060
  }
1041
1061
 
1042
- /** Concatenate every text block (or return a string as-is) with no length cap. */
1043
- #joinTextContent(content: unknown): string {
1044
- if (typeof content === "string") return content;
1062
+ /** Concatenate text blocks, stopping as soon as `limit` characters exist. */
1063
+ #joinTextContent(content: unknown, limit = Number.POSITIVE_INFINITY): string {
1064
+ if (typeof content === "string") return content.length > limit ? content.slice(0, limit) : content;
1045
1065
  if (Array.isArray(content)) {
1046
1066
  let result = "";
1047
1067
  for (const c of content) {
@@ -1054,6 +1074,7 @@ class TreeList implements Component {
1054
1074
  typeof c.text === "string"
1055
1075
  ) {
1056
1076
  result += c.text;
1077
+ if (result.length >= limit) return result.slice(0, limit);
1057
1078
  }
1058
1079
  }
1059
1080
  return result;
@@ -43,7 +43,7 @@ import { editorKey, editorKeys } from "../chrome/keybinding-hints";
43
43
  import type { TspSpan, TspTableColumn, TspText, TspTone } from "@linxiraos/pi-wire";
44
44
  import { col, elapsed, node, span, text } from "../native/describe";
45
45
  import { type DescribeContext, leafKey, type NativeChild, type NativeNode, type NativeUiEvent } from "../native/node";
46
- import { actionButton } from "../native/overlay";
46
+ import { actionBar, actionButton } from "../native/overlay";
47
47
 
48
48
  /** Local calendar-day activity consumed by the usage heatmap. */
49
49
  export interface DailyActivityPoint {
@@ -84,10 +84,12 @@ export interface ProviderCard {
84
84
  /** Number of represented accounts, including unavailable usage lookups. */
85
85
  accounts: number;
86
86
  unavailableAccounts: string[];
87
- /** Window rows sorted most-pressing first. */
87
+ /** Window rows in provider-declared order (e.g. 5h → weekly → monthly); the fullest CARD_MAX_WINDOWS lead. */
88
88
  windows: CardWindowRow[];
89
- /** True when every account reports no limits (e.g. enterprise plans). */
89
+ /** True when every account reports no limits (e.g. enterprise plans, providers without a quota API). */
90
90
  unlimited: boolean;
91
+ /** Distinct report notes across accounts (e.g. why a provider exposes no quota windows). */
92
+ notes: string[];
91
93
  /** True when nothing is used anywhere (or there are no limits): collapses to a tick. */
92
94
  idle: boolean;
93
95
  resetCredits?: {
@@ -146,8 +148,10 @@ function compactWindowTag(window: NonNullable<UsageLimit["window"]>): string {
146
148
  * Collapse usage reports into one compact card per provider: limits grouped by
147
149
  * quota bucket (label + window), each bucket showing the mean used fraction
148
150
  * across accounts (matching the classic report's aggregate "% free") with the
149
- * most-used account's reset countdown. Cards sort most-pressing first so
150
- * what's burning is on top-left; fully idle providers collapse into a tick.
151
+ * most-used account's reset countdown. Rows keep the provider's declared window
152
+ * order so 5h → week → month reads the same every time; cards sort by their
153
+ * fullest window so what's burning is on top-left; fully idle providers
154
+ * collapse into a tick.
151
155
  */
152
156
  export function buildProviderCards(
153
157
  reports: UsageReport[],
@@ -181,7 +185,7 @@ export function buildProviderCards(
181
185
  }
182
186
  }
183
187
 
184
- const windows: CardWindowRow[] = [...buckets.values()].map(bucket => {
188
+ const declared: CardWindowRow[] = [...buckets.values()].map(bucket => {
185
189
  const fractions = bucket.limits
186
190
  .map(limit => resolveUsedFraction(limit))
187
191
  .filter((value): value is number => value !== undefined);
@@ -200,7 +204,13 @@ export function buildProviderCards(
200
204
  usedText: fraction === undefined ? formatAbsoluteOnlyAmount(bucket.limits) : undefined,
201
205
  };
202
206
  });
203
- windows.sort((a, b) => (b.fraction ?? -1) - (a.fraction ?? -1));
207
+ // Cards render at most CARD_MAX_WINDOWS rows; when a provider declares
208
+ // more, keep the fullest ones visible (still in declared order) so an
209
+ // exhausted bucket never hides behind "+N more".
210
+ const visible = new Set(
211
+ [...declared].sort((a, b) => (b.fraction ?? -1) - (a.fraction ?? -1)).slice(0, CARD_MAX_WINDOWS),
212
+ );
213
+ const windows = [...declared.filter(row => visible.has(row)), ...declared.filter(row => !visible.has(row))];
204
214
  // The window tag earns its columns only when sibling rows would otherwise
205
215
  // be indistinguishable (e.g. Antigravity's daily vs weekly "Usage (Google)").
206
216
  for (const window of windows) {
@@ -254,6 +264,7 @@ export function buildProviderCards(
254
264
  unavailableAccounts: unavailable,
255
265
  windows,
256
266
  unlimited: windows.length === 0 && unavailable.length === 0,
267
+ notes: [...new Set(providerReports.flatMap(report => report.notes ?? []))],
257
268
  idle:
258
269
  unavailable.length === 0 &&
259
270
  !resetCredits &&
@@ -264,9 +275,12 @@ export function buildProviderCards(
264
275
  });
265
276
  }
266
277
 
278
+ const worstFraction = new Map(
279
+ cards.map(card => [card, card.windows.reduce((max, window) => Math.max(max, window.fraction ?? -1), -1)]),
280
+ );
267
281
  cards.sort((a, b) => {
268
- const aWorst = a.windows[0]?.fraction ?? -1;
269
- const bWorst = b.windows[0]?.fraction ?? -1;
282
+ const aWorst = worstFraction.get(a)!;
283
+ const bWorst = worstFraction.get(b)!;
270
284
  if (aWorst !== bWorst) return bWorst - aWorst;
271
285
  return a.name.localeCompare(b.name);
272
286
  });
@@ -440,6 +454,25 @@ function mutedText(content: string, wrap = false): NativeNode {
440
454
  return text([span(content, "muted")], wrap ? { wrap: "word" } : { truncate: "end" });
441
455
  }
442
456
 
457
+ /**
458
+ * A quiet line under the provider grid naming the providers that get no
459
+ * frame (`Untouched: Kimi Code, Zai`); `title` says why on hover.
460
+ */
461
+ function footnote(key: string, label: string, cards: readonly ProviderCard[], title?: string): NativeNode {
462
+ const names = cards.map(card => card.name).join(", ");
463
+ return node(
464
+ "text",
465
+ {
466
+ spans: [span(`${label}: `, "dim"), span(names)],
467
+ wrap: "word",
468
+ role: "zeta.usage.footnote",
469
+ ...(title ? { title: sanitizeDisplayLine(title) } : {}),
470
+ },
471
+ undefined,
472
+ key,
473
+ );
474
+ }
475
+
443
476
  /** Stable, human account name for a report in the detail view. */
444
477
  function reportAccountLabel(report: UsageReport, limit: UsageLimit | undefined, index: number): string {
445
478
  const metadata = report.metadata;
@@ -463,16 +496,14 @@ function detailWindowLabel(label: string, limit: UsageLimit): string | undefined
463
496
  return sanitizeDisplayLine(windowLabel);
464
497
  }
465
498
 
499
+ const WHOLE_DOLLARS = new Intl.NumberFormat("en-US", { maximumFractionDigits: 0 });
500
+ const COMPACT_COUNT = new Intl.NumberFormat("en-US", { notation: "compact", maximumFractionDigits: 1 });
501
+
466
502
  /** `$1,234 · 5.6K requests` totals for the activity summary. */
467
503
  function formatActivityTotals(layout: HeatmapLayout): string {
468
504
  const cost =
469
- layout.totalCost >= 1
470
- ? `$${new Intl.NumberFormat("en-US", { maximumFractionDigits: 0 }).format(layout.totalCost)}`
471
- : `$${layout.totalCost.toFixed(2)}`;
472
- const requests = new Intl.NumberFormat("en-US", { notation: "compact", maximumFractionDigits: 1 }).format(
473
- layout.totalRequests,
474
- );
475
- return `${cost} · ${requests} requests`;
505
+ layout.totalCost >= 1 ? `$${WHOLE_DOLLARS.format(layout.totalCost)}` : `$${layout.totalCost.toFixed(2)}`;
506
+ return `${cost} · ${COMPACT_COUNT.format(layout.totalRequests)} requests`;
476
507
  }
477
508
 
478
509
  // =============================================================================
@@ -547,6 +578,8 @@ export class UsageDashboardComponent implements Component {
547
578
  #activityError: string | null = null;
548
579
  #syncing = true;
549
580
  #detailCache: { width: number; lines: string[] } | null = null;
581
+ /** ANSI overview rows; rebuilt when the revision or width changes. */
582
+ #overviewCache: { revision: number; width: number; lines: string[] } | null = null;
550
583
  #lastViewportRows = 10;
551
584
  #closed = false;
552
585
  readonly #panel: OverlayPanel;
@@ -592,6 +625,7 @@ export class UsageDashboardComponent implements Component {
592
625
 
593
626
  invalidate(): void {
594
627
  this.#detailCache = null;
628
+ this.#overviewCache = null;
595
629
  this.#panel.invalidate();
596
630
  }
597
631
 
@@ -836,15 +870,8 @@ export class UsageDashboardComponent implements Component {
836
870
  const ramp = this.#heatRamp();
837
871
  const reset = "\x1b[39m";
838
872
 
839
- const cost =
840
- layout.totalCost >= 1
841
- ? `$${new Intl.NumberFormat("en-US", { maximumFractionDigits: 0 }).format(layout.totalCost)}`
842
- : `$${layout.totalCost.toFixed(2)}`;
843
- const requests = new Intl.NumberFormat("en-US", { notation: "compact", maximumFractionDigits: 1 }).format(
844
- layout.totalRequests,
845
- );
846
873
  summary.push(
847
- `${theme.bold(theme.fg("accent", "Activity"))} ${theme.fg("dim", `${cost} · ${requests} requests · last ${weeks} weeks`)}${this.#syncing ? theme.fg("dim", " · syncing…") : ""}`,
874
+ `${theme.bold(theme.fg("accent", "Activity"))} ${theme.fg("dim", `${formatActivityTotals(layout)} · last ${weeks} weeks`)}${this.#syncing ? theme.fg("dim", " · syncing…") : ""}`,
848
875
  );
849
876
  summary.push("");
850
877
 
@@ -876,10 +903,13 @@ export class UsageDashboardComponent implements Component {
876
903
  // ---------------------------------------------------------------------------
877
904
 
878
905
  #overviewLines(innerWidth: number): string[] {
906
+ const cached = this.#overviewCache;
907
+ if (cached?.revision === this.#revision && cached.width === innerWidth) return cached.lines;
879
908
  const lines: string[] = [];
880
909
  lines.push(...this.#renderCardsGrid(innerWidth));
881
910
  lines.push("");
882
911
  lines.push(...this.#renderHeatmap(innerWidth));
912
+ this.#overviewCache = { revision: this.#revision, width: innerWidth, lines };
883
913
  return lines;
884
914
  }
885
915
 
@@ -935,9 +965,9 @@ export class UsageDashboardComponent implements Component {
935
965
  * The sheet body (the terminal's `lg` overlay is the frame): a head row
936
966
  * (checked ago, Overview/Details tabs, Refresh), then either the provider
937
967
  * grid of frames with window meters and the activity heatmap, or the
938
- * per-provider detail tables. `meter`/`chart` fall back to
939
- * `progress`/`table` on terminals without them. Rebuilt only when its
940
- * inputs change.
968
+ * per-provider detail tables, then Close like the other report sheets.
969
+ * `meter`/`chart` fall back to `progress`/`table` on terminals without
970
+ * them. Rebuilt only when its inputs change.
941
971
  */
942
972
  describe(cx: DescribeContext): NativeNode {
943
973
  const meter = cx.supports("meter");
@@ -947,15 +977,20 @@ export class UsageDashboardComponent implements Component {
947
977
  return cache.node;
948
978
  }
949
979
  const body = this.#view === "detail" ? this.#describeDetail() : this.#describeOverview(meter, chart);
950
- const root = col([this.#describeHead(), node("col", { gap: "lg" }, body, this.#view)], { gap: "lg" });
980
+ // Esc leaves Details for Overview first, so only Overview's Close wears its keycap.
981
+ const close = actionButton("Close", "close", this.#view === "overview" ? { keys: "escape" } : {});
982
+ const root = col([this.#describeHead(), node("col", { gap: "lg" }, body, this.#view), actionBar([null, close])], {
983
+ gap: "lg",
984
+ });
951
985
  this.#nativeCache = { revision: this.#revision, meter, chart, node: root };
952
986
  return root;
953
987
  }
954
988
 
955
- /** Tab clicks switch views like Enter/Esc; the Refresh button runs `r`. */
989
+ /** Tab clicks switch views like Enter/Esc; the Refresh button runs `r`; Close closes from either view. */
956
990
  handleNativeEvent(event: NativeUiEvent): void {
957
991
  if (event.type === "action") {
958
992
  if (event.act === "refresh") void this.#refresh();
993
+ else if (event.act === "close") this.#close();
959
994
  return;
960
995
  }
961
996
  if (event.type !== "select" && event.type !== "activate") return;
@@ -1007,41 +1042,59 @@ export class UsageDashboardComponent implements Component {
1007
1042
  const children: NativeChild[] = [];
1008
1043
  if (this.#cards.length === 0) {
1009
1044
  children.push(
1010
- node(
1011
- "text",
1012
- { spans: [span("No usage data available.")], role: "zeta.usage.untouched" },
1013
- undefined,
1014
- "none",
1015
- ),
1045
+ node("text", { spans: [span("No usage data available.")], role: "zeta.usage.footnote" }, undefined, "none"),
1016
1046
  );
1017
1047
  } else {
1018
- // Unlimited providers keep a frame reading "No limits"; only untouched ones collapse.
1019
- const active = this.#cards.filter(entry => !entry.idle || entry.unlimited);
1020
- const idle = this.#cards.filter(entry => entry.idle && !entry.unlimited);
1021
- if (active.length > 0) {
1048
+ // Only providers with something to show get a frame: the rest are
1049
+ // named in footnotes, whether untouched, unreadable or quota-less.
1050
+ const framed: ProviderCard[] = [];
1051
+ const untouched: ProviderCard[] = [];
1052
+ const unreported: ProviderCard[] = [];
1053
+ const unmetered: ProviderCard[] = [];
1054
+ for (const entry of this.#cards) {
1055
+ const shown =
1056
+ entry.windows.length > 0 || entry.resetCredits !== undefined || entry.daybreakAccounts !== undefined;
1057
+ if (!shown) (entry.unavailableAccounts.length > 0 ? unreported : unmetered).push(entry);
1058
+ else if (entry.idle) untouched.push(entry);
1059
+ else framed.push(entry);
1060
+ }
1061
+ if (framed.length > 0) {
1022
1062
  children.push(
1023
1063
  node(
1024
1064
  "row",
1025
1065
  { wrap: true, gap: "md", role: "zeta.usage.grid" },
1026
- active.map(entry => this.#describeCard(entry, meter)),
1066
+ framed.map(entry => this.#describeCard(entry, meter)),
1027
1067
  "providers",
1028
1068
  ),
1029
1069
  );
1030
1070
  }
1031
- if (idle.length > 0) {
1032
- children.push(
1033
- node(
1034
- "text",
1035
- {
1036
- spans: [span(`Untouched: ${idle.map(entry => entry.name).join(", ")}`)],
1037
- wrap: "word",
1038
- role: "zeta.usage.untouched",
1039
- },
1040
- undefined,
1041
- "idle",
1071
+ const notes: NativeNode[] = [];
1072
+ if (untouched.length > 0) notes.push(footnote("idle", "Untouched", untouched));
1073
+ if (unreported.length > 0) {
1074
+ const accounts = unreported.flatMap(entry =>
1075
+ entry.unavailableAccounts.map(account => `${entry.name} (${account})`),
1076
+ );
1077
+ notes.push(
1078
+ footnote(
1079
+ "unreported",
1080
+ "No usage data",
1081
+ unreported,
1082
+ `No usage report came back for ${accounts.join(", ")}: the sign-in expired, the request failed, or the plan has no quotas`,
1083
+ ),
1084
+ );
1085
+ }
1086
+ if (unmetered.length > 0) {
1087
+ const why = unmetered.flatMap(entry => entry.notes.map(note => `${entry.name}: ${note}`));
1088
+ notes.push(
1089
+ footnote(
1090
+ "unmetered",
1091
+ "No quotas reported",
1092
+ unmetered,
1093
+ why.length > 0 ? why.join(" • ") : "These providers report no quota windows to track",
1042
1094
  ),
1043
1095
  );
1044
1096
  }
1097
+ if (notes.length > 0) children.push(node("col", { gap: "xs" }, notes, "footnotes"));
1045
1098
  }
1046
1099
  children.push(this.#describeActivity(chart));
1047
1100
  return children;
@@ -1092,10 +1145,10 @@ export class UsageDashboardComponent implements Component {
1092
1145
  );
1093
1146
  }
1094
1147
  for (const account of entry.unavailableAccounts) {
1095
- children.push(mutedText(`${sanitizeDisplayLine(account)}: usage unavailable`, true));
1148
+ children.push(mutedText(`${sanitizeDisplayLine(account)}: no usage data`, true));
1096
1149
  }
1097
1150
  if (entry.unlimited) {
1098
- children.push(mutedText("No limits"));
1151
+ children.push(mutedText("No quotas reported"));
1099
1152
  } else {
1100
1153
  for (const [index, window] of entry.windows.slice(0, CARD_MAX_WINDOWS).entries()) {
1101
1154
  const label: TspSpan[] = [span(sanitizeDisplayLine(window.label))];
@@ -1104,7 +1157,9 @@ export class UsageDashboardComponent implements Component {
1104
1157
  text(label, { role: "zeta.usage.label", truncate: "middle", title: sanitizeDisplayLine(window.label) }),
1105
1158
  ];
1106
1159
  if (window.fraction === undefined) {
1107
- cells.push(text([span(window.usedText ?? "No data", "muted")], { truncate: "end" }));
1160
+ cells.push(
1161
+ text([span(window.usedText ?? "No data", "muted")], { role: "zeta.usage.amount", truncate: "end" }),
1162
+ );
1108
1163
  } else {
1109
1164
  const token =
1110
1165
  window.status === "exhausted" ? "error" : window.status === "warning" ? "warning" : undefined;
@@ -1221,7 +1276,7 @@ export class UsageDashboardComponent implements Component {
1221
1276
  const children: NativeChild[] = [];
1222
1277
  for (const account of unavailable) {
1223
1278
  if (account.provider !== entry.provider) continue;
1224
- children.push(mutedText(`${sanitizeDisplayLine(account.label)}: usage unavailable`, true));
1279
+ children.push(mutedText(`${sanitizeDisplayLine(account.label)}: no usage data`, true));
1225
1280
  }
1226
1281
 
1227
1282
  const facts: { k: TspText; v: TspText }[] = [];
@@ -1313,7 +1368,7 @@ export class UsageDashboardComponent implements Component {
1313
1368
  children.push(node("table", { cols, rows }, undefined, "limits"));
1314
1369
  for (const note of new Set(limitNotes)) children.push(mutedText(note, true));
1315
1370
  } else if (unavailable.every(account => account.provider !== entry.provider)) {
1316
- children.push(mutedText("No limits"));
1371
+ children.push(mutedText("No quotas reported"));
1317
1372
  }
1318
1373
  sections.push(
1319
1374
  node(
@@ -1384,8 +1439,7 @@ export class UsageDashboardComponent implements Component {
1384
1439
  this.#setView("overview");
1385
1440
  return;
1386
1441
  }
1387
- this.dispose();
1388
- this.#options.onClose();
1442
+ this.#close();
1389
1443
  return;
1390
1444
  }
1391
1445
  if (matchesKey(data, "r")) {
@@ -1408,4 +1462,9 @@ export class UsageDashboardComponent implements Component {
1408
1462
  this.#options.requestRender();
1409
1463
  } else if (matchesKey(data, "end")) this.#scrollBy(Number.MAX_SAFE_INTEGER);
1410
1464
  }
1465
+
1466
+ #close(): void {
1467
+ this.dispose();
1468
+ this.#options.onClose();
1469
+ }
1411
1470
  }
@@ -1,4 +1,4 @@
1
- import type { UsageLimit, UsageReport, UsageResetCredits } from "@linxiraos/pi-ai";
1
+ import type { UsageLimit, UsageReport, UsageResetCreditDetail, UsageResetCredits } from "@linxiraos/pi-ai";
2
2
 
3
3
  /** Include the usage tier in a limit title unless its label already names it. */
4
4
  export function formatLimitTitle(limit: UsageLimit): string {
@@ -51,6 +51,13 @@ export interface UsageResetSummary {
51
51
  unavailableReason?: string;
52
52
  }
53
53
 
54
+ /** Expiry (epoch ms) of a credit that still banks a reset, whether or not it can be spent now. */
55
+ export function bankedResetCreditExpiryMs(credit: UsageResetCreditDetail): number | undefined {
56
+ if (!credit.expiresAt || credit.remainingCount === 0 || credit.status === "redeemed") return undefined;
57
+ const expiryMs = Date.parse(credit.expiresAt);
58
+ return Number.isFinite(expiryMs) ? expiryMs : undefined;
59
+ }
60
+
54
61
  /**
55
62
  * Normalize old count-only and current provider reset metadata for display.
56
63
  * `availableCount` is banked inventory; `redeemableCount` is the subset that
@@ -68,9 +75,8 @@ export function summarizeUsageResetCredits(
68
75
  let latestExpired: string | undefined;
69
76
  let latestExpiredMs = Number.NEGATIVE_INFINITY;
70
77
  for (const credit of reset.credits ?? []) {
71
- if (!credit.expiresAt || credit.remainingCount === 0 || credit.status === "redeemed") continue;
72
- const expiryMs = Date.parse(credit.expiresAt);
73
- if (!Number.isFinite(expiryMs)) continue;
78
+ const expiryMs = bankedResetCreditExpiryMs(credit);
79
+ if (expiryMs === undefined) continue;
74
80
  if (expiryMs > nowMs && expiryMs < soonestExpiryMs) {
75
81
  soonestExpiryMs = expiryMs;
76
82
  soonestExpiry = credit.expiresAt;
@@ -5,17 +5,19 @@ import { formatDuration, formatNumber } from "@linxiraos/pi-utils";
5
5
  import { theme } from "../theme/theme";
6
6
  import { formatMetricRow, MetricRow, type MetricSpec } from "../components/metric";
7
7
  import { node, row, span, text } from "../native/describe";
8
- import type { NativeChild, NativeNode } from "../native/node";
8
+ import type { DescribeContext, NativeChild, NativeNode } from "../native/node";
9
9
 
10
10
  /** Below this the rate is nonsense (cached/instant responses yield absurd tok/s). */
11
11
  const MIN_DURATION_MS = 100;
12
12
 
13
- /** Local `YYYY-MM-DD HH:mm:ss` stamp for the per-turn usage row. */
14
- function formatUsageTimestamp(ms: number): string {
13
+ /** Local ISO date with a wall-clock time; only the hour cycle follows the terminal. */
14
+ function formatUsageTimestamp(ms: number, hour12 = false): string {
15
15
  const d = new Date(ms);
16
16
  const pad = (n: number): string => String(n).padStart(2, "0");
17
17
  const date = `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
18
- const time = `${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
18
+ const time = hour12
19
+ ? d.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", second: "2-digit", hour12 })
20
+ : `${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
19
21
  return `${date} ${time}`;
20
22
  }
21
23
 
@@ -161,6 +163,8 @@ class UsageRowBlock extends Container {
161
163
  readonly #timestamp: number | undefined;
162
164
  readonly #turnElapsedMs: number | undefined;
163
165
  #nativeNode: NativeNode | undefined;
166
+ /** The terminal's clock {@link #nativeNode} was described with. */
167
+ #nativeHour12: boolean | undefined;
164
168
 
165
169
  constructor(usage: Usage, durationMs?: number, ttftMs?: number, timestamp?: number, turnElapsedMs?: number) {
166
170
  super();
@@ -186,13 +190,23 @@ class UsageRowBlock extends Container {
186
190
  * ttft 0.9s · 96 tok/s`): worded metrics instead of the ANSI icons, the
187
191
  * full timestamp in the tooltip, and throughput as a `rate`.
188
192
  */
189
- override describe(): NativeNode {
190
- if (this.#nativeNode) return this.#nativeNode;
193
+ override describe(cx?: DescribeContext): NativeNode {
194
+ // The terminal's clock: the process locale doesn't reliably carry the user's 12/24-hour choice.
195
+ const hour12 = cx?.hour12;
196
+ if (this.#nativeNode && this.#nativeHour12 === hour12) return this.#nativeNode;
197
+ this.#nativeHour12 = hour12;
191
198
  const usage = this.#usage;
192
199
  const parts: string[] = [];
193
200
  const timestamp = this.#timestamp;
194
201
  const stamped = timestamp !== undefined && Number.isFinite(timestamp) && timestamp > 0;
195
- if (stamped) parts.push(formatUsageTimestamp(timestamp).slice(11, 16));
202
+ // Only a 12-hour clock goes through the locale; 24-hour keeps the tooltip's own `HH:mm`.
203
+ if (stamped) {
204
+ parts.push(
205
+ hour12
206
+ ? new Date(timestamp).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", hour12 })
207
+ : formatUsageTimestamp(timestamp).slice(11, 16),
208
+ );
209
+ }
196
210
  if (this.#turnElapsedMs !== undefined && this.#turnElapsedMs > 0) {
197
211
  parts.push(`Δ ${formatDuration(Math.round(this.#turnElapsedMs))}`);
198
212
  }
@@ -211,7 +225,7 @@ class UsageRowBlock extends Container {
211
225
  role: "zeta.usage.turn",
212
226
  gap: "none",
213
227
  align: "baseline",
214
- ...(stamped ? { title: formatUsageTimestamp(timestamp) } : {}),
228
+ ...(stamped ? { title: formatUsageTimestamp(timestamp, hour12) } : {}),
215
229
  });
216
230
  return this.#nativeNode;
217
231
  }
@@ -12,7 +12,7 @@ import {
12
12
  visibleWidth,
13
13
  } from "../index";
14
14
  import { fileHyperlink } from "../render/hyperlink";
15
- import { registerNativeBlob } from "../native/blobs";
15
+ import { nativeImageNode } from "../native/blobs";
16
16
  import { node, row, span } from "../native/describe";
17
17
  import { plainText } from "../native/spans";
18
18
  import type { DescribeContext, NativeNode } from "../native/node";
@@ -33,12 +33,9 @@ const RESET_FG = "\x1b[39m";
33
33
  * (pastes are usually re-encoded JPEG/WebP) convert before transmit — the same pipeline
34
34
  * the transcript uses. `null` = conversion in flight or failed. */
35
35
  const kImagePng = Symbol("zeta.imagePng");
36
- /** Content address of the draft image's decoded bytes, registered once for TSP `image` nodes. */
37
- const kImageBlob = Symbol("zeta.imageBlob");
38
36
 
39
37
  interface ImageContentWithPng extends ImageContent {
40
38
  [kImagePng]?: ImageContent | null;
41
- [kImageBlob]?: string;
42
39
  }
43
40
 
44
41
  /**
@@ -57,7 +54,9 @@ export class AttachmentChipsBand implements Component {
57
54
  private readonly requestRender: () => void,
58
55
  ) {}
59
56
 
60
- #native: { chips: readonly ComposerChipDescriptor[]; node: NativeNode } | undefined;
57
+ #native:
58
+ | { chips: readonly ComposerChipDescriptor[]; node: NativeNode; images: ReadonlyMap<ImageContent, NativeNode> }
59
+ | undefined;
61
60
 
62
61
  /**
63
62
  * A wrapping `row` of chip `card`s (`zeta.composer.chip`) titled with the
@@ -69,6 +68,7 @@ export class AttachmentChipsBand implements Component {
69
68
  const chips = this.editor.composerChips();
70
69
  if (this.#native?.chips === chips) return this.#native.node;
71
70
  const cards: NativeNode[] = [];
71
+ const images = new Map<ImageContent, NativeNode>();
72
72
  for (const chip of chips) {
73
73
  const icon = theme.symbol(
74
74
  chip.kind === "paste" ? "chip.paste" : chip.kind === "video" ? "chip.video" : "chip.image",
@@ -86,11 +86,14 @@ export class AttachmentChipsBand implements Component {
86
86
  } else {
87
87
  const dims = this.#imageDims(chip.image);
88
88
  caption = dims ? `${dims.width}x${dims.height}` : "";
89
- const image = chip.image as ImageContentWithPng;
90
- const blob = image[kImageBlob] ?? registerNativeBlob(Buffer.from(image.data, "base64"), image.mimeType);
91
- image[kImageBlob] = blob;
89
+ const image = chip.image;
90
+ const native =
91
+ images.get(image) ??
92
+ this.#native?.images.get(image) ??
93
+ nativeImageNode(Buffer.from(image.data, "base64"), image.mimeType);
94
+ images.set(image, native);
92
95
  content = node("image", {
93
- blob,
96
+ ...native.p,
94
97
  alt: title,
95
98
  w: dims?.width,
96
99
  h: dims?.height,
@@ -106,7 +109,7 @@ export class AttachmentChipsBand implements Component {
106
109
  );
107
110
  }
108
111
  const described = row(cards, { gap: "sm", wrap: true, role: "zeta.composer.chips", hidden: cards.length === 0 });
109
- this.#native = { chips, node: described };
112
+ this.#native = { chips, node: described, images };
110
113
  return described;
111
114
  }
112
115
 
@@ -249,7 +252,8 @@ export class AttachmentChipsBand implements Component {
249
252
 
250
253
  /** Leading 4 rows x 12 cols of the pasted text, muted. */
251
254
  #textInterior(entry: TextAttachment): string[] {
252
- const lines = entry.content.split("\n");
255
+ // Only the leading rows are drawn; don't split a multi-KB paste every frame.
256
+ const lines = entry.content.split("\n", INNER_ROWS);
253
257
  const rows: string[] = [];
254
258
  for (let r = 0; r < INNER_ROWS; r++) {
255
259
  const cut = truncateToWidth(replaceTabs(lines[r] ?? ""), INNER_COLS);