@linxiraos/pi-tui 1.1.27 → 1.2.1

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 (376) hide show
  1. package/CHANGELOG.md +50 -461
  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 +2 -7
  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 +19 -42
  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/splash.d.ts +1 -1
  95. package/dist/types/setup/scenes/types.d.ts +1 -0
  96. package/dist/types/status-line/component.d.ts +3 -0
  97. package/dist/types/status-line/host.d.ts +4 -1
  98. package/dist/types/status-line/segments.d.ts +2 -0
  99. package/dist/types/status-line/types.d.ts +14 -3
  100. package/dist/types/terminal-capabilities.d.ts +27 -35
  101. package/dist/types/terminal-multiplexer.d.ts +127 -14
  102. package/dist/types/terminal.d.ts +22 -6
  103. package/dist/types/theme/active-symbols.d.ts +1 -1
  104. package/dist/types/theme/schema.d.ts +11 -2
  105. package/dist/types/theme/symbols.d.ts +1 -1
  106. package/dist/types/theme/theme-class.d.ts +7 -0
  107. package/dist/types/theme/theme.d.ts +1 -1
  108. package/dist/types/theme/tui-adapters.d.ts +3 -0
  109. package/dist/types/tmux.d.ts +11 -4
  110. package/dist/types/tools/daemon.d.ts +1 -1
  111. package/dist/types/tools/eval.d.ts +22 -3
  112. package/dist/types/tools/glob.d.ts +4 -0
  113. package/dist/types/tools/native-view.d.ts +47 -0
  114. package/dist/types/tools/output-meta.d.ts +11 -1
  115. package/dist/types/tools/renderer.d.ts +37 -2
  116. package/dist/types/tools/streaming-output.d.ts +20 -1
  117. package/dist/types/tools/task.d.ts +0 -4
  118. package/dist/types/tools/write.d.ts +9 -2
  119. package/dist/types/tools/yield.d.ts +22 -0
  120. package/dist/types/tui.d.ts +31 -0
  121. package/dist/types/utils.d.ts +6 -0
  122. package/dist/types/vim.d.ts +19 -5
  123. package/package.json +13 -9
  124. package/src/app-keybindings.d.ts +1 -1
  125. package/src/app-keybindings.ts +1 -1
  126. package/src/apps/autoresearch-dashboard.ts +7 -3
  127. package/src/apps/cleanse-board.ts +2 -2
  128. package/src/apps/cleanse-picker.ts +1 -1
  129. package/src/apps/debug/log-viewer.ts +188 -72
  130. package/src/apps/debug/raw-sse-buffer.ts +8 -0
  131. package/src/apps/debug/raw-sse.ts +39 -5
  132. package/src/apps/git/diff-pane.ts +26 -5
  133. package/src/apps/git/git-tui.ts +2 -2
  134. package/src/apps/git/help.ts +29 -20
  135. package/src/apps/if-bench-board.ts +2 -2
  136. package/src/apps/ps-top.ts +2 -2
  137. package/src/apps/setup-model-picker.ts +1 -1
  138. package/src/autocomplete.d.ts +5 -2
  139. package/src/autocomplete.ts +67 -43
  140. package/src/charts/chart-plan.d.ts +674 -0
  141. package/src/charts/chart-plan.ts +5081 -0
  142. package/src/charts/chart-svg.d.ts +59 -0
  143. package/src/charts/chart-svg.ts +4657 -0
  144. package/src/charts/table-data.d.ts +175 -0
  145. package/src/charts/table-data.ts +1478 -0
  146. package/src/chat/advisor-message.ts +11 -0
  147. package/src/chat/assistant-message.d.ts +5 -0
  148. package/src/chat/assistant-message.ts +219 -50
  149. package/src/chat/chat-transcript-builder.d.ts +2 -0
  150. package/src/chat/chat-transcript-builder.ts +5 -3
  151. package/src/chat/compaction-summary-message.d.ts +1 -0
  152. package/src/chat/compaction-summary-message.ts +8 -0
  153. package/src/chat/execution-shared.d.ts +6 -5
  154. package/src/chat/execution-shared.ts +42 -25
  155. package/src/chat/fence-figure.d.ts +42 -0
  156. package/src/chat/fence-figure.ts +80 -0
  157. package/src/chat/figure-markdown.d.ts +42 -0
  158. package/src/chat/figure-markdown.ts +174 -0
  159. package/src/chat/messages.d.ts +1 -1
  160. package/src/chat/messages.ts +1 -1
  161. package/src/chat/read-tool-group.ts +6 -4
  162. package/src/chat/skill-message.ts +6 -0
  163. package/src/chat/svg-figure.d.ts +33 -0
  164. package/src/chat/svg-figure.ts +399 -0
  165. package/src/chat/svg-source.d.ts +49 -0
  166. package/src/chat/svg-source.ts +188 -0
  167. package/src/chat/table-chart.d.ts +89 -0
  168. package/src/chat/table-chart.ts +294 -0
  169. package/src/chat/tool-execution.d.ts +2 -0
  170. package/src/chat/tool-execution.ts +80 -6
  171. package/src/chat/user-message.d.ts +15 -2
  172. package/src/chat/user-message.ts +55 -3
  173. package/src/chrome/shared.ts +53 -0
  174. package/src/chrome/transcript-container.d.ts +6 -0
  175. package/src/chrome/transcript-container.ts +93 -13
  176. package/src/components/box.d.ts +1 -0
  177. package/src/components/box.ts +5 -0
  178. package/src/components/disclosure.d.ts +1 -0
  179. package/src/components/disclosure.ts +8 -0
  180. package/src/components/editor.d.ts +15 -4
  181. package/src/components/editor.ts +294 -46
  182. package/src/components/form.d.ts +1 -0
  183. package/src/components/form.ts +8 -0
  184. package/src/components/image.d.ts +12 -44
  185. package/src/components/image.ts +47 -151
  186. package/src/components/key-value-list.d.ts +1 -0
  187. package/src/components/key-value-list.ts +4 -0
  188. package/src/components/layout/row.d.ts +1 -0
  189. package/src/components/layout/row.ts +7 -0
  190. package/src/components/layout/stack.d.ts +1 -0
  191. package/src/components/layout/stack.ts +7 -0
  192. package/src/components/markdown.d.ts +36 -3
  193. package/src/components/markdown.ts +433 -318
  194. package/src/components/scroll-view.d.ts +1 -0
  195. package/src/components/scroll-view.ts +5 -0
  196. package/src/components/section.d.ts +1 -0
  197. package/src/components/section.ts +5 -0
  198. package/src/components/settings-list.ts +2 -0
  199. package/src/components/table.d.ts +1 -0
  200. package/src/components/table.ts +4 -0
  201. package/src/components/text.d.ts +1 -0
  202. package/src/components/text.ts +7 -0
  203. package/src/components/tree-view.d.ts +7 -0
  204. package/src/components/tree-view.ts +38 -8
  205. package/src/components/truncated-text.d.ts +1 -0
  206. package/src/components/truncated-text.ts +5 -0
  207. package/src/desktop-notify.ts +1 -1
  208. package/src/fuzzy.d.ts +29 -1
  209. package/src/fuzzy.ts +115 -30
  210. package/src/key-hint-format.d.ts +1 -1
  211. package/src/key-hint-format.ts +1 -1
  212. package/src/keybinding-matchers.d.ts +3 -0
  213. package/src/keybinding-matchers.ts +6 -2
  214. package/src/keybindings.d.ts +1 -1
  215. package/src/keybindings.ts +1 -1
  216. package/src/kitty-graphics.d.ts +2 -2
  217. package/src/kitty-graphics.ts +4 -4
  218. package/src/loop-watchdog.d.ts +8 -7
  219. package/src/loop-watchdog.ts +18 -13
  220. package/src/multiplexers/cmux.d.ts +16 -0
  221. package/src/multiplexers/cmux.ts +29 -0
  222. package/src/multiplexers/herdr.d.ts +26 -0
  223. package/src/multiplexers/herdr.ts +64 -0
  224. package/src/multiplexers/notify.d.ts +13 -0
  225. package/src/multiplexers/notify.ts +24 -0
  226. package/src/multiplexers/orca.d.ts +12 -0
  227. package/src/multiplexers/orca.ts +16 -0
  228. package/src/multiplexers/screen.d.ts +9 -0
  229. package/src/multiplexers/screen.ts +15 -0
  230. package/src/multiplexers/tmux.d.ts +29 -0
  231. package/src/multiplexers/tmux.ts +40 -0
  232. package/src/multiplexers/types.d.ts +100 -0
  233. package/src/multiplexers/types.ts +110 -0
  234. package/src/multiplexers/wmux.d.ts +12 -0
  235. package/src/multiplexers/wmux.ts +16 -0
  236. package/src/multiplexers/zellij.d.ts +12 -0
  237. package/src/multiplexers/zellij.ts +24 -0
  238. package/src/native/backend.d.ts +3 -2
  239. package/src/native/backend.ts +174 -18
  240. package/src/native/blobs.d.ts +7 -3
  241. package/src/native/blobs.ts +47 -12
  242. package/src/native/encode.d.ts +3 -4
  243. package/src/native/encode.ts +58 -31
  244. package/src/native/overlay.d.ts +5 -0
  245. package/src/native/overlay.ts +13 -0
  246. package/src/native/reconcile.ts +4 -0
  247. package/src/overlays/agent-hub-projection.d.ts +6 -5
  248. package/src/overlays/agent-hub-projection.ts +124 -13
  249. package/src/overlays/agent-hub-types.d.ts +9 -0
  250. package/src/overlays/agent-hub-types.ts +6 -0
  251. package/src/overlays/agent-hub.ts +46 -31
  252. package/src/overlays/agent-transcript-viewer.ts +91 -23
  253. package/src/overlays/agents-hub.ts +28 -8
  254. package/src/overlays/annotation-overlay.ts +293 -90
  255. package/src/overlays/annotation-types.ts +2 -0
  256. package/src/overlays/ask-dialog.ts +7 -10
  257. package/src/overlays/btw-history-panel.ts +12 -1
  258. package/src/overlays/copy-selector.ts +30 -8
  259. package/src/overlays/extensions/state-manager.ts +52 -12
  260. package/src/overlays/history-search.ts +100 -7
  261. package/src/overlays/hook-selector.ts +12 -5
  262. package/src/overlays/jobs-panel.ts +28 -46
  263. package/src/overlays/model-browser.ts +341 -110
  264. package/src/overlays/model-hub.ts +300 -49
  265. package/src/overlays/move-overlay.ts +28 -14
  266. package/src/overlays/oauth-selector.ts +54 -70
  267. package/src/overlays/plan-review-overlay.ts +194 -70
  268. package/src/overlays/plugin-settings.ts +11 -5
  269. package/src/overlays/report-panel.ts +2 -2
  270. package/src/overlays/rewind-selector.ts +86 -17
  271. package/src/overlays/session-observer-registry.d.ts +2 -0
  272. package/src/overlays/session-observer-registry.ts +10 -2
  273. package/src/overlays/session-selector.ts +64 -26
  274. package/src/overlays/settings-defs.ts +4 -3
  275. package/src/overlays/settings-selector.ts +33 -8
  276. package/src/overlays/snapcompact-shape-preview.ts +2 -3
  277. package/src/overlays/stats-notice.ts +1 -1
  278. package/src/overlays/tree-selector.ts +41 -20
  279. package/src/overlays/usage-dashboard.ts +100 -50
  280. package/src/overlays/usage-display.ts +10 -4
  281. package/src/prompt/attachment-chips.ts +15 -11
  282. package/src/prompt/composer-attachments.ts +6 -4
  283. package/src/prompt/composer-cache.ts +4 -6
  284. package/src/prompt/composer.ts +6 -37
  285. package/src/prompt/custom-editor.ts +54 -26
  286. package/src/prompt/image-source.ts +2 -1
  287. package/src/prompt/interactive-context-helpers.ts +30 -18
  288. package/src/prompt/macos-spelling.ts +7 -0
  289. package/src/prompt/magic-keywords.ts +11 -2
  290. package/src/prompt/model-mention-autocomplete.ts +14 -14
  291. package/src/prompt/tips.txt +6 -5
  292. package/src/prompt/welcome.ts +235 -411
  293. package/src/prompt/word-completion.ts +38 -5
  294. package/src/render/composite.d.ts +2 -0
  295. package/src/render/composite.ts +64 -0
  296. package/src/render/output-pane.d.ts +1 -0
  297. package/src/render/output-pane.ts +6 -0
  298. package/src/render/render-utils.d.ts +1 -0
  299. package/src/render/render-utils.ts +11 -0
  300. package/src/render/tool-card.d.ts +6 -0
  301. package/src/render/tool-card.ts +15 -0
  302. package/src/render/width-aware-text.d.ts +2 -0
  303. package/src/render/width-aware-text.ts +5 -0
  304. package/src/setup/scenes/model.ts +2 -1
  305. package/src/setup/scenes/splash.ts +116 -53
  306. package/src/setup/scenes/types.ts +1 -0
  307. package/src/status-line/component.ts +292 -78
  308. package/src/status-line/host.ts +4 -1
  309. package/src/status-line/segments.ts +40 -22
  310. package/src/status-line/types.ts +15 -3
  311. package/src/terminal-capabilities.d.ts +27 -35
  312. package/src/terminal-capabilities.ts +75 -189
  313. package/src/terminal-multiplexer.d.ts +127 -14
  314. package/src/terminal-multiplexer.ts +112 -33
  315. package/src/terminal.d.ts +22 -6
  316. package/src/terminal.ts +139 -37
  317. package/src/theme/active-symbols.d.ts +1 -1
  318. package/src/theme/active-symbols.ts +1 -1
  319. package/src/theme/schema-validation.ts +1 -0
  320. package/src/theme/schema.d.ts +11 -2
  321. package/src/theme/schema.ts +14 -1
  322. package/src/theme/session-color.ts +1 -7
  323. package/src/theme/symbols.d.ts +1 -1
  324. package/src/theme/symbols.ts +6 -1
  325. package/src/theme/theme-class.d.ts +7 -0
  326. package/src/theme/theme-class.ts +26 -1
  327. package/src/theme/theme-schema.json +5 -1
  328. package/src/theme/theme.d.ts +1 -1
  329. package/src/theme/theme.ts +8 -4
  330. package/src/theme/tui-adapters.d.ts +3 -0
  331. package/src/theme/tui-adapters.ts +68 -0
  332. package/src/tmux.d.ts +11 -4
  333. package/src/tmux.ts +47 -11
  334. package/src/tools/ast-edit.ts +59 -35
  335. package/src/tools/ast-grep.ts +35 -16
  336. package/src/tools/bash.ts +92 -42
  337. package/src/tools/daemon.d.ts +1 -1
  338. package/src/tools/daemon.ts +1 -1
  339. package/src/tools/default-renderer.ts +35 -19
  340. package/src/tools/edit.ts +142 -120
  341. package/src/tools/eval-format/index.ts +28 -2
  342. package/src/tools/eval-format/javascript.ts +7 -5
  343. package/src/tools/eval.d.ts +22 -3
  344. package/src/tools/eval.ts +364 -208
  345. package/src/tools/fetch.ts +5 -6
  346. package/src/tools/glob.d.ts +4 -0
  347. package/src/tools/glob.ts +33 -65
  348. package/src/tools/grep.ts +81 -48
  349. package/src/tools/index.ts +2 -0
  350. package/src/tools/lsp.ts +81 -59
  351. package/src/tools/mcp.ts +111 -68
  352. package/src/tools/native-view.d.ts +47 -0
  353. package/src/tools/native-view.ts +102 -1
  354. package/src/tools/output-meta.d.ts +11 -1
  355. package/src/tools/output-meta.ts +23 -3
  356. package/src/tools/read.ts +7 -5
  357. package/src/tools/renderer.d.ts +37 -2
  358. package/src/tools/renderer.ts +42 -2
  359. package/src/tools/streaming-output.d.ts +20 -1
  360. package/src/tools/streaming-output.ts +149 -50
  361. package/src/tools/task.d.ts +0 -4
  362. package/src/tools/task.ts +112 -59
  363. package/src/tools/todo.ts +119 -30
  364. package/src/tools/wait.ts +34 -24
  365. package/src/tools/web-search.ts +1 -1
  366. package/src/tools/write.d.ts +9 -2
  367. package/src/tools/write.ts +147 -95
  368. package/src/tools/yield.d.ts +22 -0
  369. package/src/tools/yield.ts +225 -0
  370. package/src/tui.d.ts +31 -0
  371. package/src/tui.ts +309 -160
  372. package/src/utils.d.ts +6 -0
  373. package/src/utils.ts +19 -0
  374. package/src/vim.d.ts +19 -5
  375. package/src/vim.ts +333 -18
  376. package/src/windows-input-mode.ts +8 -0
@@ -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)}`;
@@ -838,6 +937,24 @@ function metaColumnsWidth(widths: readonly number[]): number {
838
937
  /** What the per-row perf column shows at the current width. */
839
938
  type PerfMode = "off" | "tps" | "full";
840
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
+
841
958
  /**
842
959
  * The reusable browser component. Renders a fixed-height block
843
960
  * (`maxVisible + LIST_ROW_START + DETAIL_ROWS` rows) so host mouse geometry
@@ -855,7 +972,17 @@ export class ModelBrowser implements Component {
855
972
  #roles: RoleAssignments = {};
856
973
  #mruOrder: ReadonlyArray<string> = [];
857
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 };
858
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;
859
986
  #hoveredIndex: number | null = null;
860
987
  #maxVisible = 10;
861
988
  #showProvider: boolean;
@@ -1024,6 +1151,22 @@ export class ModelBrowser implements Component {
1024
1151
  return this.#menu.visibleItems.length;
1025
1152
  }
1026
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
+
1027
1170
  /** Move selection to `selector`; false when it is not in the current view. */
1028
1171
  selectSelector(selector: string): boolean {
1029
1172
  if (!this.#menu.setSelectedKey(selector)) return false;
@@ -1042,17 +1185,26 @@ export class ModelBrowser implements Component {
1042
1185
  * unfiltered when the query is blank.
1043
1186
  */
1044
1187
  #filterItems(items: readonly ModelBrowserItem[], query: string): readonly ModelBrowserItem[] {
1045
- const base = items.filter(item => !this.#isDisabled(item));
1046
- const ranked = this.#preserveQueryOrder
1047
- ? query.trim()
1048
- ? fuzzyRank(base, query, modelSearchText).map(result => result.item)
1049
- : base
1050
- : rankModelItems(query, base, {
1051
- roles: this.#roles,
1052
- mruOrder: this.#mruOrder,
1053
- affinity: this.#affinity,
1054
- });
1055
- 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));
1056
1208
  }
1057
1209
 
1058
1210
  /** True when `item`'s context window is smaller than the live session token count (grayed row; hosts compact before switching). */
@@ -1273,14 +1425,76 @@ export class ModelBrowser implements Component {
1273
1425
  return index;
1274
1426
  }
1275
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
+
1276
1487
  /** Measured TPS/TTFT, falling back to the catalog TPS as an estimated `~118t/s`. */
1277
1488
  #perfCell(item: ModelBrowserItem, mode: PerfMode): string {
1278
1489
  if (mode === "off") return "";
1279
- const perf = this.#perf.get(item.selector);
1280
- if (perf) {
1281
- const tps = formatTps(perf.tps);
1282
- if (mode === "full" && perf.ttftMs !== null) return `${formatTtft(perf.ttftMs)} ${tps}`;
1283
- 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}`;
1284
1498
  }
1285
1499
  const tps = item.model.tps;
1286
1500
  return tps != null && Number.isFinite(tps) && tps > 0 ? `~${formatTps(tps)}` : "";
@@ -1295,7 +1509,8 @@ export class ModelBrowser implements Component {
1295
1509
  costWidth: number,
1296
1510
  intelligenceWidth: number,
1297
1511
  perfWidth: number,
1298
- perfMode: PerfMode,
1512
+ metrics: MetricCells,
1513
+ perfCell: string,
1299
1514
  ): string {
1300
1515
  if (item.id === "separator") {
1301
1516
  const dashCount = Math.max(0, width - 4);
@@ -1312,21 +1527,17 @@ export class ModelBrowser implements Component {
1312
1527
  : item.id;
1313
1528
  const currentMark =
1314
1529
  item.selector === this.#currentSelector ? ` ${theme.fg("success", theme.status.enabled)}` : "";
1315
- const ompBadge = this.#settings.isOmpOriginProvider?.(item.provider)
1316
- ? ` ${theme.fg("dim", tuiText("models.originOmp", "from OMP"))}`
1317
- : "";
1318
1530
  const overLimit = overContext
1319
1531
  ? ` ${theme.status.disabled} context>${formatNumber(item.model.contextWindow ?? 0).toLowerCase()}`
1320
1532
  : "";
1321
- let left = `${prefix}${providerPrefix}${name}${ompBadge}${currentMark}${overLimit}`;
1533
+ let left = `${prefix}${providerPrefix}${name}${currentMark}${overLimit}`;
1322
1534
 
1323
1535
  // Metric columns collapse when empty or when the row needs room for its name.
1324
1536
  const cols: string[] = [];
1325
- if (intelligenceWidth > 0)
1326
- cols.push(theme.fg("dim", padLeftVisible(formatIntelligence(item.model), intelligenceWidth)));
1327
- if (perfWidth > 0) cols.push(theme.fg("dim", padLeftVisible(this.#perfCell(item, perfMode), perfWidth)));
1328
- if (ctxWidth > 0) cols.push(theme.fg("dim", padLeftVisible(formatContext(item.model), ctxWidth)));
1329
- if (costWidth > 0) cols.push(theme.fg("dim", padLeftVisible(formatCostPair(item.model), costWidth)));
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)));
1330
1541
  const metaWidth = metaColumnsWidth([intelligenceWidth, perfWidth, ctxWidth, costWidth]);
1331
1542
  const available = Math.max(1, width - metaWidth - (cols.length > 0 ? 1 : 0));
1332
1543
  left = truncateToWidth(left, available);
@@ -1365,10 +1576,10 @@ export class ModelBrowser implements Component {
1365
1576
  if (model.input.includes("image")) facts.push("vision");
1366
1577
  const intelligence = formatIntelligence(model);
1367
1578
  if (intelligence) facts.push(intelligence);
1368
- const perf = this.#perf.get(selected.selector);
1369
- if (perf) {
1370
- facts.push(`~${formatTps(perf.tps)}`);
1371
- 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`);
1372
1583
  } else if (model.tps != null && Number.isFinite(model.tps) && model.tps > 0) {
1373
1584
  facts.push(`~${formatTps(model.tps)}`);
1374
1585
  }
@@ -1421,10 +1632,7 @@ export class ModelBrowser implements Component {
1421
1632
 
1422
1633
  if (total === 0) {
1423
1634
  const message =
1424
- this.#emptyText?.() ??
1425
- (this.query.trim()
1426
- ? tuiText("mbNoMatchingModels", " No matching models")
1427
- : tuiText("mbNoModelsInScope", " No models available in this scope"));
1635
+ this.#emptyText?.() ?? (this.query.trim() ? " No matching models" : " No models available in this scope");
1428
1636
  lines.push(truncateToWidth(theme.fg("muted", message), width));
1429
1637
  for (let i = 1; i < this.#maxVisible; i++) lines.push("");
1430
1638
  } else {
@@ -1435,15 +1643,22 @@ export class ModelBrowser implements Component {
1435
1643
  const perfMode: PerfMode = width >= PERF_FULL_MIN_WIDTH ? "full" : width >= PERF_TPS_MIN_WIDTH ? "tps" : "off";
1436
1644
  let intelligenceWidth = 0;
1437
1645
  let perfWidth = 0;
1646
+ const visible = this.#menu.visibleItems;
1647
+ const windowMetrics: MetricCells[] = [];
1648
+ const windowPerf: string[] = [];
1438
1649
  for (let i = startIndex; i < endIndex; i++) {
1439
- const item = this.#menu.visibleItems[i];
1650
+ const item = visible[i];
1440
1651
  if (!item) continue;
1441
- ctxWidth = Math.max(ctxWidth, visibleWidth(formatContext(item.model)));
1442
- 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);
1443
1658
  if (perfMode !== "off") {
1444
- intelligenceWidth = Math.max(intelligenceWidth, visibleWidth(formatIntelligence(item.model)));
1659
+ intelligenceWidth = Math.max(intelligenceWidth, metrics.intelligenceWidth);
1445
1660
  }
1446
- perfWidth = Math.max(perfWidth, visibleWidth(this.#perfCell(item, perfMode)));
1661
+ perfWidth = Math.max(perfWidth, visibleWidth(perfCell));
1447
1662
  }
1448
1663
  // Preserve at least a readable name by dropping cost, then context.
1449
1664
  let nameRoom = width - 2 - metaColumnsWidth([intelligenceWidth, perfWidth, ctxWidth, costWidth]);
@@ -1453,7 +1668,7 @@ export class ModelBrowser implements Component {
1453
1668
 
1454
1669
  const rows: string[] = [];
1455
1670
  for (let i = startIndex; i < endIndex; i++) {
1456
- const item = this.#menu.visibleItems[i];
1671
+ const item = visible[i];
1457
1672
  if (!item) continue;
1458
1673
  rows.push(
1459
1674
  this.#renderRow(
@@ -1465,7 +1680,8 @@ export class ModelBrowser implements Component {
1465
1680
  costWidth,
1466
1681
  intelligenceWidth,
1467
1682
  perfWidth,
1468
- perfMode,
1683
+ windowMetrics[i - startIndex]!,
1684
+ windowPerf[i - startIndex]!,
1469
1685
  ),
1470
1686
  );
1471
1687
  }
@@ -1636,10 +1852,10 @@ export class ModelBrowser implements Component {
1636
1852
  if (model.input.includes("image")) facts.push("vision");
1637
1853
  const intelligence = formatIntelligence(model);
1638
1854
  if (intelligence) facts.push(intelligence);
1639
- const perf = this.#perf.get(selected.selector);
1640
- if (perf) {
1641
- facts.push(`~${formatTps(perf.tps)}`);
1642
- 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`);
1643
1859
  } else if (model.tps != null && Number.isFinite(model.tps) && model.tps > 0) {
1644
1860
  facts.push(`~${formatTps(model.tps)}`);
1645
1861
  }
@@ -1751,7 +1967,9 @@ export class ModelBrowser implements Component {
1751
1967
  const facts: Record<string, TspText | number> = {};
1752
1968
  const int = model.int != null && Number.isFinite(model.int) ? model.int : undefined;
1753
1969
  if (int !== undefined) facts.int = String(Math.round(int));
1754
- const speed = this.#perfCell(item, "tps").replace(/t\/s$/, "");
1970
+ const speed = this.#perfCell(item, "tps")
1971
+ .replace(/\s*t\/s\s*/, " ")
1972
+ .trim();
1755
1973
  if (speed) facts.speed = speed;
1756
1974
  if (model.contextWindow) facts.ctx = model.contextWindow;
1757
1975
  facts.price = pickerPrice(model);
@@ -1893,7 +2111,7 @@ export class ModelBrowser implements Component {
1893
2111
  pickerPreview(mode: "full" | "compact", current?: string): readonly NativeChild[] {
1894
2112
  const selected = this.getSelected();
1895
2113
  const item = selected && !this.#isDisabled(selected) ? selected : undefined;
1896
- const key = `${mode}\0${current ?? ""}`;
2114
+ const key = `${mode}\0${current ?? ""}\0${this.#settings.revision}`;
1897
2115
  const memo = this.#pickerPreview;
1898
2116
  if (
1899
2117
  memo !== undefined &&
@@ -1913,7 +2131,7 @@ export class ModelBrowser implements Component {
1913
2131
  modelPreview(item: ModelBrowserItem, mode: "full" | "compact", current: string | undefined): NativeChild[] {
1914
2132
  const model = item.model;
1915
2133
  const selector = `${model.provider}/${model.id}`;
1916
- const perf = this.#perf.get(selector);
2134
+ const measured = this.#perfFor(item);
1917
2135
  const ctx = model.contextWindow ?? 0;
1918
2136
  const out = model.maxTokens ?? 0;
1919
2137
  const overContext = this.isOverContext(item);
@@ -1973,10 +2191,12 @@ export class ModelBrowser implements Component {
1973
2191
  if (badges.length > 0) children.push(row(badges, { gap: "xs", wrap: true }));
1974
2192
 
1975
2193
  const speed: string[] = [];
1976
- if (perf) {
1977
- speed.push(`${formatTps(perf.tps).replace("t/s", " t/s")}`);
1978
- if (perf.ttftMs !== null) speed.push(`${formatTtft(perf.ttftMs).replace("s", " s")} TTFT`);
1979
- 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"}`);
1980
2200
  } else if (model.tps != null && Number.isFinite(model.tps) && model.tps > 0) {
1981
2201
  speed.push(`~${formatTps(model.tps).replace("t/s", " t/s")} (catalog)`);
1982
2202
  }
@@ -1985,6 +2205,17 @@ export class ModelBrowser implements Component {
1985
2205
  if (v) facts.push({ k: [span(k, "muted")], v: [span(v, "mono")] });
1986
2206
  };
1987
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
+ }
1988
2219
  fact("Max output", out > 0 ? out.toLocaleString("en-US") : undefined);
1989
2220
  fact("Price", isFreeModel(model) ? "free" : `${previewPrice(model)} per M`);
1990
2221
  fact("Speed", speed.length > 0 ? speed.join(" · ") : undefined);