@linxiraos/pi-tui 1.1.27 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (373) hide show
  1. package/CHANGELOG.md +47 -463
  2. package/README.md +7 -5
  3. package/dist/types/app-keybindings.d.ts +1 -1
  4. package/dist/types/apps/cleanse-board.d.ts +1 -1
  5. package/dist/types/apps/debug/log-viewer.d.ts +4 -0
  6. package/dist/types/apps/debug/raw-sse-buffer.d.ts +2 -0
  7. package/dist/types/apps/git/git-tui.d.ts +2 -2
  8. package/dist/types/apps/git/help.d.ts +3 -1
  9. package/dist/types/apps/if-bench-board.d.ts +2 -2
  10. package/dist/types/apps/setup-model-picker.d.ts +1 -1
  11. package/dist/types/autocomplete.d.ts +5 -2
  12. package/dist/types/charts/chart-plan.d.ts +674 -0
  13. package/dist/types/charts/chart-svg.d.ts +59 -0
  14. package/dist/types/charts/table-data.d.ts +175 -0
  15. package/dist/types/chat/assistant-message.d.ts +5 -0
  16. package/dist/types/chat/chat-transcript-builder.d.ts +2 -0
  17. package/dist/types/chat/compaction-summary-message.d.ts +1 -0
  18. package/dist/types/chat/execution-shared.d.ts +6 -5
  19. package/dist/types/chat/fence-figure.d.ts +42 -0
  20. package/dist/types/chat/figure-markdown.d.ts +42 -0
  21. package/dist/types/chat/messages.d.ts +1 -1
  22. package/dist/types/chat/svg-figure.d.ts +33 -0
  23. package/dist/types/chat/svg-source.d.ts +49 -0
  24. package/dist/types/chat/table-chart.d.ts +89 -0
  25. package/dist/types/chat/tool-execution.d.ts +2 -0
  26. package/dist/types/chat/user-message.d.ts +15 -2
  27. package/dist/types/chrome/shared.d.ts +9 -0
  28. package/dist/types/chrome/transcript-container.d.ts +6 -0
  29. package/dist/types/components/box.d.ts +1 -0
  30. package/dist/types/components/disclosure.d.ts +1 -0
  31. package/dist/types/components/editor.d.ts +15 -4
  32. package/dist/types/components/form.d.ts +1 -0
  33. package/dist/types/components/image.d.ts +12 -44
  34. package/dist/types/components/key-value-list.d.ts +1 -0
  35. package/dist/types/components/layout/row.d.ts +1 -0
  36. package/dist/types/components/layout/stack.d.ts +1 -0
  37. package/dist/types/components/markdown.d.ts +36 -3
  38. package/dist/types/components/scroll-view.d.ts +1 -0
  39. package/dist/types/components/section.d.ts +1 -0
  40. package/dist/types/components/table.d.ts +1 -0
  41. package/dist/types/components/text.d.ts +1 -0
  42. package/dist/types/components/tree-view.d.ts +7 -0
  43. package/dist/types/components/truncated-text.d.ts +1 -0
  44. package/dist/types/fuzzy.d.ts +29 -1
  45. package/dist/types/key-hint-format.d.ts +1 -1
  46. package/dist/types/keybinding-matchers.d.ts +3 -0
  47. package/dist/types/keybindings.d.ts +1 -1
  48. package/dist/types/kitty-graphics.d.ts +2 -2
  49. package/dist/types/loop-watchdog.d.ts +8 -7
  50. package/dist/types/multiplexers/cmux.d.ts +16 -0
  51. package/dist/types/multiplexers/herdr.d.ts +26 -0
  52. package/dist/types/multiplexers/notify.d.ts +13 -0
  53. package/dist/types/multiplexers/orca.d.ts +12 -0
  54. package/dist/types/multiplexers/screen.d.ts +9 -0
  55. package/dist/types/multiplexers/tmux.d.ts +29 -0
  56. package/dist/types/multiplexers/types.d.ts +100 -0
  57. package/dist/types/multiplexers/wmux.d.ts +12 -0
  58. package/dist/types/multiplexers/zellij.d.ts +12 -0
  59. package/dist/types/native/backend.d.ts +3 -2
  60. package/dist/types/native/blobs.d.ts +7 -3
  61. package/dist/types/native/encode.d.ts +3 -4
  62. package/dist/types/native/overlay.d.ts +5 -0
  63. package/dist/types/overlays/agent-hub-projection.d.ts +6 -5
  64. package/dist/types/overlays/agent-hub-types.d.ts +9 -0
  65. package/dist/types/overlays/agent-hub.d.ts +2 -0
  66. package/dist/types/overlays/agent-transcript-viewer.d.ts +11 -2
  67. package/dist/types/overlays/agents-hub.d.ts +9 -0
  68. package/dist/types/overlays/annotation-overlay.d.ts +19 -2
  69. package/dist/types/overlays/annotation-types.d.ts +2 -0
  70. package/dist/types/overlays/ask-dialog.d.ts +5 -2
  71. package/dist/types/overlays/history-search.d.ts +10 -0
  72. package/dist/types/overlays/hook-selector.d.ts +9 -4
  73. package/dist/types/overlays/jobs-panel.d.ts +1 -7
  74. package/dist/types/overlays/model-browser.d.ts +54 -7
  75. package/dist/types/overlays/model-hub.d.ts +23 -0
  76. package/dist/types/overlays/oauth-selector.d.ts +0 -1
  77. package/dist/types/overlays/plan-review-overlay.d.ts +3 -2
  78. package/dist/types/overlays/session-observer-registry.d.ts +2 -0
  79. package/dist/types/overlays/session-selector.d.ts +6 -2
  80. package/dist/types/overlays/settings-defs.d.ts +1 -0
  81. package/dist/types/overlays/usage-dashboard.d.ts +8 -4
  82. package/dist/types/overlays/usage-display.d.ts +3 -1
  83. package/dist/types/prompt/composer.d.ts +1 -1
  84. package/dist/types/prompt/custom-editor.d.ts +11 -8
  85. package/dist/types/prompt/image-source.d.ts +2 -1
  86. package/dist/types/prompt/interactive-context-helpers.d.ts +4 -1
  87. package/dist/types/prompt/model-mention-autocomplete.d.ts +1 -1
  88. package/dist/types/prompt/welcome.d.ts +6 -1
  89. package/dist/types/render/composite.d.ts +2 -0
  90. package/dist/types/render/output-pane.d.ts +1 -0
  91. package/dist/types/render/render-utils.d.ts +1 -0
  92. package/dist/types/render/tool-card.d.ts +6 -0
  93. package/dist/types/render/width-aware-text.d.ts +2 -0
  94. package/dist/types/setup/scenes/types.d.ts +1 -0
  95. package/dist/types/status-line/component.d.ts +3 -0
  96. package/dist/types/status-line/host.d.ts +4 -1
  97. package/dist/types/status-line/segments.d.ts +2 -0
  98. package/dist/types/status-line/types.d.ts +14 -3
  99. package/dist/types/terminal-capabilities.d.ts +27 -35
  100. package/dist/types/terminal-multiplexer.d.ts +127 -14
  101. package/dist/types/terminal.d.ts +22 -6
  102. package/dist/types/theme/active-symbols.d.ts +1 -1
  103. package/dist/types/theme/schema.d.ts +11 -2
  104. package/dist/types/theme/symbols.d.ts +1 -1
  105. package/dist/types/theme/theme-class.d.ts +7 -0
  106. package/dist/types/theme/theme.d.ts +1 -1
  107. package/dist/types/theme/tui-adapters.d.ts +3 -0
  108. package/dist/types/tmux.d.ts +11 -4
  109. package/dist/types/tools/daemon.d.ts +1 -1
  110. package/dist/types/tools/eval.d.ts +22 -3
  111. package/dist/types/tools/glob.d.ts +4 -0
  112. package/dist/types/tools/native-view.d.ts +47 -0
  113. package/dist/types/tools/output-meta.d.ts +11 -1
  114. package/dist/types/tools/renderer.d.ts +37 -2
  115. package/dist/types/tools/streaming-output.d.ts +20 -1
  116. package/dist/types/tools/task.d.ts +0 -4
  117. package/dist/types/tools/write.d.ts +9 -2
  118. package/dist/types/tools/yield.d.ts +22 -0
  119. package/dist/types/tui.d.ts +31 -0
  120. package/dist/types/utils.d.ts +6 -0
  121. package/dist/types/vim.d.ts +19 -5
  122. package/package.json +13 -9
  123. package/src/app-keybindings.d.ts +1 -1
  124. package/src/app-keybindings.ts +1 -1
  125. package/src/apps/autoresearch-dashboard.ts +7 -3
  126. package/src/apps/cleanse-board.ts +2 -2
  127. package/src/apps/cleanse-picker.ts +1 -1
  128. package/src/apps/debug/log-viewer.ts +188 -72
  129. package/src/apps/debug/raw-sse-buffer.ts +8 -0
  130. package/src/apps/debug/raw-sse.ts +39 -5
  131. package/src/apps/git/diff-pane.ts +26 -5
  132. package/src/apps/git/git-tui.ts +2 -2
  133. package/src/apps/git/help.ts +29 -20
  134. package/src/apps/if-bench-board.ts +2 -2
  135. package/src/apps/ps-top.ts +2 -2
  136. package/src/apps/setup-model-picker.ts +1 -1
  137. package/src/autocomplete.d.ts +5 -2
  138. package/src/autocomplete.ts +67 -43
  139. package/src/charts/chart-plan.d.ts +674 -0
  140. package/src/charts/chart-plan.ts +5081 -0
  141. package/src/charts/chart-svg.d.ts +59 -0
  142. package/src/charts/chart-svg.ts +4657 -0
  143. package/src/charts/table-data.d.ts +175 -0
  144. package/src/charts/table-data.ts +1478 -0
  145. package/src/chat/advisor-message.ts +11 -0
  146. package/src/chat/assistant-message.d.ts +5 -0
  147. package/src/chat/assistant-message.ts +219 -50
  148. package/src/chat/chat-transcript-builder.d.ts +2 -0
  149. package/src/chat/chat-transcript-builder.ts +5 -3
  150. package/src/chat/compaction-summary-message.d.ts +1 -0
  151. package/src/chat/compaction-summary-message.ts +8 -0
  152. package/src/chat/execution-shared.d.ts +6 -5
  153. package/src/chat/execution-shared.ts +42 -25
  154. package/src/chat/fence-figure.d.ts +42 -0
  155. package/src/chat/fence-figure.ts +80 -0
  156. package/src/chat/figure-markdown.d.ts +42 -0
  157. package/src/chat/figure-markdown.ts +174 -0
  158. package/src/chat/messages.d.ts +1 -1
  159. package/src/chat/messages.ts +1 -1
  160. package/src/chat/read-tool-group.ts +6 -4
  161. package/src/chat/skill-message.ts +6 -0
  162. package/src/chat/svg-figure.d.ts +33 -0
  163. package/src/chat/svg-figure.ts +399 -0
  164. package/src/chat/svg-source.d.ts +49 -0
  165. package/src/chat/svg-source.ts +188 -0
  166. package/src/chat/table-chart.d.ts +89 -0
  167. package/src/chat/table-chart.ts +294 -0
  168. package/src/chat/tool-execution.d.ts +2 -0
  169. package/src/chat/tool-execution.ts +80 -6
  170. package/src/chat/user-message.d.ts +15 -2
  171. package/src/chat/user-message.ts +55 -3
  172. package/src/chrome/shared.ts +53 -0
  173. package/src/chrome/transcript-container.d.ts +6 -0
  174. package/src/chrome/transcript-container.ts +93 -13
  175. package/src/components/box.d.ts +1 -0
  176. package/src/components/box.ts +5 -0
  177. package/src/components/disclosure.d.ts +1 -0
  178. package/src/components/disclosure.ts +8 -0
  179. package/src/components/editor.d.ts +15 -4
  180. package/src/components/editor.ts +294 -46
  181. package/src/components/form.d.ts +1 -0
  182. package/src/components/form.ts +8 -0
  183. package/src/components/image.d.ts +12 -44
  184. package/src/components/image.ts +47 -151
  185. package/src/components/key-value-list.d.ts +1 -0
  186. package/src/components/key-value-list.ts +4 -0
  187. package/src/components/layout/row.d.ts +1 -0
  188. package/src/components/layout/row.ts +7 -0
  189. package/src/components/layout/stack.d.ts +1 -0
  190. package/src/components/layout/stack.ts +7 -0
  191. package/src/components/markdown.d.ts +36 -3
  192. package/src/components/markdown.ts +433 -318
  193. package/src/components/scroll-view.d.ts +1 -0
  194. package/src/components/scroll-view.ts +5 -0
  195. package/src/components/section.d.ts +1 -0
  196. package/src/components/section.ts +5 -0
  197. package/src/components/settings-list.ts +2 -0
  198. package/src/components/table.d.ts +1 -0
  199. package/src/components/table.ts +4 -0
  200. package/src/components/text.d.ts +1 -0
  201. package/src/components/text.ts +7 -0
  202. package/src/components/tree-view.d.ts +7 -0
  203. package/src/components/tree-view.ts +38 -8
  204. package/src/components/truncated-text.d.ts +1 -0
  205. package/src/components/truncated-text.ts +5 -0
  206. package/src/desktop-notify.ts +1 -1
  207. package/src/fuzzy.d.ts +29 -1
  208. package/src/fuzzy.ts +115 -30
  209. package/src/key-hint-format.d.ts +1 -1
  210. package/src/key-hint-format.ts +1 -1
  211. package/src/keybinding-matchers.d.ts +3 -0
  212. package/src/keybinding-matchers.ts +6 -2
  213. package/src/keybindings.d.ts +1 -1
  214. package/src/keybindings.ts +1 -1
  215. package/src/kitty-graphics.d.ts +2 -2
  216. package/src/kitty-graphics.ts +4 -4
  217. package/src/loop-watchdog.d.ts +8 -7
  218. package/src/loop-watchdog.ts +18 -13
  219. package/src/multiplexers/cmux.d.ts +16 -0
  220. package/src/multiplexers/cmux.ts +29 -0
  221. package/src/multiplexers/herdr.d.ts +26 -0
  222. package/src/multiplexers/herdr.ts +64 -0
  223. package/src/multiplexers/notify.d.ts +13 -0
  224. package/src/multiplexers/notify.ts +24 -0
  225. package/src/multiplexers/orca.d.ts +12 -0
  226. package/src/multiplexers/orca.ts +16 -0
  227. package/src/multiplexers/screen.d.ts +9 -0
  228. package/src/multiplexers/screen.ts +15 -0
  229. package/src/multiplexers/tmux.d.ts +29 -0
  230. package/src/multiplexers/tmux.ts +40 -0
  231. package/src/multiplexers/types.d.ts +100 -0
  232. package/src/multiplexers/types.ts +110 -0
  233. package/src/multiplexers/wmux.d.ts +12 -0
  234. package/src/multiplexers/wmux.ts +16 -0
  235. package/src/multiplexers/zellij.d.ts +12 -0
  236. package/src/multiplexers/zellij.ts +24 -0
  237. package/src/native/backend.d.ts +3 -2
  238. package/src/native/backend.ts +174 -18
  239. package/src/native/blobs.d.ts +7 -3
  240. package/src/native/blobs.ts +47 -12
  241. package/src/native/encode.d.ts +3 -4
  242. package/src/native/encode.ts +58 -31
  243. package/src/native/overlay.d.ts +5 -0
  244. package/src/native/overlay.ts +13 -0
  245. package/src/native/reconcile.ts +4 -0
  246. package/src/overlays/agent-hub-projection.d.ts +6 -5
  247. package/src/overlays/agent-hub-projection.ts +124 -13
  248. package/src/overlays/agent-hub-types.d.ts +9 -0
  249. package/src/overlays/agent-hub-types.ts +6 -0
  250. package/src/overlays/agent-hub.ts +44 -29
  251. package/src/overlays/agent-transcript-viewer.ts +91 -23
  252. package/src/overlays/agents-hub.ts +28 -8
  253. package/src/overlays/annotation-overlay.ts +293 -90
  254. package/src/overlays/annotation-types.ts +2 -0
  255. package/src/overlays/ask-dialog.ts +7 -10
  256. package/src/overlays/btw-history-panel.ts +12 -1
  257. package/src/overlays/copy-selector.ts +30 -8
  258. package/src/overlays/extensions/state-manager.ts +52 -12
  259. package/src/overlays/history-search.ts +100 -7
  260. package/src/overlays/hook-selector.ts +12 -5
  261. package/src/overlays/jobs-panel.ts +28 -46
  262. package/src/overlays/model-browser.ts +341 -110
  263. package/src/overlays/model-hub.ts +300 -49
  264. package/src/overlays/move-overlay.ts +28 -14
  265. package/src/overlays/oauth-selector.ts +54 -70
  266. package/src/overlays/plan-review-overlay.ts +194 -70
  267. package/src/overlays/plugin-settings.ts +3 -3
  268. package/src/overlays/report-panel.ts +2 -2
  269. package/src/overlays/rewind-selector.ts +86 -17
  270. package/src/overlays/session-observer-registry.d.ts +2 -0
  271. package/src/overlays/session-observer-registry.ts +10 -2
  272. package/src/overlays/session-selector.ts +64 -26
  273. package/src/overlays/settings-defs.ts +4 -3
  274. package/src/overlays/settings-selector.ts +33 -8
  275. package/src/overlays/snapcompact-shape-preview.ts +2 -3
  276. package/src/overlays/stats-notice.ts +1 -1
  277. package/src/overlays/tree-selector.ts +41 -20
  278. package/src/overlays/usage-dashboard.ts +100 -50
  279. package/src/overlays/usage-display.ts +10 -4
  280. package/src/prompt/attachment-chips.ts +15 -11
  281. package/src/prompt/composer-attachments.ts +6 -4
  282. package/src/prompt/composer.ts +4 -15
  283. package/src/prompt/custom-editor.ts +54 -26
  284. package/src/prompt/image-source.ts +2 -1
  285. package/src/prompt/interactive-context-helpers.ts +30 -18
  286. package/src/prompt/macos-spelling.ts +7 -0
  287. package/src/prompt/magic-keywords.ts +11 -2
  288. package/src/prompt/model-mention-autocomplete.ts +14 -14
  289. package/src/prompt/tips.txt +6 -5
  290. package/src/prompt/welcome.ts +55 -12
  291. package/src/prompt/word-completion.ts +38 -5
  292. package/src/render/composite.d.ts +2 -0
  293. package/src/render/composite.ts +64 -0
  294. package/src/render/output-pane.d.ts +1 -0
  295. package/src/render/output-pane.ts +6 -0
  296. package/src/render/render-utils.d.ts +1 -0
  297. package/src/render/render-utils.ts +11 -0
  298. package/src/render/tool-card.d.ts +6 -0
  299. package/src/render/tool-card.ts +15 -0
  300. package/src/render/width-aware-text.d.ts +2 -0
  301. package/src/render/width-aware-text.ts +5 -0
  302. package/src/setup/scenes/model.ts +2 -1
  303. package/src/setup/scenes/types.ts +1 -0
  304. package/src/status-line/component.ts +292 -78
  305. package/src/status-line/host.ts +4 -1
  306. package/src/status-line/segments.ts +40 -22
  307. package/src/status-line/types.ts +15 -3
  308. package/src/terminal-capabilities.d.ts +27 -35
  309. package/src/terminal-capabilities.ts +75 -189
  310. package/src/terminal-multiplexer.d.ts +127 -14
  311. package/src/terminal-multiplexer.ts +112 -33
  312. package/src/terminal.d.ts +22 -6
  313. package/src/terminal.ts +139 -37
  314. package/src/theme/active-symbols.d.ts +1 -1
  315. package/src/theme/active-symbols.ts +1 -1
  316. package/src/theme/schema-validation.ts +1 -0
  317. package/src/theme/schema.d.ts +11 -2
  318. package/src/theme/schema.ts +14 -1
  319. package/src/theme/session-color.ts +1 -7
  320. package/src/theme/symbols.d.ts +1 -1
  321. package/src/theme/symbols.ts +6 -1
  322. package/src/theme/theme-class.d.ts +7 -0
  323. package/src/theme/theme-class.ts +26 -1
  324. package/src/theme/theme-schema.json +5 -1
  325. package/src/theme/theme.d.ts +1 -1
  326. package/src/theme/theme.ts +8 -4
  327. package/src/theme/tui-adapters.d.ts +3 -0
  328. package/src/theme/tui-adapters.ts +68 -0
  329. package/src/tmux.d.ts +11 -4
  330. package/src/tmux.ts +47 -11
  331. package/src/tools/ast-edit.ts +59 -35
  332. package/src/tools/ast-grep.ts +35 -16
  333. package/src/tools/bash.ts +92 -42
  334. package/src/tools/daemon.d.ts +1 -1
  335. package/src/tools/daemon.ts +1 -1
  336. package/src/tools/default-renderer.ts +35 -19
  337. package/src/tools/edit.ts +142 -120
  338. package/src/tools/eval-format/index.ts +28 -2
  339. package/src/tools/eval-format/javascript.ts +7 -5
  340. package/src/tools/eval.d.ts +22 -3
  341. package/src/tools/eval.ts +364 -208
  342. package/src/tools/fetch.ts +5 -6
  343. package/src/tools/glob.d.ts +4 -0
  344. package/src/tools/glob.ts +33 -65
  345. package/src/tools/grep.ts +81 -48
  346. package/src/tools/index.ts +2 -0
  347. package/src/tools/lsp.ts +81 -59
  348. package/src/tools/mcp.ts +111 -68
  349. package/src/tools/native-view.d.ts +47 -0
  350. package/src/tools/native-view.ts +102 -1
  351. package/src/tools/output-meta.d.ts +11 -1
  352. package/src/tools/output-meta.ts +23 -3
  353. package/src/tools/read.ts +7 -5
  354. package/src/tools/renderer.d.ts +37 -2
  355. package/src/tools/renderer.ts +42 -2
  356. package/src/tools/streaming-output.d.ts +20 -1
  357. package/src/tools/streaming-output.ts +149 -50
  358. package/src/tools/task.d.ts +0 -4
  359. package/src/tools/task.ts +112 -59
  360. package/src/tools/todo.ts +119 -30
  361. package/src/tools/wait.ts +34 -24
  362. package/src/tools/web-search.ts +1 -1
  363. package/src/tools/write.d.ts +9 -2
  364. package/src/tools/write.ts +147 -95
  365. package/src/tools/yield.d.ts +22 -0
  366. package/src/tools/yield.ts +225 -0
  367. package/src/tui.d.ts +31 -0
  368. package/src/tui.ts +309 -160
  369. package/src/utils.d.ts +6 -0
  370. package/src/utils.ts +19 -0
  371. package/src/vim.d.ts +19 -5
  372. package/src/vim.ts +333 -18
  373. package/src/windows-input-mode.ts +8 -0
@@ -23,12 +23,11 @@ import type {
23
23
  import type { KeysApi, Model } from "@linxiraos/pi-ai";
24
24
  import { getOAuthProviders } from "@linxiraos/pi-ai/oauth";
25
25
  import { getSupportedEfforts } from "@linxiraos/pi-catalog/model-thinking";
26
- import { modelsAreEqual } from "@linxiraos/pi-catalog/models";
27
26
  import { providerEntry } from "@linxiraos/pi-catalog/compat/providers";
28
27
  import { MODEL_KINDS, modelKind, type ModelKind } from "@linxiraos/pi-catalog/types";
29
28
  import type { Component, TUI } from "../tui";
30
29
  import { extractPrintableText, matchesKey } from "../keys";
31
- import { fuzzyFilter } from "../fuzzy";
30
+ import { FuzzyCorpus } from "../fuzzy";
32
31
  import { formatKeyHint, formatKeyHints } from "../app-keybindings";
33
32
  import { boundKeys, editorKey, editorKeys } from "../chrome/keybinding-hints";
34
33
  import type { KeyName } from "../key-hint-format";
@@ -48,7 +47,7 @@ import type {
48
47
  ResolvedModelRoleValue,
49
48
  } from "./model-browser";
50
49
  import { AUTO_THINKING, type ConfiguredThinkingLevel, getConfiguredThinkingLevelMetadata } from "../thinking";
51
- import { thinkingLevelGlyph } from "../render/render-utils";
50
+ import { sanitizeDisplayWarning, thinkingLevelGlyph } from "../render/render-utils";
52
51
  import { theme } from "../theme/theme";
53
52
  import { matchesSelectCancel, matchesSelectDown, matchesSelectUp } from "../keybinding-matchers";
54
53
  import {
@@ -60,6 +59,7 @@ import {
60
59
  thinkingDotToken,
61
60
  type RoleAssignments,
62
61
  resolveRoleAssignments,
62
+ scopedRolePool,
63
63
  sortModelItems,
64
64
  } from "./model-browser";
65
65
  import {
@@ -170,9 +170,31 @@ export interface ModelHubCallbacks {
170
170
  onSwitchPreset?: (name: string) => void | Promise<void>;
171
171
  /** Persist a new quick-switch cycle order (the ctrl+p role cycle). */
172
172
  onCycleOrderChange?: (order: string[]) => void;
173
+ /**
174
+ * Persist typed text as `model`'s own compaction limit: a token base the
175
+ * policy scales (`400k`), a fixed trigger (`f400k`), a percentage (`80%`), or
176
+ * empty to reset. Rejected input returns `{ kind: "error" }`; input that
177
+ * needs acknowledgement (one that opens the extended window) returns
178
+ * `{ kind: "confirm" }` until called again with the same text and
179
+ * `confirmed`. Either keeps the field open.
180
+ */
181
+ onCompactionPointChange?: (
182
+ model: Model,
183
+ input: string,
184
+ confirmed: boolean,
185
+ ) => CompactionPointChangeResult | undefined;
186
+ /**
187
+ * One short line on where `model` would compact with the typed limit
188
+ * (`compacts at 340K · 85% of 400K base`), shown beside the field while
189
+ * typing; undefined for input that does not parse or fit.
190
+ */
191
+ previewCompactionPoint?: (model: Model, input: string) => string | undefined;
173
192
  onCancel: () => void;
174
193
  }
175
194
 
195
+ /** Why {@link ModelHubCallbacks.onCompactionPointChange} kept the field open. */
196
+ export type CompactionPointChangeResult = { kind: "error"; message: string } | { kind: "confirm"; message: string };
197
+
176
198
  export interface ModelHubOptions {
177
199
  /** Preselect this provider's sidebar entry (e.g. when reopening after /login). */
178
200
  initialProviderId?: string;
@@ -224,6 +246,20 @@ type StripState =
224
246
  kind: "name";
225
247
  purpose: "role" | "preset";
226
248
  input: Input;
249
+ }
250
+ | {
251
+ /**
252
+ * Footer text input setting `model`'s compaction limit; `error` is the last
253
+ * rejection, `confirm` the warning a second Enter on the same `value`
254
+ * accepts, `preview` the host's live line for the typed `value`.
255
+ */
256
+ kind: "name";
257
+ purpose: "compaction";
258
+ model: Model;
259
+ input: Input;
260
+ error?: string;
261
+ confirm?: { value: string; message: string };
262
+ preview?: { value: string; text: string | undefined };
227
263
  };
228
264
 
229
265
  /** A Roles-view command; keys and the picker's action bar both run {@link ModelHubComponent}'s `#runRolesAction`. */
@@ -236,6 +272,7 @@ type RolesAction =
236
272
  | "later"
237
273
  | "new"
238
274
  | "thinking"
275
+ | "compaction"
239
276
  | "save"
240
277
  | "nextPreset"
241
278
  | "prevPreset";
@@ -249,6 +286,7 @@ const ROLES_ACTION_KEYS: Record<string, RolesAction> = {
249
286
  "]": "later",
250
287
  n: "new",
251
288
  t: "thinking",
289
+ k: "compaction",
252
290
  s: "save",
253
291
  // Letter twins of ctrl+←/→, which macOS reserves for switching Spaces.
254
292
  p: "nextPreset",
@@ -286,6 +324,41 @@ function providerInitials(providerId: string): string {
286
324
 
287
325
  const PROVIDER_REFRESH_DEBOUNCE_MS = 120;
288
326
  const RECENT_LIMIT = 15;
327
+ /** Accepted compaction limit input, shown beside the field: a scaled base, a fixed trigger, or a percentage. */
328
+ const COMPACTION_INPUT_HINT = "400k base · f400k fixed · 80% · empty resets";
329
+
330
+ type CompactionStrip = Extract<StripState, { purpose: "compaction" }>;
331
+
332
+ /** Whether the compaction field still holds the text its pending warning was raised for. */
333
+ function compactionConfirmPending(strip: CompactionStrip): boolean {
334
+ return strip.confirm !== undefined && strip.confirm.value === strip.input.getValue();
335
+ }
336
+
337
+ /**
338
+ * The note beside the compaction field: the last error, a pending warning, the
339
+ * host's live preview of the typed limit, or (empty or unparsed input) the
340
+ * input syntax. Host text embeds model ids and config text, so it is
341
+ * sanitized for every render path (terminal footer, Tern picker and strip).
342
+ */
343
+ function compactionNotice(
344
+ strip: CompactionStrip,
345
+ preview: ((model: Model, input: string) => string | undefined) | undefined,
346
+ ): { text: string; style: "error" | "warning" | "dim" } {
347
+ if (strip.error) return { text: sanitizeDisplayWarning(strip.error), style: "error" };
348
+ // The footer hint and the primary action already say Enter accepts; the
349
+ // notice stays short so it fits beside the field.
350
+ if (strip.confirm && compactionConfirmPending(strip)) {
351
+ return { text: sanitizeDisplayWarning(strip.confirm.message), style: "warning" };
352
+ }
353
+ const value = strip.input.getValue();
354
+ if (preview && value.trim().length > 0) {
355
+ // Renders repeat per frame; ask the host once per typed value.
356
+ if (strip.preview?.value !== value) strip.preview = { value, text: preview(strip.model, value) };
357
+ if (strip.preview.text) return { text: sanitizeDisplayWarning(strip.preview.text), style: "dim" };
358
+ }
359
+ return { text: COMPACTION_INPUT_HINT, style: "dim" };
360
+ }
361
+
289
362
  const MODEL_KIND_TABS: ReadonlyArray<"all" | ModelKind> = ["all", ...MODEL_KINDS];
290
363
  const ROLE_TABS = ["all", "chat", "kind"] as const;
291
364
  type RoleTab = (typeof ROLE_TABS)[number];
@@ -334,7 +407,15 @@ export class ModelHubComponent implements Component {
334
407
  #roles: RoleAssignments = {};
335
408
  #availableItems: ModelBrowserItem[] = [];
336
409
  #recentItems: ModelBrowserItem[] = [];
410
+ /** Selectors of {@link #recentItems}, rebuilt with it, for search hit counts. */
411
+ #recentSelectors: ReadonlySet<string> = new Set();
337
412
  #candidateItems: ModelBrowserItem[] = [];
413
+ /** {@link #availableItems} when the candidates are that whole catalog (All scope, no role filter). */
414
+ #candidateCatalog: readonly ModelBrowserItem[] | undefined;
415
+ /** {@link #availableItems} when the browser's base items are that whole catalog; its query ranking then covers every count. */
416
+ #browserCatalog: readonly ModelBrowserItem[] | undefined;
417
+ /** Fuzzy index over {@link #availableItems}, for match counts while the browser holds a narrower scope. */
418
+ #catalogCorpus: { items: readonly ModelBrowserItem[]; corpus: FuzzyCorpus<ModelBrowserItem> } | undefined;
338
419
  #modelKindTab: "all" | ModelKind = "all";
339
420
  #roleTab: RoleTab = "all";
340
421
  #configError: string | undefined;
@@ -428,6 +509,7 @@ export class ModelHubComponent implements Component {
428
509
  row: RolesRow | undefined;
429
510
  roles: RoleAssignments;
430
511
  rows: readonly RolesRow[];
512
+ revision: number;
431
513
  children: readonly NativeChild[];
432
514
  }
433
515
  | undefined;
@@ -518,22 +600,6 @@ export class ModelHubComponent implements Component {
518
600
  return this.#settings.knownRoleIds.filter(role => !this.#settings.getRoleInfo(role).hidden);
519
601
  }
520
602
 
521
- /**
522
- * Models a `--models`/`enabledModels` scope exposes. The scope only resolves
523
- * chat models (it feeds Ctrl+P cycling), so available non-chat runners
524
- * (judge, search, image, …) join from the registry — runtime role
525
- * resolution ignores the scope for them as well.
526
- */
527
- #scopedPool(): Model[] {
528
- const pool = this.#scopedModels.map(scoped => scoped.model);
529
- for (const model of this.#registry.getAvailable("all")) {
530
- if (modelKind(model) === "chat") continue;
531
- if (this.#scopedModels.some(scoped => modelsAreEqual(scoped.model, model))) continue;
532
- pool.push(model);
533
- }
534
- return pool;
535
- }
536
-
537
603
  /** Resolve every known role: configured values first, auto-selection for the rest. */
538
604
  #reloadRoles(autoCandidates: ReadonlyArray<Model>): void {
539
605
  const allModels = this.#scopedModels.length > 0 ? autoCandidates : this.#registry.getAll("all");
@@ -552,7 +618,10 @@ export class ModelHubComponent implements Component {
552
618
  if (this.#scopedModels.length > 0) {
553
619
  this.#configError = undefined;
554
620
  try {
555
- allModels = this.#scopedPool();
621
+ allModels = scopedRolePool(
622
+ this.#scopedModels.map(scoped => scoped.model),
623
+ this.#registry,
624
+ );
556
625
  } catch (error) {
557
626
  this.#configError = error instanceof Error ? error.message : String(error);
558
627
  allModels = this.#scopedModels.map(scoped => scoped.model);
@@ -587,6 +656,7 @@ export class ModelHubComponent implements Component {
587
656
  if (item) this.#recentItems.push(item);
588
657
  if (this.#recentItems.length >= RECENT_LIMIT) break;
589
658
  }
659
+ this.#recentSelectors = new Set(this.#recentItems.map(item => item.selector));
590
660
 
591
661
  this.#buildSidebar(allModels, availableModels);
592
662
  this.#restoreSidebarAnchor(anchor);
@@ -823,11 +893,13 @@ export class ModelHubComponent implements Component {
823
893
  ? items.filter(item => this.#settings.getRoleInfo(assigning.role).accepts(item.model))
824
894
  : items;
825
895
  this.#candidateItems = [...scoped];
896
+ this.#candidateCatalog = scoped === this.#availableItems ? this.#availableItems : undefined;
826
897
  this.#applyModelKind();
827
898
  }
828
899
 
829
900
  #applyModelKind(): void {
830
901
  const kind = this.#modelKindTab;
902
+ this.#browserCatalog = kind === "all" ? this.#candidateCatalog : undefined;
831
903
  this.#browser.setItems(
832
904
  kind === "all"
833
905
  ? [...this.#candidateItems]
@@ -936,16 +1008,14 @@ export class ModelHubComponent implements Component {
936
1008
  this.#composeEntries();
937
1009
  return;
938
1010
  }
939
- const matches = fuzzyFilter(this.#availableItems, query, modelSearchText);
1011
+ const matches = this.#catalogMatches(query);
940
1012
  const counts = new Map<string, number>();
1013
+ let recentCount = 0;
941
1014
  for (const item of matches) {
942
1015
  counts.set(item.provider, (counts.get(item.provider) ?? 0) + 1);
1016
+ if (this.#recentSelectors.has(item.selector)) recentCount++;
943
1017
  }
944
- const recentSelectors = new Set(this.#recentItems.map(item => item.selector));
945
- this.#recentSearchCount = matches.reduce(
946
- (total, item) => total + (recentSelectors.has(item.selector) ? 1 : 0),
947
- 0,
948
- );
1018
+ this.#recentSearchCount = recentCount;
949
1019
  this.#searchTotal = matches.length;
950
1020
  this.#searchCounts = counts;
951
1021
  this.#composeEntries();
@@ -959,6 +1029,24 @@ export class ModelHubComponent implements Component {
959
1029
  }
960
1030
  }
961
1031
 
1032
+ /**
1033
+ * Catalog items matching a non-blank `query`. When the browser holds the
1034
+ * whole catalog its ranking for this query already ran, so reuse it;
1035
+ * otherwise scan a catalog index kept across keystrokes.
1036
+ */
1037
+ #catalogMatches(query: string): readonly ModelBrowserItem[] {
1038
+ if (this.#browserCatalog === this.#availableItems && this.#browser.query === query) {
1039
+ const ranked = this.#browser.queryMatches;
1040
+ if (ranked) return ranked;
1041
+ }
1042
+ let cached = this.#catalogCorpus;
1043
+ if (cached?.items !== this.#availableItems) {
1044
+ cached = { items: this.#availableItems, corpus: new FuzzyCorpus(this.#availableItems, modelSearchText) };
1045
+ this.#catalogCorpus = cached;
1046
+ }
1047
+ return cached.corpus.rank(query).map(result => result.item);
1048
+ }
1049
+
962
1050
  /**
963
1051
  * Entries the scope hop skips: separators always; while searching, also
964
1052
  * the Roles view (not a model scope), an empty Recent, locked providers,
@@ -1149,7 +1237,13 @@ export class ModelHubComponent implements Component {
1149
1237
  #roleForScope(role: string, scope: ModelRoleSelectionScope): ResolvedModelRoleValue {
1150
1238
  const roleValue =
1151
1239
  scope === "project" ? this.#settings.getProjectModelRole(role) : this.#settings.getGlobalModelRole(role);
1152
- const allModels = this.#scopedModels.length > 0 ? this.#scopedPool() : this.#registry.getAll("all");
1240
+ const allModels =
1241
+ this.#scopedModels.length > 0
1242
+ ? scopedRolePool(
1243
+ this.#scopedModels.map(scoped => scoped.model),
1244
+ this.#registry,
1245
+ )
1246
+ : this.#registry.getAll("all");
1153
1247
  const roleLookup: ModelRoleLookup = {
1154
1248
  getModelRole: scopedRole =>
1155
1249
  scope === "project"
@@ -1731,10 +1825,46 @@ export class ModelHubComponent implements Component {
1731
1825
  this.#strip = { kind: "name", purpose, input: new Input() };
1732
1826
  }
1733
1827
 
1734
- /** Validate and commit the name strip: a new role jumps into assigning it, a preset name saves it. */
1828
+ /** The model a Roles row resolves to (assigned role or resolvable fallback), whose compaction point `k` edits. */
1829
+ #roleRowModel(): Model | undefined {
1830
+ const row = this.#rolesRows[this.#roleIndex];
1831
+ if (row?.kind === "role") return this.#roles[row.role]?.model;
1832
+ if (row?.kind === "fallback") return this.#resolveFallbackEntry(row.role, row.chainIndex)?.item.model;
1833
+ return undefined;
1834
+ }
1835
+
1836
+ /** Open the footer input for the Roles row's model compaction point, prefilled with its own entry. */
1837
+ #openCompactionStrip(): void {
1838
+ if (!this.#callbacks.onCompactionPointChange) return;
1839
+ const model = this.#roleRowModel();
1840
+ if (!model) return;
1841
+ const input = new Input();
1842
+ input.setValue(this.#settings.compactionPointFor?.(model)?.draft ?? "");
1843
+ this.#strip = { kind: "name", purpose: "compaction", model, input };
1844
+ }
1845
+
1846
+ /** Validate and commit the name strip: a new role, a preset name, or a compaction point. */
1735
1847
  #submitNameStrip(): void {
1736
1848
  const strip = this.#strip;
1737
1849
  if (strip?.kind !== "name") return;
1850
+ if (strip.purpose === "compaction") {
1851
+ const value = strip.input.getValue();
1852
+ const result = this.#callbacks.onCompactionPointChange?.(strip.model, value, compactionConfirmPending(strip));
1853
+ if (result?.kind === "error") {
1854
+ strip.error = result.message;
1855
+ strip.confirm = undefined;
1856
+ return;
1857
+ }
1858
+ if (result?.kind === "confirm") {
1859
+ strip.error = undefined;
1860
+ strip.confirm = { value, message: result.message };
1861
+ return;
1862
+ }
1863
+ this.#strip = null;
1864
+ this.#frame.chipRanges = [];
1865
+ this.#refreshAfterMutation();
1866
+ return;
1867
+ }
1738
1868
  if (strip.purpose === "preset") {
1739
1869
  this.#submitPresetName();
1740
1870
  return;
@@ -1916,7 +2046,13 @@ export class ModelHubComponent implements Component {
1916
2046
 
1917
2047
  if (rolesView) {
1918
2048
  const printable = extractPrintableText(data);
1919
- if (this.#focus === "scope" && printable !== undefined && printable.trim().length > 0) {
2049
+ // Typing searches the catalog from the sidebar, and from the rows
2050
+ // while a query is live (the picker shows its search field only
2051
+ // then); otherwise the rows' letter commands own printable keys.
2052
+ const typed = printable !== undefined && printable.trim().length > 0;
2053
+ const editsQuery =
2054
+ this.#browser.query.length > 0 && (printable !== undefined || matchesKey(data, "backspace"));
2055
+ if ((this.#focus === "scope" && typed) || editsQuery) {
1920
2056
  this.#setActiveEntry("all");
1921
2057
  this.#focus = "list";
1922
2058
  this.#browser.handleInput(data);
@@ -1993,6 +2129,7 @@ export class ModelHubComponent implements Component {
1993
2129
  return;
1994
2130
  }
1995
2131
  strip.input.handleInput(data);
2132
+ if (strip.purpose === "compaction") strip.error = undefined;
1996
2133
  return;
1997
2134
  }
1998
2135
  if (moveStripSelection(strip, data)) return;
@@ -2156,6 +2293,9 @@ export class ModelHubComponent implements Component {
2156
2293
  case "prevPreset":
2157
2294
  this.#switchPreset(action === "nextPreset" ? 1 : -1);
2158
2295
  return;
2296
+ case "compaction":
2297
+ this.#openCompactionStrip();
2298
+ return;
2159
2299
  case "thinking":
2160
2300
  if (role) {
2161
2301
  const target = this.#roleThinkingTarget(role);
@@ -2355,9 +2495,14 @@ export class ModelHubComponent implements Component {
2355
2495
  Math.max(0, active),
2356
2496
  );
2357
2497
  const { names, active: activePreset } = this.#presets();
2498
+ // p/P type into a live query instead of switching presets.
2499
+ const presetKeys =
2500
+ this.#browser.query.length > 0
2501
+ ? formatKeyHints(["ctrl+left", "ctrl+right"])
2502
+ : `${formatKeyHints(["ctrl+left", "ctrl+right"])} · ${formatKeyHints(["p", "shift+p"])}`;
2358
2503
  const preset =
2359
2504
  names.length > 0
2360
- ? ` ${theme.fg("dim", "Preset:")} ${activePreset ? theme.fg("accent", activePreset) : theme.fg("muted", "custom")} ${theme.fg("dim", `${formatKeyHints(["ctrl+left", "ctrl+right"])} · ${formatKeyHints(["p", "shift+p"])}`)}`
2505
+ ? ` ${theme.fg("dim", "Preset:")} ${activePreset ? theme.fg("accent", activePreset) : theme.fg("muted", "custom")} ${theme.fg("dim", presetKeys)}`
2361
2506
  : "";
2362
2507
  return truncateToWidth(
2363
2508
  ` ${theme.fg("dim", "Roles:")} ${track} ${theme.fg("dim", formatKeyHints(["alt+left", "alt+right"]))}${preset}`,
@@ -2631,6 +2776,9 @@ export class ModelHubComponent implements Component {
2631
2776
  const strip = this.#strip;
2632
2777
  if (strip) {
2633
2778
  if (strip.kind === "name") {
2779
+ if (strip.purpose === "compaction") {
2780
+ return `${enter} ${compactionConfirmPending(strip) ? "accept" : "set"} compaction point · ${cancel} cancel`;
2781
+ }
2634
2782
  if (strip.purpose === "preset") {
2635
2783
  return `${enter} save preset · ${cancel} cancel`;
2636
2784
  }
@@ -2661,14 +2809,22 @@ export class ModelHubComponent implements Component {
2661
2809
  this.#presets().names.length > 0 ? ` · ${formatKeyHints(["ctrl+left", "ctrl+right"])} preset` : "";
2662
2810
  return `${upDown} providers · ${enterRight} roles · ${altLeftRight} tabs${presets} · ${cancel} close`;
2663
2811
  }
2812
+ // A live query takes printable keys and backspace, so the letter commands rest.
2813
+ if (this.#browser.query.length > 0) {
2814
+ return `${upDown} rows · ${enter} pick · type to search · ${left} providers · ${cancel} close`;
2815
+ }
2664
2816
  const row = this.#rolesRows[this.#roleIndex];
2817
+ const compaction =
2818
+ this.#callbacks.onCompactionPointChange && this.#roleRowModel()
2819
+ ? ` · ${formatKeyHint("k")} compaction limit`
2820
+ : "";
2665
2821
  if (row?.kind === "fallback") {
2666
2822
  // Advertise `t` only when the entry resolves: wildcards always
2667
2823
  // inherit and unknown models have no ladder to offer, so the
2668
2824
  // action would be inert there.
2669
2825
  const editable = this.#resolveFallbackEntry(row.role, row.chainIndex) !== undefined;
2670
2826
  const thinking = editable ? ` · ${formatKeyHint("t")} thinking` : "";
2671
- return `${upDown} rows · ${enter} replace · ${formatKeyHint("f")} add another · ${formatKeyHint("x")} remove${thinking} · [/] reorder · ${left} providers`;
2827
+ return `${upDown} rows · ${enter} replace · ${formatKeyHint("f")} add another · ${formatKeyHint("x")} remove${thinking}${compaction} · [/] reorder · ${left} providers`;
2672
2828
  }
2673
2829
  if (row?.kind === "chainKey") {
2674
2830
  return `${upDown} rows · ${formatKeyHints(["enter", "f"])} add fallback · ${formatKeyHint("x")} clear chain · ${left} providers`;
@@ -2682,7 +2838,7 @@ export class ModelHubComponent implements Component {
2682
2838
  const thinking = editable ? ` · ${formatKeyHint("t")} thinking` : "";
2683
2839
  const savePreset = this.#callbacks.onSavePreset ? ` · ${formatKeyHint("s")} save preset` : "";
2684
2840
  const switchPreset = this.#presets().names.length > 0 ? ` · ${formatKeyHints(["p", "shift+p"])} preset` : "";
2685
- return `${upDown} rows · ${enter} pick · ${formatKeyHint("f")} fallback · ${formatKeyHint("x")} clear${thinking} · ${formatKeyHint("c")} cycle · [/] reorder · ${formatKeyHint("n")} new${savePreset}${switchPreset}`;
2841
+ return `${upDown} rows · ${enter} pick · ${formatKeyHint("f")} fallback · ${formatKeyHint("x")} clear${thinking}${compaction} · ${formatKeyHint("c")} cycle · [/] reorder · ${formatKeyHint("n")} new${savePreset}${switchPreset}`;
2686
2842
  }
2687
2843
  if (entry.kind === "provider" && entry.locked) {
2688
2844
  return entry.oauth
@@ -2707,12 +2863,26 @@ export class ModelHubComponent implements Component {
2707
2863
 
2708
2864
  #renderStrip(width: number, strip: StripState): string {
2709
2865
  if (strip.kind === "name") {
2710
- const preset = strip.purpose === "preset";
2711
- const labelText = preset ? "Preset name:" : "New role name:";
2866
+ const labelText =
2867
+ strip.purpose === "compaction"
2868
+ ? `${strip.model.id} limit:`
2869
+ : strip.purpose === "preset"
2870
+ ? "Preset name:"
2871
+ : "New role name:";
2712
2872
  const label = theme.fg("accent", labelText);
2713
- const inputWidth = Math.max(8, Math.min(32, width - visibleWidth(labelText) - 24));
2873
+ // A compaction point is at most a few characters (`1500k`, `12.5%`); a
2874
+ // narrow field leaves the row for its notice.
2875
+ const maxInputWidth = strip.purpose === "compaction" ? 10 : 32;
2876
+ const inputWidth = Math.max(8, Math.min(maxInputWidth, width - visibleWidth(labelText) - 24));
2714
2877
  const inputLine = strip.input.render(inputWidth)[0] ?? "";
2715
- return truncateToWidth(`${label} ${inputLine} ${theme.fg("dim", "(letters, digits, - and _)")}`, width);
2878
+ let hint: string;
2879
+ if (strip.purpose === "compaction") {
2880
+ const notice = compactionNotice(strip, this.#callbacks.previewCompactionPoint);
2881
+ hint = theme.fg(notice.style, notice.text);
2882
+ } else {
2883
+ hint = theme.fg("dim", "(letters, digits, - and _)");
2884
+ }
2885
+ return truncateToWidth(`${label} ${inputLine} ${hint}`, width);
2716
2886
  }
2717
2887
 
2718
2888
  const prefix =
@@ -3134,7 +3304,9 @@ export class ModelHubComponent implements Component {
3134
3304
  size: "lg",
3135
3305
  layout: "rows",
3136
3306
  preview: "side",
3137
- query: this.#browser.query,
3307
+ // The Roles rows' letter commands own printable keys until a query
3308
+ // is live, so the sheet offers no search field to type into.
3309
+ query: rolesView && this.#browser.query.length === 0 ? null : this.#browser.query,
3138
3310
  cursor: this.#browser.cursor,
3139
3311
  placeholder: "Search models…",
3140
3312
  scopes: this.#pickerScopes(),
@@ -3363,12 +3535,19 @@ export class ModelHubComponent implements Component {
3363
3535
  if (strip) {
3364
3536
  const apply =
3365
3537
  strip.kind === "name"
3366
- ? pickerAction(
3367
- strip.purpose === "preset" ? "presetName" : "roleName",
3368
- strip.purpose === "preset" ? "Save preset" : "Create role",
3369
- "enter",
3370
- { primary: true },
3371
- )
3538
+ ? strip.purpose === "compaction"
3539
+ ? pickerAction(
3540
+ "compactionPoint",
3541
+ compactionConfirmPending(strip) ? "Accept compaction point" : "Set compaction point",
3542
+ "enter",
3543
+ { primary: true },
3544
+ )
3545
+ : pickerAction(
3546
+ strip.purpose === "preset" ? "presetName" : "roleName",
3547
+ strip.purpose === "preset" ? "Save preset" : "Create role",
3548
+ "enter",
3549
+ { primary: true },
3550
+ )
3372
3551
  : pickerAction(
3373
3552
  "stripApply",
3374
3553
  strip.kind === "thinking" ? "Apply" : strip.kind === "scope" ? "Save to scope" : "Assign / clear",
@@ -3390,9 +3569,20 @@ export class ModelHubComponent implements Component {
3390
3569
  }
3391
3570
  if (rolesView) {
3392
3571
  const row = this.#rolesRows[this.#roleIndex];
3572
+ // A live query takes the letter keys: those buttons keep working but lose their keycaps.
3573
+ const searching = this.#browser.query.length > 0;
3393
3574
  const roleAction = (action: RolesAction, label: string, key: string, primary = false) =>
3394
- pickerAction(`roles:${action}`, label, key, primary ? { primary: true } : undefined);
3575
+ pickerAction(
3576
+ `roles:${action}`,
3577
+ label,
3578
+ searching && key !== "enter" ? undefined : key,
3579
+ primary ? { primary: true } : undefined,
3580
+ );
3395
3581
  const actions: (TspPickerAction | undefined)[] = [];
3582
+ const compaction =
3583
+ this.#callbacks.onCompactionPointChange && this.#roleRowModel()
3584
+ ? roleAction("compaction", "Compaction limit", "k")
3585
+ : undefined;
3396
3586
  switch (row?.kind) {
3397
3587
  case "role": {
3398
3588
  const assigned = this.#roles[row.role];
@@ -3401,6 +3591,7 @@ export class ModelHubComponent implements Component {
3401
3591
  roleAction("fallback", "Add fallback", "f"),
3402
3592
  assigned && !assigned.autoSelected ? roleAction("clear", "Clear", "x") : undefined,
3403
3593
  this.#roleThinkingTarget(row.role) ? roleAction("thinking", "Thinking", "t") : undefined,
3594
+ compaction,
3404
3595
  roleAction("cycle", this.#cycleOrder().includes(row.role) ? "Leave cycle" : "Add to cycle", "c"),
3405
3596
  roleAction("new", "New role", "n"),
3406
3597
  this.#callbacks.onSavePreset ? roleAction("save", "Save preset", "s") : undefined,
@@ -3416,6 +3607,7 @@ export class ModelHubComponent implements Component {
3416
3607
  this.#resolveFallbackEntry(row.role, row.chainIndex)
3417
3608
  ? roleAction("thinking", "Thinking", "t")
3418
3609
  : undefined,
3610
+ compaction,
3419
3611
  roleAction("earlier", "Earlier", "["),
3420
3612
  roleAction("later", "Later", "]"),
3421
3613
  );
@@ -3452,6 +3644,19 @@ export class ModelHubComponent implements Component {
3452
3644
  /** The open strip as the picker's chip strip (role assignment, save scope, thinking level, new role name). */
3453
3645
  #pickerStrip(strip: StripState): NonNullable<TspPickerProps["strip"]> {
3454
3646
  if (strip.kind === "name") {
3647
+ if (strip.purpose === "compaction") {
3648
+ const notice = compactionNotice(strip, this.#callbacks.previewCompactionPoint);
3649
+ return {
3650
+ label: [
3651
+ span(strip.model.id, "mono"),
3652
+ span(" limit ", "muted"),
3653
+ span(strip.input.getValue(), "mono"),
3654
+ span("▏", "accent"),
3655
+ span(` ${notice.text}`, notice.style),
3656
+ ],
3657
+ items: [],
3658
+ };
3659
+ }
3455
3660
  return {
3456
3661
  label: [
3457
3662
  span(strip.purpose === "preset" ? "Preset name " : "New role name ", "muted"),
@@ -3591,7 +3796,14 @@ export class ModelHubComponent implements Component {
3591
3796
  /** The selected Roles-view row's preview: its model's facts and its fallback chain. */
3592
3797
  #rolePreview(row: RolesRow | undefined): readonly NativeChild[] {
3593
3798
  const memo = this.#pickerRolePreview;
3594
- if (memo !== undefined && memo.row === row && memo.roles === this.#roles && memo.rows === this.#rolesRows) {
3799
+ const revision = this.#settings.revision;
3800
+ if (
3801
+ memo !== undefined &&
3802
+ memo.row === row &&
3803
+ memo.roles === this.#roles &&
3804
+ memo.rows === this.#rolesRows &&
3805
+ memo.revision === revision
3806
+ ) {
3595
3807
  return memo.children;
3596
3808
  }
3597
3809
  const children: NativeChild[] = [];
@@ -3665,7 +3877,7 @@ export class ModelHubComponent implements Component {
3665
3877
  );
3666
3878
  break;
3667
3879
  }
3668
- this.#pickerRolePreview = { row, roles: this.#roles, rows: this.#rolesRows, children };
3880
+ this.#pickerRolePreview = { row, roles: this.#roles, rows: this.#rolesRows, revision, children };
3669
3881
  return children;
3670
3882
  }
3671
3883
 
@@ -3765,6 +3977,9 @@ export class ModelHubComponent implements Component {
3765
3977
  case "presetName":
3766
3978
  this.#submitPresetName();
3767
3979
  return;
3980
+ case "compactionPoint":
3981
+ this.#submitNameStrip();
3982
+ return;
3768
3983
  case "cancel":
3769
3984
  if (this.#strip) this.#closeStrip();
3770
3985
  return;
@@ -3815,6 +4030,19 @@ export class ModelHubComponent implements Component {
3815
4030
  const strip = this.#strip;
3816
4031
  if (!strip) return undefined;
3817
4032
  if (strip.kind === "name") {
4033
+ if (strip.purpose === "compaction") {
4034
+ const notice = compactionNotice(strip, this.#callbacks.previewCompactionPoint);
4035
+ return node(
4036
+ "row",
4037
+ { gap: "sm", align: "center" },
4038
+ [
4039
+ text([span(`${strip.model.id} limit:`, "accent")]),
4040
+ col([strip.input], { grow: 1 }),
4041
+ text([span(notice.text, notice.style)], { wrap: "word" }),
4042
+ ],
4043
+ "compactionPoint",
4044
+ );
4045
+ }
3818
4046
  const preset = strip.purpose === "preset";
3819
4047
  return node(
3820
4048
  "row",
@@ -3868,9 +4096,17 @@ export class ModelHubComponent implements Component {
3868
4096
  if (strip) {
3869
4097
  switch (strip.kind) {
3870
4098
  case "name":
3871
- return strip.purpose === "preset"
3872
- ? [keys("save preset", "enter"), cancel("cancel")]
3873
- : [keys("create + pick model", "enter"), cancel("cancel")];
4099
+ return strip.purpose === "compaction"
4100
+ ? [
4101
+ keys(
4102
+ compactionConfirmPending(strip) ? "accept compaction point" : "set compaction point",
4103
+ "enter",
4104
+ ),
4105
+ cancel("cancel"),
4106
+ ]
4107
+ : strip.purpose === "preset"
4108
+ ? [keys("save preset", "enter"), cancel("cancel")]
4109
+ : [keys("create + pick model", "enter"), cancel("cancel")];
3874
4110
  case "role":
3875
4111
  return [keys("choose", "left", "right"), keys("assign/clear", "enter"), cancel("cancel")];
3876
4112
  case "scope":
@@ -3903,7 +4139,20 @@ export class ModelHubComponent implements Component {
3903
4139
  cancel("close"),
3904
4140
  ];
3905
4141
  }
4142
+ // A live query takes printable keys and backspace, so the letter commands rest.
4143
+ if (this.#browser.query.length > 0) {
4144
+ return [
4145
+ upDown("rows"),
4146
+ keys("pick", "enter"),
4147
+ search,
4148
+ keys("providers", "left"),
4149
+ presetHint,
4150
+ cancel("close"),
4151
+ ];
4152
+ }
3906
4153
  const row = this.#rolesRows[this.#roleIndex];
4154
+ const compaction =
4155
+ this.#callbacks.onCompactionPointChange && this.#roleRowModel() ? keys("compaction limit", "k") : undefined;
3907
4156
  if (row?.kind === "fallback") {
3908
4157
  // Advertise `t` only where a strip would open, as the ANSI footer does.
3909
4158
  const editable = this.#resolveFallbackEntry(row.role, row.chainIndex) !== undefined;
@@ -3913,6 +4162,7 @@ export class ModelHubComponent implements Component {
3913
4162
  keys("add another", "f"),
3914
4163
  keys("remove", "x"),
3915
4164
  editable ? keys("thinking", "t") : undefined,
4165
+ compaction,
3916
4166
  reorder,
3917
4167
  keys("providers", "left"),
3918
4168
  ];
@@ -3935,6 +4185,7 @@ export class ModelHubComponent implements Component {
3935
4185
  keys("fallback", "f"),
3936
4186
  keys("clear", "x"),
3937
4187
  editable ? keys("thinking", "t") : undefined,
4188
+ compaction,
3938
4189
  keys("cycle", "c"),
3939
4190
  reorder,
3940
4191
  keys("new", "n"),