@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
@@ -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
 
@@ -1012,41 +1042,59 @@ export class UsageDashboardComponent implements Component {
1012
1042
  const children: NativeChild[] = [];
1013
1043
  if (this.#cards.length === 0) {
1014
1044
  children.push(
1015
- node(
1016
- "text",
1017
- { spans: [span("No usage data available.")], role: "zeta.usage.untouched" },
1018
- undefined,
1019
- "none",
1020
- ),
1045
+ node("text", { spans: [span("No usage data available.")], role: "zeta.usage.footnote" }, undefined, "none"),
1021
1046
  );
1022
1047
  } else {
1023
- // Unlimited providers keep a frame reading "No limits"; only untouched ones collapse.
1024
- const active = this.#cards.filter(entry => !entry.idle || entry.unlimited);
1025
- const idle = this.#cards.filter(entry => entry.idle && !entry.unlimited);
1026
- 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) {
1027
1062
  children.push(
1028
1063
  node(
1029
1064
  "row",
1030
1065
  { wrap: true, gap: "md", role: "zeta.usage.grid" },
1031
- active.map(entry => this.#describeCard(entry, meter)),
1066
+ framed.map(entry => this.#describeCard(entry, meter)),
1032
1067
  "providers",
1033
1068
  ),
1034
1069
  );
1035
1070
  }
1036
- if (idle.length > 0) {
1037
- children.push(
1038
- node(
1039
- "text",
1040
- {
1041
- spans: [span(`Untouched: ${idle.map(entry => entry.name).join(", ")}`)],
1042
- wrap: "word",
1043
- role: "zeta.usage.untouched",
1044
- },
1045
- undefined,
1046
- "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`,
1047
1083
  ),
1048
1084
  );
1049
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",
1094
+ ),
1095
+ );
1096
+ }
1097
+ if (notes.length > 0) children.push(node("col", { gap: "xs" }, notes, "footnotes"));
1050
1098
  }
1051
1099
  children.push(this.#describeActivity(chart));
1052
1100
  return children;
@@ -1097,10 +1145,10 @@ export class UsageDashboardComponent implements Component {
1097
1145
  );
1098
1146
  }
1099
1147
  for (const account of entry.unavailableAccounts) {
1100
- children.push(mutedText(`${sanitizeDisplayLine(account)}: usage unavailable`, true));
1148
+ children.push(mutedText(`${sanitizeDisplayLine(account)}: no usage data`, true));
1101
1149
  }
1102
1150
  if (entry.unlimited) {
1103
- children.push(mutedText("No limits"));
1151
+ children.push(mutedText("No quotas reported"));
1104
1152
  } else {
1105
1153
  for (const [index, window] of entry.windows.slice(0, CARD_MAX_WINDOWS).entries()) {
1106
1154
  const label: TspSpan[] = [span(sanitizeDisplayLine(window.label))];
@@ -1109,7 +1157,9 @@ export class UsageDashboardComponent implements Component {
1109
1157
  text(label, { role: "zeta.usage.label", truncate: "middle", title: sanitizeDisplayLine(window.label) }),
1110
1158
  ];
1111
1159
  if (window.fraction === undefined) {
1112
- 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
+ );
1113
1163
  } else {
1114
1164
  const token =
1115
1165
  window.status === "exhausted" ? "error" : window.status === "warning" ? "warning" : undefined;
@@ -1226,7 +1276,7 @@ export class UsageDashboardComponent implements Component {
1226
1276
  const children: NativeChild[] = [];
1227
1277
  for (const account of unavailable) {
1228
1278
  if (account.provider !== entry.provider) continue;
1229
- children.push(mutedText(`${sanitizeDisplayLine(account.label)}: usage unavailable`, true));
1279
+ children.push(mutedText(`${sanitizeDisplayLine(account.label)}: no usage data`, true));
1230
1280
  }
1231
1281
 
1232
1282
  const facts: { k: TspText; v: TspText }[] = [];
@@ -1318,7 +1368,7 @@ export class UsageDashboardComponent implements Component {
1318
1368
  children.push(node("table", { cols, rows }, undefined, "limits"));
1319
1369
  for (const note of new Set(limitNotes)) children.push(mutedText(note, true));
1320
1370
  } else if (unavailable.every(account => account.provider !== entry.provider)) {
1321
- children.push(mutedText("No limits"));
1371
+ children.push(mutedText("No quotas reported"));
1322
1372
  }
1323
1373
  sections.push(
1324
1374
  node(
@@ -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;
@@ -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);
@@ -165,6 +165,9 @@ export const COMPOSER_TOKEN_REGEX = new RegExp(
165
165
  "gu",
166
166
  );
167
167
 
168
+ /** Last compiled {@link referencedAttachments} scanner, keyed by its source (labels and glyphs rarely change). */
169
+ let cachedScanner: { source: string; scanner: RegExp } | undefined;
170
+
168
171
  /**
169
172
  * Attachment indices referenced by a composer buffer, per kind. Image/video count compact chips
170
173
  * and expanded markers; paste counts compact chips only, since `[Paste #N]` markers number the
@@ -182,10 +185,9 @@ export function referencedAttachments(
182
185
  const icons = [...kindByIcon.keys()].sort((a, b) => b.length - a.length).map(glyphSource);
183
186
  const labels = [...recorded.keys()].sort((a, b) => b.length - a.length).map(label => RegExp.escape(label));
184
187
  const recordedSource = labels.length > 0 ? labels.join("|") : "(?!)";
185
- const scanner = new RegExp(
186
- `${PLACEHOLDER_REGEX.source}|(${recordedSource})(?!\\d)|(${icons.join("|")}) #([1-9]\\d*)`,
187
- "gu",
188
- );
188
+ const source = `${PLACEHOLDER_REGEX.source}|(${recordedSource})(?!\\d)|(${icons.join("|")}) #([1-9]\\d*)`;
189
+ if (cachedScanner?.source !== source) cachedScanner = { source, scanner: new RegExp(source, "gu") };
190
+ const { scanner } = cachedScanner;
189
191
 
190
192
  const refs: Record<ChipKind, Set<number>> = { image: new Set(), video: new Set(), paste: new Set() };
191
193
  for (const match of text.matchAll(scanner)) {
@@ -15,7 +15,7 @@ import {
15
15
  type TUIOptions,
16
16
  type ViewportSize,
17
17
  } from "../tui";
18
- import { sliceWithWidth, visibleWidth } from "../utils";
18
+ import { wrapLiteralLine } from "../utils";
19
19
  import type { NativeChild, NativeSurface, NativeSurfaceProvider } from "../native/node";
20
20
  import { sameItems } from "../native/memo";
21
21
  import { postmortem } from "@linxiraos/pi-utils";
@@ -82,7 +82,7 @@ export interface ComposerStatusCache {
82
82
  /** Optional dependencies and initial state for a standalone composer. */
83
83
  export interface ComposerOptions {
84
84
  readonly terminal?: Terminal;
85
- /** Extra TUI construction options (render scheduler injection for tests and `zeta render`). */
85
+ /** Extra TUI construction options (render scheduler injection for tests and `zetacode render`). */
86
86
  readonly tuiOptions?: TUIOptions;
87
87
  readonly preferences?: Partial<ComposerPreferences>;
88
88
  readonly welcome?: ComposerWelcomeUpdate;
@@ -812,20 +812,8 @@ export class Composer implements TerminalFrameProvider, NativeSurfaceProvider {
812
812
  if (!lines) return [];
813
813
  if (isInsideTerminalMultiplexer()) return lines.slice(start);
814
814
  const reflowed: string[] = [];
815
- const columns = Math.max(1, width);
816
815
  for (let index = start; index < lines.length; index++) {
817
- const line = lines[index]!;
818
- const lineWidth = visibleWidth(line);
819
- if (lineWidth === 0) {
820
- reflowed.push("");
821
- continue;
822
- }
823
- for (let column = 0; column < lineWidth;) {
824
- let slice = sliceWithWidth(line, column, columns, true);
825
- if (slice.width === 0) slice = sliceWithWidth(line, column, columns);
826
- reflowed.push(slice.text);
827
- column += Math.max(1, slice.width);
828
- }
816
+ reflowed.push(...wrapLiteralLine(lines[index]!, width));
829
817
  }
830
818
  return reflowed;
831
819
  }
@@ -921,6 +909,7 @@ export class Composer implements TerminalFrameProvider, NativeSurfaceProvider {
921
909
 
922
910
  /** Update the canonical editor reference after InteractiveMode remounts a custom editor. */
923
911
  setEditor(editor: CustomEditor): void {
912
+ this.ui.replaceHeldFocus(this.#editor, editor);
924
913
  this.#editor = editor;
925
914
  }
926
915
 
@@ -20,7 +20,6 @@ import type { DescribeContext, NativeNode, NativeUiEvent } from "../native/node"
20
20
  import type { ComposerFacts, ComposerFactsSource } from "../status-line/types";
21
21
  import { allowsModelMentions, allowsSkillTokens, SKILL_TOKEN_RE } from "./skill-tokens";
22
22
  import { expandModelMentionTags, MODEL_MENTION_RE, modelMentionToken } from "./model-mention-syntax";
23
- import { imageAttachmentSource } from "./image-source";
24
23
  import { isVideoPath } from "./video";
25
24
  import {
26
25
  attachmentSgr,
@@ -41,7 +40,7 @@ import {
41
40
  } from "./composer-attachments";
42
41
  import { type MacOSSpellingFeatures, MacOSSpellingProvider } from "./macos-spelling";
43
42
  import { hasMagicKeyword, highlightMagicKeywords, magicKeywordRanges } from "./magic-keywords";
44
- import type { TspEditorDecoration } from "@linxiraos/pi-wire";
43
+ import type { TspEditorDecoration, TspText } from "@linxiraos/pi-wire";
45
44
  import { isNativeRendering } from "../native/state";
46
45
  import { isQueuedMessageList, parseQueueShorthand, QUEUE_LIST_MARKER_RE } from "./queue-input";
47
46
  import { type WordCompletionMethod, WordCompletionProvider } from "./word-completion";
@@ -50,8 +49,8 @@ import { fgOrPlain, theme } from "../theme/theme";
50
49
  /** A shell-mode draft's sigil (`!`, `!!`, `$`, `$$`) with its surrounding blanks; the mode chip stands in for it natively. */
51
50
  const SHELL_SIGIL_RE = /^\s*(?:!!?|\$\$?)[ \t]?/;
52
51
 
53
- /** The composer's TSP placeholder; the rotating ANSI hints (thinking effort, …) move into control tooltips. */
54
- const NATIVE_COMPOSER_PLACEHOLDER = "Ask zeta — / commands · @ files · ! bash";
52
+ /** The untitled session's TSP placeholder; the rotating ANSI hints (thinking effort, …) move into control tooltips. */
53
+ const NATIVE_COMPOSER_PLACEHOLDER = "What are we cooking?";
55
54
 
56
55
  /** Live composer state the TSP layout shows; the interactive host wires {@link CustomEditor.composerState}. */
57
56
  export interface ComposerNativeState {
@@ -67,6 +66,8 @@ export interface ComposerNativeState {
67
66
  readonly running: boolean;
68
67
  /** The viewed subagent's lineage, outermost first and the viewed agent last; undefined on the main session. */
69
68
  readonly viewing?: readonly string[];
69
+ /** The session's title, quoted in italics as the TSP placeholder; undefined until it has one. */
70
+ readonly title?: string;
70
71
  }
71
72
 
72
73
  /** Action code prefix of the viewing header's links: `focus:<agent id>`, {@link MAIN_AGENT_ID} for the main session. */
@@ -220,7 +221,7 @@ function normalizePastedPath(path: string): string {
220
221
  }
221
222
  }
222
223
  }
223
- return unquoted.replace(SHELL_ESCAPED_PATH_CHAR_REGEX, "$1");
224
+ return (unquoted.startsWith("\\~/") ? unquoted.slice(1) : unquoted).replace(SHELL_ESCAPED_PATH_CHAR_REGEX, "$1");
224
225
  }
225
226
 
226
227
  function isExplicitPastedPath(path: string): boolean {
@@ -408,7 +409,7 @@ export function extractImagePathFromText(text: string): string | undefined {
408
409
 
409
410
  /**
410
411
  * Resolve the {@link EditorTheme} from a `CustomEditor`/`Editor` constructor
411
- * argument list, tolerating both the zeta `(theme)` and upstream-pi
412
+ * argument list, tolerating both the omp `(theme)` and upstream-pi
412
413
  * `(tui, theme, keybindings)` conventions (see {@link CustomEditor}'s
413
414
  * constructor). A real `EditorTheme` is identified structurally — it exposes a
414
415
  * `borderColor` function and a `symbols` object — so a `TUI` passed in the first
@@ -478,13 +479,13 @@ export class CustomEditor extends Editor {
478
479
  chips: ComposerChipDescriptor[];
479
480
  }
480
481
  | undefined;
481
- /** Host-wired producer of per-image `file://` links (session blob store); drives clickable
482
- * chip tokens for restored drafts (esc-esc, `/tree`, branch). */
482
+ /** Host-wired producer of per-image chip targets (a file on disk, or a session blob copy);
483
+ * drives clickable chip tokens for restored drafts (esc-esc, `/tree`, branch). */
483
484
  draftImageLinkMaterializer?: (images: readonly ImageContent[]) => Promise<(string | undefined)[] | undefined>;
484
485
 
485
486
  /**
486
487
  * The host {@link TUI}, captured when a plugin constructs this editor through
487
- * the upstream-pi `(tui, theme, keybindings)` convention. Undefined for zeta's
488
+ * the upstream-pi `(tui, theme, keybindings)` convention. Undefined for omp's
488
489
  * own `new CustomEditor(theme)` callers (they drive repaints through the
489
490
  * interactive-mode wiring instead). Plugins that call `this.tui.requestRender()`
490
491
  * in their overrides read it here (issue #4766).
@@ -492,7 +493,7 @@ export class CustomEditor extends Editor {
492
493
  tui?: TUI;
493
494
 
494
495
  /**
495
- * Accept both the zeta constructor convention — `new CustomEditor(theme)` —
496
+ * Accept both the omp constructor convention — `new CustomEditor(theme)` —
496
497
  * and the upstream-pi `Editor` convention — `new Editor(tui, theme, keybindings)`
497
498
  * — that {@link ExtensionUIContext.setEditorComponent}'s factory contract
498
499
  * advertises `(tui, theme, keybindings)`. Plugins written against upstream pi
@@ -789,7 +790,7 @@ export class CustomEditor extends Editor {
789
790
  if (!materialize || images.length === 0) return;
790
791
  const links = await materialize(images);
791
792
  if (!links || this.pendingImages !== images) return;
792
- this.pendingImageLinks = images.map((image, index) => imageAttachmentSource(image)?.path ?? links[index]);
793
+ this.pendingImageLinks = links;
793
794
  this.imageLinks = this.pendingImageLinks;
794
795
  this.#requestShimmerRepaint?.();
795
796
  }
@@ -798,10 +799,21 @@ export class CustomEditor extends Editor {
798
799
  * indivisible: a stray backspace deletes the whole token instead of corrupting it. */
799
800
  override atomicTokenPattern = COMPOSER_TOKEN_REGEX;
800
801
 
802
+ /** Atom-table revision and pattern the composer token matcher was last built for. */
803
+ #tokenPatternAtomsRevision = -1;
804
+ #tokenPattern: RegExp | undefined;
805
+
801
806
  #syncComposerTokenPattern(): void {
802
- const labels = [...this.atoms].filter(([, expansion]) => expansion.startsWith("^")).map(([label]) => label);
807
+ // Rebuild only when the atom table changed or someone replaced the pattern since the last sync.
808
+ if (this.#tokenPatternAtomsRevision === this.atomsRevision && this.#tokenPattern === this.atomicTokenPattern) {
809
+ return;
810
+ }
811
+ const labels: string[] = [];
812
+ for (const [label, expansion] of this.atoms) if (expansion.startsWith("^")) labels.push(label);
803
813
  const next = composerTokenRegex(labels);
804
814
  if (next.source !== this.atomicTokenPattern.source) this.atomicTokenPattern = next;
815
+ this.#tokenPatternAtomsRevision = this.atomsRevision;
816
+ this.#tokenPattern = this.atomicTokenPattern;
805
817
  }
806
818
 
807
819
  /** Magic-keyword shimmer cadence — drives one editor repaint every 70 ms while
@@ -819,27 +831,35 @@ export class CustomEditor extends Editor {
819
831
  * timer to request the next animation frame. Undefined when nobody is
820
832
  * listening (tests, headless callers); the timer chain still self-cleans. */
821
833
  #requestShimmerRepaint: (() => void) | undefined;
822
- #queueDecorationText: string | undefined;
834
+ /** Text revision the per-revision decoration inputs below were computed for. */
835
+ #decorationRevision = -1;
836
+ #decorationText = "";
823
837
  #decorationLines: readonly string[] = [""];
824
838
  #queueShorthandActive = false;
825
839
  #queueListActive = false;
826
840
 
827
841
  /** Decorate magic keywords, attachments, and the queue-composer header/list markers.
828
842
  * Queue shorthand reserves its first logical line as a dim `Queueing` label; sequential
829
- * item markers use the accent color so separate follow-ups remain visible while composing. */
843
+ * item markers use the accent color so separate follow-ups remain visible while composing.
844
+ * Called once per layout segment, so whole-buffer inputs are computed once per text revision. */
830
845
  override decorateText = (text: string, context: EditorTextDecorationContext): string => {
831
846
  this.#syncComposerTokenPattern();
832
- const editorText = this.getText();
847
+ if (this.#decorationRevision !== this.textRevision) {
848
+ this.#decorationRevision = this.textRevision;
849
+ const editorText = this.getText();
850
+ if (this.#decorationText !== editorText) {
851
+ this.#decorationText = editorText;
852
+ this.#decorationLines = this.getLines();
853
+ const queueBody = parseQueueShorthand(editorText);
854
+ this.#queueShorthandActive = queueBody !== undefined;
855
+ this.#queueListActive = queueBody !== undefined && isQueuedMessageList(queueBody);
856
+ }
857
+ }
858
+ // One string instance per revision: whole-buffer memos downstream hit on identity.
859
+ const editorText = this.#decorationText;
833
860
  const animated = this.focused && this.#shimmerEnabled() && hasMagicKeyword(editorText);
834
861
  const phase = animated ? (Date.now() % CustomEditor.SHIMMER_PERIOD_MS) / CustomEditor.SHIMMER_PERIOD_MS : 0;
835
862
  if (animated) this.#scheduleShimmerFrame();
836
- if (this.#queueDecorationText !== editorText) {
837
- this.#queueDecorationText = editorText;
838
- this.#decorationLines = this.getLines();
839
- const queueBody = parseQueueShorthand(editorText);
840
- this.#queueShorthandActive = queueBody !== undefined;
841
- this.#queueListActive = queueBody !== undefined && isQueuedMessageList(queueBody);
842
- }
843
863
  let sourceSearchOffset = 0;
844
864
  const locateSource = (value: string): number => {
845
865
  const offset = text.indexOf(value, sourceSearchOffset);
@@ -1018,10 +1038,18 @@ export class CustomEditor extends Editor {
1018
1038
  protected override get nativeSendable(): boolean {
1019
1039
  return this.onSubmit !== undefined && !this.disableSubmit;
1020
1040
  }
1021
- /** Viewing a subagent, the draft goes to it: the placeholder names it. */
1022
- override describePlaceholder = (): string => {
1023
- const agent = this.composerState().viewing?.at(-1);
1024
- return agent === undefined ? NATIVE_COMPOSER_PLACEHOLDER : `Message ${agent}`;
1041
+ /** The quoted-title placeholder, kept while the title stands so the editor node stays reused. */
1042
+ #titlePlaceholder: { title: string; text: TspText } | undefined;
1043
+ /** Viewing a subagent, the draft goes to it: the placeholder names it; else the session's title, quoted in italics. */
1044
+ override describePlaceholder = (): TspText => {
1045
+ const { viewing, title } = this.composerState();
1046
+ const agent = viewing?.at(-1);
1047
+ if (agent !== undefined) return `Message ${agent}`;
1048
+ if (!title) return NATIVE_COMPOSER_PLACEHOLDER;
1049
+ if (this.#titlePlaceholder?.title !== title) {
1050
+ this.#titlePlaceholder = { title, text: [{ t: `“${title}”`, s: "em" }] };
1051
+ }
1052
+ return this.#titlePlaceholder.text;
1025
1053
  };
1026
1054
  /** A shell-mode draft highlights as its language. */
1027
1055
  override describeLanguage = (): string | undefined => this.composerState().shell?.kind;