@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
@@ -11,10 +11,9 @@ import { ThinkingLevel } from "@linxiraos/pi-agent-core";
11
11
  import type { Model } from "@linxiraos/pi-ai";
12
12
  import { buildModel } from "@linxiraos/pi-catalog/build";
13
13
  import { getModelPricingStatus, modelsAreEqual } from "@linxiraos/pi-catalog/models";
14
- import type { ModelKind, ModelPricingStatus } from "@linxiraos/pi-catalog/types";
14
+ import { modelKind, type ModelKind, type ModelPricingStatus } from "@linxiraos/pi-catalog/types";
15
15
  import type { Component } from "../tui";
16
- import { tuiText } from "../i18n";
17
- import { fuzzyRank } from "../fuzzy";
16
+ import { FuzzyCorpus, fuzzyRank } from "../fuzzy";
18
17
  import { Input } from "../components/input";
19
18
  import { ScrollView } from "../components/scroll-view";
20
19
  import { matchesKey } from "../keys";
@@ -28,7 +27,7 @@ import {
28
27
  parseConfiguredThinkingLevel,
29
28
  } from "../thinking";
30
29
  import { thinkingLevelGlyph } from "../render/render-utils";
31
- import { type ThemeColor, theme } from "../theme/theme";
30
+ import { type Theme, type ThemeColor, theme } from "../theme/theme";
32
31
  import {
33
32
  matchesSelectCancel,
34
33
  matchesSelectDown,
@@ -123,6 +122,18 @@ export interface ResolvedModelRoleValue {
123
122
  warning?: string;
124
123
  }
125
124
 
125
+ /** Where auto-compaction triggers for one model, as the model hub's preview shows it (edited from the Roles view). */
126
+ export interface ModelCompactionPoint {
127
+ /** Context tokens that trigger auto-compaction; undefined when auto-compaction is off or the window is unknown. */
128
+ tokens: number | undefined;
129
+ /** Why it triggers there, in a few words: `fixed`, `85% of 400K base`, `80% of window`. */
130
+ basis?: string;
131
+ /** What sets it: the matching `compaction.modelThresholds` key, `global`, or `default`. */
132
+ source: string;
133
+ /** The model's own exact entry as editable text (`90k`, `80%`); absent when it has none. */
134
+ draft?: string;
135
+ }
136
+
126
137
  /** Host-provided preferences and model-role resolution for the browser. */
127
138
  export interface ModelBrowserSource extends ModelRoleLookup {
128
139
  /**
@@ -135,11 +146,17 @@ export interface ModelBrowserSource extends ModelRoleLookup {
135
146
  readonly knownRoleIds: readonly string[];
136
147
  readonly mruOrder: readonly string[];
137
148
  readonly modelPerf: ReadonlyMap<string, ModelBrowserPerf>;
149
+ /**
150
+ * Service tier the host would send for `model`, when it runs one. Rows with a
151
+ * measured tier aggregate (`selector@tier`) then display that tier's numbers
152
+ * instead of the standard aggregate.
153
+ */
154
+ serviceTierFor?(model: Model): string | undefined;
138
155
  getRoleInfo(role: string): ModelBrowserRoleInfo;
139
- /** True when the provider was injected from the upstream OMP model config. */
140
- isOmpOriginProvider?(provider: string): boolean;
141
156
  defaultRoleChain(role: string): string[];
142
157
  resolveRoleValue(value: string | undefined, models: Model[], roleLookup?: ModelRoleLookup): ResolvedModelRoleValue;
158
+ /** Auto-compaction point for `model`; absent hosts show no compaction row in the preview. */
159
+ compactionPointFor?(model: Model): ModelCompactionPoint | undefined;
143
160
  }
144
161
 
145
162
  /** Read-only catalog surface consumed by model browsers. */
@@ -246,6 +263,22 @@ export function resolveRoleAssignments(
246
263
  return roles;
247
264
  }
248
265
 
266
+ /**
267
+ * Role-resolution pool for a `--models`/`enabledModels` scope: the scoped
268
+ * models plus every available non-chat runner (judge, search, image, …). The
269
+ * scope only filters chat models (it feeds Ctrl+P cycling) and runtime role
270
+ * resolution ignores it for other kinds, so displayed role assignments must too.
271
+ * Shared by the /models hub and the session picker.
272
+ */
273
+ export function scopedRolePool(scoped: ReadonlyArray<Model>, registry: ModelBrowserRegistry): Model[] {
274
+ const pool = [...scoped];
275
+ for (const model of registry.getAvailable("all")) {
276
+ if (modelKind(model) === "chat" || scoped.some(entry => modelsAreEqual(entry, model))) continue;
277
+ pool.push(model);
278
+ }
279
+ return pool;
280
+ }
281
+
249
282
  /** Wrap raw models into browser items. */
250
283
  export function buildBrowserItems(models: ReadonlyArray<Model>): ModelBrowserItem[] {
251
284
  return models.map(model => ({
@@ -300,13 +333,17 @@ export interface SortModelItemsOptions {
300
333
  * then per provider by priority, version, and recency.
301
334
  */
302
335
  export function sortModelItems(items: ModelBrowserItem[], options: SortModelItemsOptions = {}): void {
336
+ items.sort(modelItemComparator(options));
337
+ }
338
+
339
+ function modelItemComparator(options: SortModelItemsOptions): (a: ModelBrowserItem, b: ModelBrowserItem) => number {
303
340
  const { roles = {}, mruOrder = [], skipRoleRank = false } = options;
304
341
  const mruIndex = new Map(mruOrder.map((key, i) => [key, i]));
305
342
 
306
343
  const dateRe = /-(\d{8})$/;
307
344
  const latestRe = /-latest$/;
308
345
 
309
- items.sort((a, b) => {
346
+ return (a, b) => {
310
347
  if (!skipRoleRank) {
311
348
  const aRank = computeModelRank(a.model, roles);
312
349
  const bRank = computeModelRank(b.model, roles);
@@ -351,9 +388,9 @@ export function sortModelItems(items: ModelBrowserItem[], options: SortModelItem
351
388
  // Both have dates — descending (newest first)
352
389
  if (aDate && bDate) return bDate.localeCompare(aDate);
353
390
 
354
- // One has date, other is latest — latest first
355
- return aIsLatest ? -1 : bIsLatest ? 1 : a.id.localeCompare(b.id);
356
- });
391
+ // Both -latest — alphabetical (a fixed -1 here made the order depend on input order)
392
+ return a.id.localeCompare(b.id);
393
+ };
357
394
  }
358
395
 
359
396
  /** Picker candidates and ordering inputs shared with composer model mentions. */
@@ -366,8 +403,16 @@ export interface SessionModelScope {
366
403
 
367
404
  /** Catalog inputs a {@link SessionModelScope} is derived from. */
368
405
  interface SessionModelScopeInputs {
406
+ /** Chat models listed as picker rows. */
369
407
  models: ReadonlyArray<Model>;
408
+ /** Catalog that configured role values resolve against. */
370
409
  allModels: ReadonlyArray<Model>;
410
+ /**
411
+ * Available models of every kind that unconfigured roles auto-select from,
412
+ * as at runtime; a chat-only pool would pin `judge` on a chat model while
413
+ * the session judges with an available native judge.
414
+ */
415
+ roleModels: ReadonlyArray<Model>;
371
416
  error: string | undefined;
372
417
  }
373
418
 
@@ -375,21 +420,32 @@ function readSessionModelScopeInputs(
375
420
  registry: ModelBrowserRegistry,
376
421
  scopedModels: ReadonlyArray<Model>,
377
422
  ): SessionModelScopeInputs {
378
- if (scopedModels.length > 0) return { models: scopedModels, allModels: scopedModels, error: undefined };
423
+ if (scopedModels.length > 0) {
424
+ let roleModels: ReadonlyArray<Model>;
425
+ let error: string | undefined;
426
+ try {
427
+ roleModels = scopedRolePool(scopedModels, registry);
428
+ } catch (cause) {
429
+ error = cause instanceof Error ? cause.message : String(cause);
430
+ roleModels = scopedModels;
431
+ }
432
+ return { models: scopedModels, allModels: roleModels, roleModels, error };
433
+ }
379
434
  const loadError = registry.getError();
380
435
  let error = loadError ? String(loadError) : undefined;
381
- let models: ReadonlyArray<Model>;
436
+ let roleModels: ReadonlyArray<Model>;
382
437
  try {
383
- models = registry.getAvailable();
438
+ roleModels = registry.getAvailable("all");
384
439
  } catch (cause) {
385
440
  error = cause instanceof Error ? cause.message : String(cause);
386
- models = [];
441
+ roleModels = [];
387
442
  }
388
- return { models, allModels: registry.getAll("all"), error };
443
+ const models = roleModels.filter(model => modelKind(model) === "chat");
444
+ return { models, allModels: registry.getAll("all"), roleModels, error };
389
445
  }
390
446
 
391
447
  function scopeFromInputs(settings: ModelBrowserSource, inputs: SessionModelScopeInputs): SessionModelScope {
392
- const roles = resolveRoleAssignments(settings, inputs.allModels, inputs.models);
448
+ const roles = resolveRoleAssignments(settings, inputs.allModels, inputs.roleModels);
393
449
  const mruOrder = settings.mruOrder;
394
450
  const items = buildBrowserItems(inputs.models);
395
451
  sortModelItems(items, { roles, mruOrder });
@@ -434,6 +490,7 @@ export class SessionModelScopeCache {
434
490
  inputs.error === cachedInputs.error &&
435
491
  sameItems(inputs.models, cachedInputs.models) &&
436
492
  sameItems(inputs.allModels, cachedInputs.allModels) &&
493
+ sameItems(inputs.roleModels, cachedInputs.roleModels) &&
437
494
  sameItems(this.#settings.mruOrder, cached.mruOrder)
438
495
  ) {
439
496
  return cached;
@@ -533,58 +590,100 @@ function compactModelSearchText(value: string): string {
533
590
  return value.toLowerCase().replace(/[^\p{Letter}\p{Mark}\p{Number}]+/gu, "");
534
591
  }
535
592
 
536
- /** Exact id/selector → contiguous literal → fuzzy-only. */
537
- function modelSearchTier(query: string, item: ModelBrowserItem): number {
593
+ /** Exact id/selector → contiguous literal → fuzzy-only, against precomputed compact forms. */
594
+ function modelSearchTier(query: string, id: string, selector: string): number {
538
595
  if (!query) return 2;
539
- const id = compactModelSearchText(item.id);
540
- const selector = compactModelSearchText(item.selector);
541
596
  if (query === id || query === selector) return 0;
542
597
  if (id.includes(query) || selector.includes(query)) return 1;
543
598
  return 2;
544
599
  }
545
600
 
601
+ /** Inputs to {@link rankModelItems} / {@link ModelItemRanker}. */
602
+ export interface ModelRankOptions {
603
+ roles: RoleAssignments;
604
+ mruOrder: ReadonlyArray<string>;
605
+ affinity: SearchAffinity;
606
+ }
607
+
608
+ /** Query-independent sort keys of one candidate, computed once per ranker. */
609
+ interface ModelRankKeys {
610
+ compactId: string;
611
+ compactSelector: string;
612
+ modelAffinity: number;
613
+ providerAffinity: number;
614
+ /** Position class in the MRU/version order; comparator-equal items share it. */
615
+ orderGroup: number;
616
+ }
617
+
618
+ /**
619
+ * {@link rankModelItems} prepared for one candidate list: the fuzzy index, the
620
+ * MRU/version order, and the affinity ranks depend only on the items and options,
621
+ * so per-keystroke callers (composer mentions, `/switch` arguments, the picker)
622
+ * hold one ranker and pay only the per-query fuzzy scan.
623
+ */
624
+ export class ModelItemRanker {
625
+ readonly items: ReadonlyArray<ModelBrowserItem>;
626
+ readonly options: ModelRankOptions;
627
+ readonly #corpus: FuzzyCorpus<ModelBrowserItem>;
628
+ readonly #keys = new Map<ModelBrowserItem, ModelRankKeys>();
629
+
630
+ constructor(items: ReadonlyArray<ModelBrowserItem>, options: ModelRankOptions) {
631
+ this.items = items;
632
+ this.options = options;
633
+ this.#corpus = new FuzzyCorpus(items, modelSearchText);
634
+ const compare = modelItemComparator({ roles: options.roles, mruOrder: options.mruOrder, skipRoleRank: true });
635
+ const ordered = [...items].sort(compare);
636
+ let group = 0;
637
+ for (let i = 0; i < ordered.length; i++) {
638
+ const item = ordered[i]!;
639
+ if (i > 0 && compare(ordered[i - 1]!, item) !== 0) group++;
640
+ this.#keys.set(item, {
641
+ compactId: compactModelSearchText(item.id),
642
+ compactSelector: compactModelSearchText(item.selector),
643
+ modelAffinity: options.affinity.models.get(item.selector.toLowerCase()) ?? Number.MAX_SAFE_INTEGER,
644
+ providerAffinity: options.affinity.providers.get(item.provider.toLowerCase()) ?? Number.MAX_SAFE_INTEGER,
645
+ orderGroup: group,
646
+ });
647
+ }
648
+ }
649
+
650
+ /** Rank by text relevance, user affinity, and MRU/version order. */
651
+ rank(query: string): ModelBrowserItem[] {
652
+ if (!query.trim()) return [...this.items];
653
+ const queryKey = compactModelSearchText(query);
654
+ // Exact and contiguous matches stay ahead of fuzzy-only candidates; affinity
655
+ // breaks ties before fuzzy quality and the normal MRU/version ordering, and
656
+ // fuzzy order settles items that ordering cannot tell apart.
657
+ const rows = this.#corpus.rank(query).map((result, fuzzyIndex) => {
658
+ const keys = this.#keys.get(result.item)!;
659
+ return {
660
+ item: result.item,
661
+ keys,
662
+ tier: modelSearchTier(queryKey, keys.compactId, keys.compactSelector),
663
+ bucket: Math.round(result.score / 10),
664
+ fuzzyIndex,
665
+ };
666
+ });
667
+ rows.sort(
668
+ (a, b) =>
669
+ a.tier - b.tier ||
670
+ a.keys.modelAffinity - b.keys.modelAffinity ||
671
+ a.keys.providerAffinity - b.keys.providerAffinity ||
672
+ a.bucket - b.bucket ||
673
+ a.keys.orderGroup - b.keys.orderGroup ||
674
+ a.fuzzyIndex - b.fuzzyIndex,
675
+ );
676
+ return rows.map(row => row.item);
677
+ }
678
+ }
679
+
546
680
  /** Rank picker and mention candidates by text relevance, user affinity, and MRU/version order. */
547
681
  export function rankModelItems(
548
682
  query: string,
549
683
  items: ReadonlyArray<ModelBrowserItem>,
550
- options: { roles: RoleAssignments; mruOrder: ReadonlyArray<string>; affinity: SearchAffinity },
684
+ options: ModelRankOptions,
551
685
  ): ModelBrowserItem[] {
552
- if (!query.trim()) return [...items];
553
- const ranked = fuzzyRank(items, query, modelSearchText);
554
- const matches = ranked.map(result => result.item);
555
- // Exact and contiguous matches stay ahead of fuzzy-only candidates; affinity
556
- // breaks ties before fuzzy quality and the normal MRU/version ordering.
557
- sortModelItems(matches, { roles: options.roles, mruOrder: options.mruOrder, skipRoleRank: true });
558
- const fallbackRanks = new Map(matches.map((item, index) => [item, index]));
559
- const queryKey = compactModelSearchText(query);
560
- const searchRanks = new Map<ModelBrowserItem, { tier: number; bucket: number }>();
561
- for (const result of ranked) {
562
- searchRanks.set(result.item, {
563
- tier: modelSearchTier(queryKey, result.item),
564
- bucket: Math.round(result.score / 10),
565
- });
566
- }
567
- matches.sort((a, b) => {
568
- const aSearch = searchRanks.get(a);
569
- const bSearch = searchRanks.get(b);
570
- const tierCmp = (aSearch?.tier ?? Number.MAX_SAFE_INTEGER) - (bSearch?.tier ?? Number.MAX_SAFE_INTEGER);
571
- if (tierCmp !== 0) return tierCmp;
572
-
573
- const modelCmp =
574
- (options.affinity.models.get(a.selector.toLowerCase()) ?? Number.MAX_SAFE_INTEGER) -
575
- (options.affinity.models.get(b.selector.toLowerCase()) ?? Number.MAX_SAFE_INTEGER);
576
- if (modelCmp !== 0) return modelCmp;
577
-
578
- const providerCmp =
579
- (options.affinity.providers.get(a.provider.toLowerCase()) ?? Number.MAX_SAFE_INTEGER) -
580
- (options.affinity.providers.get(b.provider.toLowerCase()) ?? Number.MAX_SAFE_INTEGER);
581
- if (providerCmp !== 0) return providerCmp;
582
-
583
- const bucketCmp = (aSearch?.bucket ?? Number.MAX_SAFE_INTEGER) - (bSearch?.bucket ?? Number.MAX_SAFE_INTEGER);
584
- if (bucketCmp !== 0) return bucketCmp;
585
- return (fallbackRanks.get(a) ?? Number.MAX_SAFE_INTEGER) - (fallbackRanks.get(b) ?? Number.MAX_SAFE_INTEGER);
586
- });
587
- return matches;
686
+ return new ModelItemRanker(items, options).rank(query);
588
687
  }
589
688
 
590
689
  /**
@@ -710,7 +809,7 @@ function previewPrice(model: Model): string {
710
809
  return withCreditBadge(model, parts.join(" · "));
711
810
  }
712
811
 
713
- /** The zeta theme token of a thinking level's dot (`thinkingHigh`); none for inherit and auto. */
812
+ /** The omp theme token of a thinking level's dot (`thinkingHigh`); none for inherit and auto. */
714
813
  export function thinkingDotToken(level: ConfiguredThinkingLevel): string | undefined {
715
814
  if (level === ThinkingLevel.Inherit || level === AUTO_THINKING) return undefined;
716
815
  return `thinking${level.charAt(0).toUpperCase()}${level.slice(1)}`;
@@ -820,9 +919,42 @@ const DETAIL_ROWS = 3;
820
919
  const PERF_TPS_MIN_WIDTH = 76;
821
920
  /** Row width from which the perf column also includes TTFT. */
822
921
  const PERF_FULL_MIN_WIDTH = 96;
922
+ /** Narrowest model-name cell retained before cost and context are dropped. */
923
+ const MIN_NAME_WIDTH = 16;
924
+
925
+ /** Total width of present metadata columns, joined by two-space gaps. */
926
+ function metaColumnsWidth(widths: readonly number[]): number {
927
+ let total = 0;
928
+ let count = 0;
929
+ for (const width of widths) {
930
+ if (width <= 0) continue;
931
+ total += width;
932
+ count++;
933
+ }
934
+ return count > 0 ? total + 2 * (count - 1) : 0;
935
+ }
936
+
823
937
  /** What the per-row perf column shows at the current width. */
824
938
  type PerfMode = "off" | "tps" | "full";
825
939
 
940
+ /** A measured perf aggregate and, when it came from one, its service tier. */
941
+ interface MeasuredPerf {
942
+ perf: ModelBrowserPerf;
943
+ tier?: string;
944
+ }
945
+
946
+ /** Plain catalog metric cells of one row and their terminal widths, valid for one model and theme. */
947
+ interface MetricCells {
948
+ model: Model;
949
+ theme: Theme;
950
+ intelligence: string;
951
+ intelligenceWidth: number;
952
+ ctx: string;
953
+ ctxWidth: number;
954
+ cost: string;
955
+ costWidth: number;
956
+ }
957
+
826
958
  /**
827
959
  * The reusable browser component. Renders a fixed-height block
828
960
  * (`maxVisible + LIST_ROW_START + DETAIL_ROWS` rows) so host mouse geometry
@@ -840,7 +972,17 @@ export class ModelBrowser implements Component {
840
972
  #roles: RoleAssignments = {};
841
973
  #mruOrder: ReadonlyArray<string> = [];
842
974
  #affinity: SearchAffinity = { models: new Map(), providers: new Map() };
975
+ /** Ranker for the menu's current item array; rebuilt when items or affinity change. */
976
+ #ranker?: { source: readonly ModelBrowserItem[]; ranker: ModelItemRanker };
843
977
  #perf: ReadonlyMap<string, ModelBrowserPerf> = new Map();
978
+ /** Fastest tier aggregate per selector in {@link #perf}, built on the first lookup miss. */
979
+ #perfTiers:
980
+ | { perf: ReadonlyMap<string, ModelBrowserPerf>; size: number; best: Map<string, MeasuredPerf> }
981
+ | undefined;
982
+ /** Theme-formatted catalog metric cells per item; rebuilt when the item's model or the theme changes. */
983
+ #metricCells = new WeakMap<ModelBrowserItem, MetricCells>();
984
+ /** Non-separator items of the last ranked visible list, for {@link queryMatches}. */
985
+ #queryMatches: { visible: readonly ModelBrowserItem[]; matches: readonly ModelBrowserItem[] } | undefined;
844
986
  #hoveredIndex: number | null = null;
845
987
  #maxVisible = 10;
846
988
  #showProvider: boolean;
@@ -1009,6 +1151,22 @@ export class ModelBrowser implements Component {
1009
1151
  return this.#menu.visibleItems.length;
1010
1152
  }
1011
1153
 
1154
+ /**
1155
+ * Base items matching the live query, best first, without the separator
1156
+ * row; undefined while the query is blank. Reuses the ranking the list
1157
+ * already ran, so hosts can derive match counts without ranking again.
1158
+ */
1159
+ get queryMatches(): readonly ModelBrowserItem[] | undefined {
1160
+ if (!this.query.trim()) return undefined;
1161
+ const visible = this.#menu.visibleItems;
1162
+ let cached = this.#queryMatches;
1163
+ if (cached?.visible !== visible) {
1164
+ cached = { visible, matches: visible.filter(item => !this.#isDisabled(item)) };
1165
+ this.#queryMatches = cached;
1166
+ }
1167
+ return cached.matches;
1168
+ }
1169
+
1012
1170
  /** Move selection to `selector`; false when it is not in the current view. */
1013
1171
  selectSelector(selector: string): boolean {
1014
1172
  if (!this.#menu.setSelectedKey(selector)) return false;
@@ -1027,17 +1185,26 @@ export class ModelBrowser implements Component {
1027
1185
  * unfiltered when the query is blank.
1028
1186
  */
1029
1187
  #filterItems(items: readonly ModelBrowserItem[], query: string): readonly ModelBrowserItem[] {
1030
- const base = items.filter(item => !this.#isDisabled(item));
1031
- const ranked = this.#preserveQueryOrder
1032
- ? query.trim()
1033
- ? fuzzyRank(base, query, modelSearchText).map(result => result.item)
1034
- : base
1035
- : rankModelItems(query, base, {
1036
- roles: this.#roles,
1037
- mruOrder: this.#mruOrder,
1038
- affinity: this.#affinity,
1039
- });
1040
- return this.#insertSeparator(ranked);
1188
+ if (this.#preserveQueryOrder) {
1189
+ const base = items.filter(item => !this.#isDisabled(item));
1190
+ return this.#insertSeparator(
1191
+ query.trim() ? fuzzyRank(base, query, modelSearchText).map(result => result.item) : base,
1192
+ );
1193
+ }
1194
+ // The menu hands back the same array until `setItems`, and roles/MRU replace the
1195
+ // affinity object, so one ranker serves every keystroke of a search.
1196
+ let cached = this.#ranker;
1197
+ if (cached?.source !== items || cached.ranker.options.affinity !== this.#affinity) {
1198
+ cached = {
1199
+ source: items,
1200
+ ranker: new ModelItemRanker(
1201
+ items.filter(item => !this.#isDisabled(item)),
1202
+ { roles: this.#roles, mruOrder: this.#mruOrder, affinity: this.#affinity },
1203
+ ),
1204
+ };
1205
+ this.#ranker = cached;
1206
+ }
1207
+ return this.#insertSeparator(cached.ranker.rank(query));
1041
1208
  }
1042
1209
 
1043
1210
  /** True when `item`'s context window is smaller than the live session token count (grayed row; hosts compact before switching). */
@@ -1258,14 +1425,76 @@ export class ModelBrowser implements Component {
1258
1425
  return index;
1259
1426
  }
1260
1427
 
1428
+ /**
1429
+ * Measured perf for a row: the tier the host would send, else the standard
1430
+ * aggregate, else any measured tier (fastest first) so a model only ever run
1431
+ * on a non-default tier still shows its real speed. `tier` is set whenever
1432
+ * the numbers come from a tier aggregate, for the caller to label.
1433
+ */
1434
+ #perfFor(item: ModelBrowserItem): MeasuredPerf | undefined {
1435
+ if (this.#perf.size === 0) return undefined;
1436
+ const tier = this.#settings.serviceTierFor?.(item.model);
1437
+ if (tier) {
1438
+ const tiered = this.#perf.get(`${item.selector}@${tier}`);
1439
+ if (tiered) return { perf: tiered, tier };
1440
+ }
1441
+ const standard = this.#perf.get(item.selector);
1442
+ if (standard) return { perf: standard };
1443
+ return this.#fastestTierPerf(item.selector);
1444
+ }
1445
+
1446
+ /** Fastest `selector@tier` aggregate (first wins ties), from an index built once per perf map. */
1447
+ #fastestTierPerf(selector: string): MeasuredPerf | undefined {
1448
+ const perfMap = this.#perf;
1449
+ let index = this.#perfTiers;
1450
+ if (index?.perf !== perfMap || index.size !== perfMap.size) {
1451
+ const best = new Map<string, MeasuredPerf>();
1452
+ for (const [key, perf] of perfMap) {
1453
+ // Every `@` may end the selector: `a@b@t` is tier `b@t` of `a` and tier `t` of `a@b`.
1454
+ for (let at = key.indexOf("@"); at >= 0; at = key.indexOf("@", at + 1)) {
1455
+ const owner = key.slice(0, at);
1456
+ const current = best.get(owner);
1457
+ if (!current || perf.tps > current.perf.tps) best.set(owner, { perf, tier: key.slice(at + 1) });
1458
+ }
1459
+ }
1460
+ index = { perf: perfMap, size: perfMap.size, best };
1461
+ this.#perfTiers = index;
1462
+ }
1463
+ return index.best.get(selector);
1464
+ }
1465
+
1466
+ /** Catalog metric cells of a row, formatted once per model and theme. */
1467
+ #metricCellsFor(item: ModelBrowserItem): MetricCells {
1468
+ const cached = this.#metricCells.get(item);
1469
+ if (cached?.model === item.model && cached.theme === theme) return cached;
1470
+ const intelligence = formatIntelligence(item.model);
1471
+ const ctx = formatContext(item.model);
1472
+ const cost = formatCostPair(item.model);
1473
+ const cells: MetricCells = {
1474
+ model: item.model,
1475
+ theme,
1476
+ intelligence,
1477
+ intelligenceWidth: visibleWidth(intelligence),
1478
+ ctx,
1479
+ ctxWidth: visibleWidth(ctx),
1480
+ cost,
1481
+ costWidth: visibleWidth(cost),
1482
+ };
1483
+ this.#metricCells.set(item, cells);
1484
+ return cells;
1485
+ }
1486
+
1261
1487
  /** Measured TPS/TTFT, falling back to the catalog TPS as an estimated `~118t/s`. */
1262
1488
  #perfCell(item: ModelBrowserItem, mode: PerfMode): string {
1263
1489
  if (mode === "off") return "";
1264
- const perf = this.#perf.get(item.selector);
1265
- if (perf) {
1266
- const tps = formatTps(perf.tps);
1267
- if (mode === "full" && perf.ttftMs !== null) return `${formatTtft(perf.ttftMs)} ${tps}`;
1268
- return tps;
1490
+ const measured = this.#perfFor(item);
1491
+ if (measured) {
1492
+ const tps = formatTps(measured.perf.tps);
1493
+ const tier = measured.tier ? ` ${measured.tier}` : "";
1494
+ if (mode === "full" && measured.perf.ttftMs !== null) {
1495
+ return `${formatTtft(measured.perf.ttftMs)} ${tps}${tier}`;
1496
+ }
1497
+ return `${tps}${tier}`;
1269
1498
  }
1270
1499
  const tps = item.model.tps;
1271
1500
  return tps != null && Number.isFinite(tps) && tps > 0 ? `~${formatTps(tps)}` : "";
@@ -1280,7 +1509,8 @@ export class ModelBrowser implements Component {
1280
1509
  costWidth: number,
1281
1510
  intelligenceWidth: number,
1282
1511
  perfWidth: number,
1283
- perfMode: PerfMode,
1512
+ metrics: MetricCells,
1513
+ perfCell: string,
1284
1514
  ): string {
1285
1515
  if (item.id === "separator") {
1286
1516
  const dashCount = Math.max(0, width - 4);
@@ -1297,33 +1527,23 @@ export class ModelBrowser implements Component {
1297
1527
  : item.id;
1298
1528
  const currentMark =
1299
1529
  item.selector === this.#currentSelector ? ` ${theme.fg("success", theme.status.enabled)}` : "";
1300
- const ompBadge = this.#settings.isOmpOriginProvider?.(item.provider)
1301
- ? ` ${theme.fg("dim", tuiText("models.originOmp", "from OMP"))}`
1302
- : "";
1303
1530
  const overLimit = overContext
1304
1531
  ? ` ${theme.status.disabled} context>${formatNumber(item.model.contextWindow ?? 0).toLowerCase()}`
1305
1532
  : "";
1306
- let left = `${prefix}${providerPrefix}${name}${ompBadge}${currentMark}${overLimit}`;
1307
-
1308
- // Metric columns collapse independently when no visible row has data.
1309
- const intelligenceCol =
1310
- intelligenceWidth > 0
1311
- ? `${theme.fg("dim", padLeftVisible(formatIntelligence(item.model), intelligenceWidth))} `
1312
- : "";
1313
- const perfCol =
1314
- perfWidth > 0 ? `${theme.fg("dim", padLeftVisible(this.#perfCell(item, perfMode), perfWidth))} ` : "";
1315
- const meta = `${intelligenceCol}${perfCol}${theme.fg("dim", padLeftVisible(formatContext(item.model), ctxWidth))} ${theme.fg("dim", padLeftVisible(formatCostPair(item.model), costWidth))}`;
1316
- const metaWidth =
1317
- ctxWidth +
1318
- costWidth +
1319
- 2 +
1320
- (intelligenceWidth > 0 ? intelligenceWidth + 2 : 0) +
1321
- (perfWidth > 0 ? perfWidth + 2 : 0);
1322
- const available = Math.max(1, width - metaWidth - 1);
1533
+ let left = `${prefix}${providerPrefix}${name}${currentMark}${overLimit}`;
1534
+
1535
+ // Metric columns collapse when empty or when the row needs room for its name.
1536
+ const cols: string[] = [];
1537
+ if (intelligenceWidth > 0) cols.push(theme.fg("dim", padLeftVisible(metrics.intelligence, intelligenceWidth)));
1538
+ if (perfWidth > 0) cols.push(theme.fg("dim", padLeftVisible(perfCell, perfWidth)));
1539
+ if (ctxWidth > 0) cols.push(theme.fg("dim", padLeftVisible(metrics.ctx, ctxWidth)));
1540
+ if (costWidth > 0) cols.push(theme.fg("dim", padLeftVisible(metrics.cost, costWidth)));
1541
+ const metaWidth = metaColumnsWidth([intelligenceWidth, perfWidth, ctxWidth, costWidth]);
1542
+ const available = Math.max(1, width - metaWidth - (cols.length > 0 ? 1 : 0));
1323
1543
  left = truncateToWidth(left, available);
1324
1544
  const gap = Math.max(0, available - visibleWidth(left));
1325
1545
 
1326
- let line = `${left}${" ".repeat(gap)} ${meta}`;
1546
+ let line = cols.length > 0 ? `${left}${" ".repeat(gap)} ${cols.join(" ")}` : `${left}${" ".repeat(gap)}`;
1327
1547
  if (overContext) {
1328
1548
  // Gray the whole row but keep the selection cursor visible: over-context
1329
1549
  // models stay selectable (the host compacts before switching).
@@ -1356,10 +1576,10 @@ export class ModelBrowser implements Component {
1356
1576
  if (model.input.includes("image")) facts.push("vision");
1357
1577
  const intelligence = formatIntelligence(model);
1358
1578
  if (intelligence) facts.push(intelligence);
1359
- const perf = this.#perf.get(selected.selector);
1360
- if (perf) {
1361
- facts.push(`~${formatTps(perf.tps)}`);
1362
- if (perf.ttftMs !== null) facts.push(`${formatTtft(perf.ttftMs)} ttft`);
1579
+ const measured = this.#perfFor(selected);
1580
+ if (measured) {
1581
+ facts.push(`~${formatTps(measured.perf.tps)}${measured.tier ? ` ${measured.tier}` : ""}`);
1582
+ if (measured.perf.ttftMs !== null) facts.push(`${formatTtft(measured.perf.ttftMs)} ttft`);
1363
1583
  } else if (model.tps != null && Number.isFinite(model.tps) && model.tps > 0) {
1364
1584
  facts.push(`~${formatTps(model.tps)}`);
1365
1585
  }
@@ -1412,10 +1632,7 @@ export class ModelBrowser implements Component {
1412
1632
 
1413
1633
  if (total === 0) {
1414
1634
  const message =
1415
- this.#emptyText?.() ??
1416
- (this.query.trim()
1417
- ? tuiText("mbNoMatchingModels", " No matching models")
1418
- : tuiText("mbNoModelsInScope", " No models available in this scope"));
1635
+ this.#emptyText?.() ?? (this.query.trim() ? " No matching models" : " No models available in this scope");
1419
1636
  lines.push(truncateToWidth(theme.fg("muted", message), width));
1420
1637
  for (let i = 1; i < this.#maxVisible; i++) lines.push("");
1421
1638
  } else {
@@ -1426,20 +1643,32 @@ export class ModelBrowser implements Component {
1426
1643
  const perfMode: PerfMode = width >= PERF_FULL_MIN_WIDTH ? "full" : width >= PERF_TPS_MIN_WIDTH ? "tps" : "off";
1427
1644
  let intelligenceWidth = 0;
1428
1645
  let perfWidth = 0;
1646
+ const visible = this.#menu.visibleItems;
1647
+ const windowMetrics: MetricCells[] = [];
1648
+ const windowPerf: string[] = [];
1429
1649
  for (let i = startIndex; i < endIndex; i++) {
1430
- const item = this.#menu.visibleItems[i];
1650
+ const item = visible[i];
1431
1651
  if (!item) continue;
1432
- ctxWidth = Math.max(ctxWidth, visibleWidth(formatContext(item.model)));
1433
- costWidth = Math.max(costWidth, visibleWidth(formatCostPair(item.model)));
1652
+ const metrics = this.#metricCellsFor(item);
1653
+ const perfCell = this.#perfCell(item, perfMode);
1654
+ windowMetrics[i - startIndex] = metrics;
1655
+ windowPerf[i - startIndex] = perfCell;
1656
+ ctxWidth = Math.max(ctxWidth, metrics.ctxWidth);
1657
+ costWidth = Math.max(costWidth, metrics.costWidth);
1434
1658
  if (perfMode !== "off") {
1435
- intelligenceWidth = Math.max(intelligenceWidth, visibleWidth(formatIntelligence(item.model)));
1659
+ intelligenceWidth = Math.max(intelligenceWidth, metrics.intelligenceWidth);
1436
1660
  }
1437
- perfWidth = Math.max(perfWidth, visibleWidth(this.#perfCell(item, perfMode)));
1661
+ perfWidth = Math.max(perfWidth, visibleWidth(perfCell));
1438
1662
  }
1663
+ // Preserve at least a readable name by dropping cost, then context.
1664
+ let nameRoom = width - 2 - metaColumnsWidth([intelligenceWidth, perfWidth, ctxWidth, costWidth]);
1665
+ if (nameRoom < MIN_NAME_WIDTH) costWidth = 0;
1666
+ nameRoom = width - 2 - metaColumnsWidth([intelligenceWidth, perfWidth, ctxWidth, costWidth]);
1667
+ if (nameRoom < MIN_NAME_WIDTH) ctxWidth = 0;
1439
1668
 
1440
1669
  const rows: string[] = [];
1441
1670
  for (let i = startIndex; i < endIndex; i++) {
1442
- const item = this.#menu.visibleItems[i];
1671
+ const item = visible[i];
1443
1672
  if (!item) continue;
1444
1673
  rows.push(
1445
1674
  this.#renderRow(
@@ -1451,7 +1680,8 @@ export class ModelBrowser implements Component {
1451
1680
  costWidth,
1452
1681
  intelligenceWidth,
1453
1682
  perfWidth,
1454
- perfMode,
1683
+ windowMetrics[i - startIndex]!,
1684
+ windowPerf[i - startIndex]!,
1455
1685
  ),
1456
1686
  );
1457
1687
  }
@@ -1622,10 +1852,10 @@ export class ModelBrowser implements Component {
1622
1852
  if (model.input.includes("image")) facts.push("vision");
1623
1853
  const intelligence = formatIntelligence(model);
1624
1854
  if (intelligence) facts.push(intelligence);
1625
- const perf = this.#perf.get(selected.selector);
1626
- if (perf) {
1627
- facts.push(`~${formatTps(perf.tps)}`);
1628
- if (perf.ttftMs !== null) facts.push(`${formatTtft(perf.ttftMs)} ttft`);
1855
+ const measured = this.#perfFor(selected);
1856
+ if (measured) {
1857
+ facts.push(`~${formatTps(measured.perf.tps)}${measured.tier ? ` ${measured.tier}` : ""}`);
1858
+ if (measured.perf.ttftMs !== null) facts.push(`${formatTtft(measured.perf.ttftMs)} ttft`);
1629
1859
  } else if (model.tps != null && Number.isFinite(model.tps) && model.tps > 0) {
1630
1860
  facts.push(`~${formatTps(model.tps)}`);
1631
1861
  }
@@ -1737,7 +1967,9 @@ export class ModelBrowser implements Component {
1737
1967
  const facts: Record<string, TspText | number> = {};
1738
1968
  const int = model.int != null && Number.isFinite(model.int) ? model.int : undefined;
1739
1969
  if (int !== undefined) facts.int = String(Math.round(int));
1740
- const speed = this.#perfCell(item, "tps").replace(/t\/s$/, "");
1970
+ const speed = this.#perfCell(item, "tps")
1971
+ .replace(/\s*t\/s\s*/, " ")
1972
+ .trim();
1741
1973
  if (speed) facts.speed = speed;
1742
1974
  if (model.contextWindow) facts.ctx = model.contextWindow;
1743
1975
  facts.price = pickerPrice(model);
@@ -1879,7 +2111,7 @@ export class ModelBrowser implements Component {
1879
2111
  pickerPreview(mode: "full" | "compact", current?: string): readonly NativeChild[] {
1880
2112
  const selected = this.getSelected();
1881
2113
  const item = selected && !this.#isDisabled(selected) ? selected : undefined;
1882
- const key = `${mode}\0${current ?? ""}`;
2114
+ const key = `${mode}\0${current ?? ""}\0${this.#settings.revision}`;
1883
2115
  const memo = this.#pickerPreview;
1884
2116
  if (
1885
2117
  memo !== undefined &&
@@ -1899,7 +2131,7 @@ export class ModelBrowser implements Component {
1899
2131
  modelPreview(item: ModelBrowserItem, mode: "full" | "compact", current: string | undefined): NativeChild[] {
1900
2132
  const model = item.model;
1901
2133
  const selector = `${model.provider}/${model.id}`;
1902
- const perf = this.#perf.get(selector);
2134
+ const measured = this.#perfFor(item);
1903
2135
  const ctx = model.contextWindow ?? 0;
1904
2136
  const out = model.maxTokens ?? 0;
1905
2137
  const overContext = this.isOverContext(item);
@@ -1959,10 +2191,12 @@ export class ModelBrowser implements Component {
1959
2191
  if (badges.length > 0) children.push(row(badges, { gap: "xs", wrap: true }));
1960
2192
 
1961
2193
  const speed: string[] = [];
1962
- if (perf) {
1963
- speed.push(`${formatTps(perf.tps).replace("t/s", " t/s")}`);
1964
- if (perf.ttftMs !== null) speed.push(`${formatTtft(perf.ttftMs).replace("s", " s")} TTFT`);
1965
- speed.push(`${perf.samples} ${perf.samples === 1 ? "sample" : "samples"}`);
2194
+ if (measured) {
2195
+ speed.push(
2196
+ `${formatTps(measured.perf.tps).replace("t/s", " t/s")}${measured.tier ? ` ${measured.tier}` : ""}`,
2197
+ );
2198
+ if (measured.perf.ttftMs !== null) speed.push(`${formatTtft(measured.perf.ttftMs).replace("s", " s")} TTFT`);
2199
+ speed.push(`${measured.perf.samples} ${measured.perf.samples === 1 ? "sample" : "samples"}`);
1966
2200
  } else if (model.tps != null && Number.isFinite(model.tps) && model.tps > 0) {
1967
2201
  speed.push(`~${formatTps(model.tps).replace("t/s", " t/s")} (catalog)`);
1968
2202
  }
@@ -1971,6 +2205,17 @@ export class ModelBrowser implements Component {
1971
2205
  if (v) facts.push({ k: [span(k, "muted")], v: [span(v, "mono")] });
1972
2206
  };
1973
2207
  fact("Context", ctx > 0 ? ctx.toLocaleString("en-US") : undefined);
2208
+ const compaction = this.#settings.compactionPointFor?.(model);
2209
+ if (compaction) {
2210
+ const value =
2211
+ compaction.tokens === undefined
2212
+ ? "off"
2213
+ : `${compaction.tokens.toLocaleString("en-US")}${compaction.basis !== undefined ? ` · ${compaction.basis}` : ""}`;
2214
+ facts.push({
2215
+ k: [span("Compacts at", "muted")],
2216
+ v: [span(value, "mono"), span(` · ${compaction.source}`, "dim")],
2217
+ });
2218
+ }
1974
2219
  fact("Max output", out > 0 ? out.toLocaleString("en-US") : undefined);
1975
2220
  fact("Price", isFreeModel(model) ? "free" : `${previewPrice(model)} per M`);
1976
2221
  fact("Speed", speed.length > 0 ? speed.join(" · ") : undefined);