@linxiraos/pi-tui 1.1.26 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (423) hide show
  1. package/CHANGELOG.md +51 -491
  2. package/README.md +13 -7
  3. package/dist/types/active-terminal.d.ts +30 -0
  4. package/dist/types/app-keybindings.d.ts +7 -2
  5. package/dist/types/apps/cleanse-board.d.ts +1 -1
  6. package/dist/types/apps/debug/log-viewer.d.ts +4 -0
  7. package/dist/types/apps/debug/raw-sse-buffer.d.ts +2 -0
  8. package/dist/types/apps/git/git-tui.d.ts +5 -4
  9. package/dist/types/apps/git/help.d.ts +3 -1
  10. package/dist/types/apps/if-bench-board.d.ts +2 -2
  11. package/dist/types/apps/setup-model-picker.d.ts +1 -1
  12. package/dist/types/autocomplete.d.ts +5 -2
  13. package/dist/types/charts/chart-plan.d.ts +674 -0
  14. package/dist/types/charts/chart-svg.d.ts +59 -0
  15. package/dist/types/charts/table-data.d.ts +175 -0
  16. package/dist/types/chat/assistant-message.d.ts +11 -1
  17. package/dist/types/chat/chat-transcript-builder.d.ts +3 -0
  18. package/dist/types/chat/compaction-summary-message.d.ts +1 -0
  19. package/dist/types/chat/execution-shared.d.ts +6 -5
  20. package/dist/types/chat/fence-figure.d.ts +42 -0
  21. package/dist/types/chat/figure-markdown.d.ts +42 -0
  22. package/dist/types/chat/messages.d.ts +1 -1
  23. package/dist/types/chat/svg-figure.d.ts +33 -0
  24. package/dist/types/chat/svg-source.d.ts +49 -0
  25. package/dist/types/chat/table-chart.d.ts +89 -0
  26. package/dist/types/chat/tool-execution.d.ts +2 -0
  27. package/dist/types/chat/user-message.d.ts +17 -4
  28. package/dist/types/chrome/shared.d.ts +9 -0
  29. package/dist/types/chrome/status-notice.d.ts +15 -3
  30. package/dist/types/chrome/transcript-container.d.ts +6 -0
  31. package/dist/types/components/box.d.ts +1 -0
  32. package/dist/types/components/disclosure.d.ts +1 -0
  33. package/dist/types/components/editor.d.ts +23 -5
  34. package/dist/types/components/form.d.ts +1 -0
  35. package/dist/types/components/image.d.ts +26 -44
  36. package/dist/types/components/input.d.ts +2 -1
  37. package/dist/types/components/key-value-list.d.ts +1 -0
  38. package/dist/types/components/layout/row.d.ts +1 -0
  39. package/dist/types/components/layout/stack.d.ts +1 -0
  40. package/dist/types/components/loader.d.ts +5 -6
  41. package/dist/types/components/markdown.d.ts +47 -5
  42. package/dist/types/components/scroll-view.d.ts +1 -0
  43. package/dist/types/components/section.d.ts +1 -0
  44. package/dist/types/components/table.d.ts +1 -0
  45. package/dist/types/components/text.d.ts +1 -0
  46. package/dist/types/components/tree-view.d.ts +7 -0
  47. package/dist/types/components/truncated-text.d.ts +1 -0
  48. package/dist/types/fuzzy.d.ts +29 -1
  49. package/dist/types/key-hint-format.d.ts +9 -1
  50. package/dist/types/keybinding-matchers.d.ts +3 -0
  51. package/dist/types/keybindings.d.ts +1 -1
  52. package/dist/types/kitty-graphics.d.ts +2 -2
  53. package/dist/types/loop-watchdog.d.ts +8 -7
  54. package/dist/types/multiplexers/cmux.d.ts +16 -0
  55. package/dist/types/multiplexers/herdr.d.ts +26 -0
  56. package/dist/types/multiplexers/notify.d.ts +13 -0
  57. package/dist/types/multiplexers/orca.d.ts +12 -0
  58. package/dist/types/multiplexers/screen.d.ts +9 -0
  59. package/dist/types/multiplexers/tmux.d.ts +29 -0
  60. package/dist/types/multiplexers/types.d.ts +100 -0
  61. package/dist/types/multiplexers/wmux.d.ts +12 -0
  62. package/dist/types/multiplexers/zellij.d.ts +12 -0
  63. package/dist/types/native/apply.d.ts +4 -3
  64. package/dist/types/native/backend.d.ts +5 -4
  65. package/dist/types/native/blobs.d.ts +7 -3
  66. package/dist/types/native/encode.d.ts +8 -6
  67. package/dist/types/native/node.d.ts +36 -5
  68. package/dist/types/native/overlay.d.ts +8 -3
  69. package/dist/types/native/settle.d.ts +4 -3
  70. package/dist/types/overlays/agent-hub-projection.d.ts +6 -5
  71. package/dist/types/overlays/agent-hub-types.d.ts +9 -0
  72. package/dist/types/overlays/agent-hub.d.ts +3 -0
  73. package/dist/types/overlays/agent-transcript-viewer.d.ts +12 -2
  74. package/dist/types/overlays/agents-hub.d.ts +9 -0
  75. package/dist/types/overlays/annotation-overlay.d.ts +19 -2
  76. package/dist/types/overlays/annotation-types.d.ts +2 -0
  77. package/dist/types/overlays/ask-dialog.d.ts +5 -2
  78. package/dist/types/overlays/btw-history-panel.d.ts +13 -1
  79. package/dist/types/overlays/copy-selector.d.ts +1 -0
  80. package/dist/types/overlays/history-search.d.ts +10 -0
  81. package/dist/types/overlays/hook-selector.d.ts +9 -4
  82. package/dist/types/overlays/hub-frame.d.ts +4 -1
  83. package/dist/types/overlays/jobs-panel.d.ts +1 -7
  84. package/dist/types/overlays/model-browser.d.ts +54 -7
  85. package/dist/types/overlays/model-hub.d.ts +37 -0
  86. package/dist/types/overlays/oauth-selector.d.ts +0 -1
  87. package/dist/types/overlays/plan-review-overlay.d.ts +3 -2
  88. package/dist/types/overlays/report-panel.d.ts +68 -0
  89. package/dist/types/overlays/rewind-selector.d.ts +1 -0
  90. package/dist/types/overlays/session-observer-registry.d.ts +2 -0
  91. package/dist/types/overlays/session-selector.d.ts +6 -2
  92. package/dist/types/overlays/settings-defs.d.ts +1 -0
  93. package/dist/types/overlays/thinking-selector.d.ts +2 -2
  94. package/dist/types/overlays/usage-dashboard.d.ts +12 -8
  95. package/dist/types/overlays/usage-display.d.ts +3 -1
  96. package/dist/types/prompt/composer-cache.d.ts +0 -16
  97. package/dist/types/prompt/composer.d.ts +18 -3
  98. package/dist/types/prompt/custom-editor.d.ts +25 -13
  99. package/dist/types/prompt/image-source.d.ts +2 -1
  100. package/dist/types/prompt/interactive-context-helpers.d.ts +5 -1
  101. package/dist/types/prompt/model-mention-autocomplete.d.ts +1 -1
  102. package/dist/types/prompt/welcome.d.ts +7 -2
  103. package/dist/types/render/composite.d.ts +2 -0
  104. package/dist/types/render/output-pane.d.ts +1 -0
  105. package/dist/types/render/render-utils.d.ts +1 -0
  106. package/dist/types/render/tool-card.d.ts +6 -0
  107. package/dist/types/render/width-aware-text.d.ts +2 -0
  108. package/dist/types/setup/scenes/types.d.ts +1 -0
  109. package/dist/types/space-hold.d.ts +40 -25
  110. package/dist/types/status-line/component.d.ts +3 -0
  111. package/dist/types/status-line/context-usage.d.ts +7 -5
  112. package/dist/types/status-line/host.d.ts +4 -1
  113. package/dist/types/status-line/segments.d.ts +2 -0
  114. package/dist/types/status-line/types.d.ts +14 -3
  115. package/dist/types/terminal-capabilities.d.ts +41 -36
  116. package/dist/types/terminal-multiplexer.d.ts +127 -14
  117. package/dist/types/terminal.d.ts +23 -16
  118. package/dist/types/theme/active-symbols.d.ts +1 -1
  119. package/dist/types/theme/schema.d.ts +11 -2
  120. package/dist/types/theme/symbols.d.ts +1 -1
  121. package/dist/types/theme/theme-class.d.ts +7 -0
  122. package/dist/types/theme/theme.d.ts +1 -1
  123. package/dist/types/theme/tui-adapters.d.ts +3 -0
  124. package/dist/types/thinking.d.ts +1 -1
  125. package/dist/types/tmux.d.ts +11 -4
  126. package/dist/types/tools/bash.d.ts +7 -2
  127. package/dist/types/tools/daemon.d.ts +1 -1
  128. package/dist/types/tools/eval.d.ts +22 -3
  129. package/dist/types/tools/glob.d.ts +4 -0
  130. package/dist/types/tools/native-view.d.ts +47 -0
  131. package/dist/types/tools/output-meta.d.ts +11 -1
  132. package/dist/types/tools/renderer.d.ts +37 -2
  133. package/dist/types/tools/streaming-output.d.ts +20 -1
  134. package/dist/types/tools/task.d.ts +2 -3
  135. package/dist/types/tools/write.d.ts +9 -2
  136. package/dist/types/tools/yield.d.ts +22 -0
  137. package/dist/types/tui.d.ts +36 -0
  138. package/dist/types/utils.d.ts +6 -0
  139. package/dist/types/vim.d.ts +19 -5
  140. package/dist/types/windows-input-mode.d.ts +33 -1
  141. package/package.json +13 -9
  142. package/src/active-terminal.d.ts +30 -0
  143. package/src/active-terminal.ts +80 -0
  144. package/src/app-keybindings.d.ts +7 -2
  145. package/src/app-keybindings.ts +7 -2
  146. package/src/apps/autoresearch-dashboard.ts +7 -3
  147. package/src/apps/cleanse-board.ts +2 -2
  148. package/src/apps/cleanse-picker.ts +1 -1
  149. package/src/apps/debug/log-viewer.ts +188 -72
  150. package/src/apps/debug/raw-sse-buffer.ts +8 -0
  151. package/src/apps/debug/raw-sse.ts +39 -5
  152. package/src/apps/git/diff-pane.ts +96 -23
  153. package/src/apps/git/git-tui.ts +23 -16
  154. package/src/apps/git/help.ts +29 -20
  155. package/src/apps/git/sidebar.ts +8 -3
  156. package/src/apps/if-bench-board.ts +2 -2
  157. package/src/apps/ps-top.ts +2 -2
  158. package/src/apps/setup-model-picker.ts +1 -1
  159. package/src/autocomplete.d.ts +5 -2
  160. package/src/autocomplete.ts +67 -43
  161. package/src/bracketed-paste.ts +10 -3
  162. package/src/charts/chart-plan.d.ts +674 -0
  163. package/src/charts/chart-plan.ts +5081 -0
  164. package/src/charts/chart-svg.d.ts +59 -0
  165. package/src/charts/chart-svg.ts +4657 -0
  166. package/src/charts/table-data.d.ts +175 -0
  167. package/src/charts/table-data.ts +1478 -0
  168. package/src/chat/advisor-message.ts +11 -0
  169. package/src/chat/assistant-message.d.ts +11 -1
  170. package/src/chat/assistant-message.ts +245 -54
  171. package/src/chat/chat-transcript-builder.d.ts +3 -0
  172. package/src/chat/chat-transcript-builder.ts +9 -3
  173. package/src/chat/compaction-summary-message.d.ts +1 -0
  174. package/src/chat/compaction-summary-message.ts +8 -0
  175. package/src/chat/execution-shared.d.ts +6 -5
  176. package/src/chat/execution-shared.ts +42 -25
  177. package/src/chat/fence-figure.d.ts +42 -0
  178. package/src/chat/fence-figure.ts +80 -0
  179. package/src/chat/figure-markdown.d.ts +42 -0
  180. package/src/chat/figure-markdown.ts +174 -0
  181. package/src/chat/messages.d.ts +1 -1
  182. package/src/chat/messages.ts +1 -1
  183. package/src/chat/read-tool-group.ts +6 -4
  184. package/src/chat/skill-message.ts +6 -0
  185. package/src/chat/svg-figure.d.ts +33 -0
  186. package/src/chat/svg-figure.ts +399 -0
  187. package/src/chat/svg-source.d.ts +49 -0
  188. package/src/chat/svg-source.ts +188 -0
  189. package/src/chat/table-chart.d.ts +89 -0
  190. package/src/chat/table-chart.ts +294 -0
  191. package/src/chat/tool-execution.d.ts +2 -0
  192. package/src/chat/tool-execution.ts +86 -7
  193. package/src/chat/user-message.d.ts +17 -4
  194. package/src/chat/user-message.ts +71 -8
  195. package/src/chrome/shared.ts +53 -0
  196. package/src/chrome/status-notice.ts +25 -7
  197. package/src/chrome/transcript-container.d.ts +6 -0
  198. package/src/chrome/transcript-container.ts +162 -19
  199. package/src/components/box.d.ts +1 -0
  200. package/src/components/box.ts +5 -0
  201. package/src/components/disclosure.d.ts +1 -0
  202. package/src/components/disclosure.ts +8 -0
  203. package/src/components/editor.d.ts +23 -5
  204. package/src/components/editor.ts +316 -47
  205. package/src/components/form.d.ts +1 -0
  206. package/src/components/form.ts +8 -0
  207. package/src/components/image.d.ts +26 -44
  208. package/src/components/image.ts +115 -152
  209. package/src/components/input.d.ts +2 -1
  210. package/src/components/input.ts +17 -7
  211. package/src/components/key-value-list.d.ts +1 -0
  212. package/src/components/key-value-list.ts +4 -0
  213. package/src/components/layout/row.d.ts +1 -0
  214. package/src/components/layout/row.ts +7 -0
  215. package/src/components/layout/stack.d.ts +1 -0
  216. package/src/components/layout/stack.ts +7 -0
  217. package/src/components/loader.d.ts +5 -6
  218. package/src/components/loader.ts +10 -17
  219. package/src/components/markdown.d.ts +47 -5
  220. package/src/components/markdown.ts +786 -303
  221. package/src/components/scroll-view.d.ts +1 -0
  222. package/src/components/scroll-view.ts +5 -0
  223. package/src/components/section.d.ts +1 -0
  224. package/src/components/section.ts +5 -0
  225. package/src/components/settings-list.ts +2 -0
  226. package/src/components/table.d.ts +1 -0
  227. package/src/components/table.ts +4 -0
  228. package/src/components/text.d.ts +1 -0
  229. package/src/components/text.ts +7 -0
  230. package/src/components/tree-view.d.ts +7 -0
  231. package/src/components/tree-view.ts +38 -8
  232. package/src/components/truncated-text.d.ts +1 -0
  233. package/src/components/truncated-text.ts +5 -0
  234. package/src/desktop-notify.ts +3 -2
  235. package/src/fuzzy.d.ts +29 -1
  236. package/src/fuzzy.ts +115 -30
  237. package/src/hotkeys-markdown.ts +9 -2
  238. package/src/key-hint-format.d.ts +9 -1
  239. package/src/key-hint-format.ts +26 -11
  240. package/src/keybinding-matchers.d.ts +3 -0
  241. package/src/keybinding-matchers.ts +6 -2
  242. package/src/keybindings.d.ts +1 -1
  243. package/src/keybindings.ts +1 -1
  244. package/src/kitty-graphics.d.ts +2 -2
  245. package/src/kitty-graphics.ts +11 -5
  246. package/src/loop-watchdog.d.ts +8 -7
  247. package/src/loop-watchdog.ts +18 -13
  248. package/src/multiplexers/cmux.d.ts +16 -0
  249. package/src/multiplexers/cmux.ts +29 -0
  250. package/src/multiplexers/herdr.d.ts +26 -0
  251. package/src/multiplexers/herdr.ts +64 -0
  252. package/src/multiplexers/notify.d.ts +13 -0
  253. package/src/multiplexers/notify.ts +24 -0
  254. package/src/multiplexers/orca.d.ts +12 -0
  255. package/src/multiplexers/orca.ts +16 -0
  256. package/src/multiplexers/screen.d.ts +9 -0
  257. package/src/multiplexers/screen.ts +15 -0
  258. package/src/multiplexers/tmux.d.ts +29 -0
  259. package/src/multiplexers/tmux.ts +40 -0
  260. package/src/multiplexers/types.d.ts +100 -0
  261. package/src/multiplexers/types.ts +110 -0
  262. package/src/multiplexers/wmux.d.ts +12 -0
  263. package/src/multiplexers/wmux.ts +16 -0
  264. package/src/multiplexers/zellij.d.ts +12 -0
  265. package/src/multiplexers/zellij.ts +24 -0
  266. package/src/native/apply.d.ts +4 -3
  267. package/src/native/apply.ts +4 -3
  268. package/src/native/backend.d.ts +5 -4
  269. package/src/native/backend.ts +199 -22
  270. package/src/native/blobs.d.ts +7 -3
  271. package/src/native/blobs.ts +47 -12
  272. package/src/native/encode.d.ts +8 -6
  273. package/src/native/encode.ts +76 -34
  274. package/src/native/node.d.ts +36 -5
  275. package/src/native/node.ts +31 -5
  276. package/src/native/overlay.d.ts +8 -3
  277. package/src/native/overlay.ts +17 -4
  278. package/src/native/reconcile.ts +16 -4
  279. package/src/native/settle.d.ts +4 -3
  280. package/src/native/settle.ts +4 -3
  281. package/src/overlays/agent-hub-projection.d.ts +6 -5
  282. package/src/overlays/agent-hub-projection.ts +124 -13
  283. package/src/overlays/agent-hub-types.d.ts +9 -0
  284. package/src/overlays/agent-hub-types.ts +6 -0
  285. package/src/overlays/agent-hub.ts +48 -29
  286. package/src/overlays/agent-transcript-viewer.ts +93 -23
  287. package/src/overlays/agents-hub.ts +28 -8
  288. package/src/overlays/annotation-overlay.ts +293 -90
  289. package/src/overlays/annotation-types.ts +2 -0
  290. package/src/overlays/ask-dialog.ts +10 -17
  291. package/src/overlays/btw-history-panel.ts +136 -39
  292. package/src/overlays/copy-selector.d.ts +1 -0
  293. package/src/overlays/copy-selector.ts +32 -8
  294. package/src/overlays/extensions/extension-dashboard.ts +4 -4
  295. package/src/overlays/extensions/state-manager.ts +52 -12
  296. package/src/overlays/history-search.ts +100 -7
  297. package/src/overlays/hook-selector.ts +12 -5
  298. package/src/overlays/hub-frame.ts +19 -1
  299. package/src/overlays/jobs-panel.ts +33 -52
  300. package/src/overlays/model-browser.ts +367 -122
  301. package/src/overlays/model-hub.ts +435 -59
  302. package/src/overlays/move-overlay.ts +28 -14
  303. package/src/overlays/oauth-selector.ts +54 -70
  304. package/src/overlays/pause-screen.ts +2 -2
  305. package/src/overlays/plan-review-overlay.ts +208 -70
  306. package/src/overlays/plugin-settings.ts +3 -3
  307. package/src/overlays/report-panel.ts +191 -0
  308. package/src/overlays/rewind-selector.ts +88 -17
  309. package/src/overlays/session-observer-registry.d.ts +2 -0
  310. package/src/overlays/session-observer-registry.ts +10 -2
  311. package/src/overlays/session-selector.ts +64 -26
  312. package/src/overlays/settings-defs.ts +5 -4
  313. package/src/overlays/settings-selector.ts +33 -8
  314. package/src/overlays/snapcompact-shape-preview.ts +2 -3
  315. package/src/overlays/stats-notice.ts +1 -1
  316. package/src/overlays/thinking-selector.ts +11 -10
  317. package/src/overlays/tree-selector.ts +41 -20
  318. package/src/overlays/usage-dashboard.ts +117 -58
  319. package/src/overlays/usage-display.ts +10 -4
  320. package/src/overlays/usage-row.ts +22 -8
  321. package/src/prompt/attachment-chips.ts +15 -11
  322. package/src/prompt/composer-attachments.ts +6 -4
  323. package/src/prompt/composer-cache.ts +7 -86
  324. package/src/prompt/composer.ts +37 -26
  325. package/src/prompt/custom-editor.ts +282 -105
  326. package/src/prompt/image-source.ts +2 -1
  327. package/src/prompt/interactive-context-helpers.ts +32 -18
  328. package/src/prompt/macos-spelling.ts +7 -0
  329. package/src/prompt/magic-keywords.ts +11 -2
  330. package/src/prompt/model-mention-autocomplete.ts +14 -14
  331. package/src/prompt/queued-messages.ts +6 -4
  332. package/src/prompt/tips.txt +13 -9
  333. package/src/prompt/welcome.ts +64 -46
  334. package/src/prompt/word-completion.ts +38 -5
  335. package/src/render/composite.d.ts +2 -0
  336. package/src/render/composite.ts +64 -0
  337. package/src/render/output-pane.d.ts +1 -0
  338. package/src/render/output-pane.ts +6 -0
  339. package/src/render/render-utils.d.ts +1 -0
  340. package/src/render/render-utils.ts +16 -1
  341. package/src/render/tool-card.d.ts +6 -0
  342. package/src/render/tool-card.ts +15 -0
  343. package/src/render/width-aware-text.d.ts +2 -0
  344. package/src/render/width-aware-text.ts +5 -0
  345. package/src/setup/scenes/model.ts +2 -1
  346. package/src/setup/scenes/types.ts +1 -0
  347. package/src/space-hold.d.ts +40 -25
  348. package/src/space-hold.ts +156 -52
  349. package/src/status-line/component.ts +292 -78
  350. package/src/status-line/context-usage.ts +21 -46
  351. package/src/status-line/host.ts +4 -1
  352. package/src/status-line/segments.ts +40 -22
  353. package/src/status-line/types.ts +15 -3
  354. package/src/terminal-capabilities.d.ts +41 -36
  355. package/src/terminal-capabilities.ts +137 -213
  356. package/src/terminal-multiplexer.d.ts +127 -14
  357. package/src/terminal-multiplexer.ts +112 -33
  358. package/src/terminal.d.ts +23 -16
  359. package/src/terminal.ts +203 -99
  360. package/src/theme/active-symbols.d.ts +1 -1
  361. package/src/theme/active-symbols.ts +1 -1
  362. package/src/theme/schema-validation.ts +1 -0
  363. package/src/theme/schema.d.ts +11 -2
  364. package/src/theme/schema.ts +14 -1
  365. package/src/theme/session-color.ts +1 -7
  366. package/src/theme/symbols.d.ts +1 -1
  367. package/src/theme/symbols.ts +11 -4
  368. package/src/theme/theme-class.d.ts +7 -0
  369. package/src/theme/theme-class.ts +26 -1
  370. package/src/theme/theme-schema.json +5 -1
  371. package/src/theme/theme.d.ts +1 -1
  372. package/src/theme/theme.ts +8 -4
  373. package/src/theme/tui-adapters.d.ts +3 -0
  374. package/src/theme/tui-adapters.ts +68 -0
  375. package/src/thinking.d.ts +1 -1
  376. package/src/thinking.ts +1 -1
  377. package/src/tmux.d.ts +11 -4
  378. package/src/tmux.ts +47 -11
  379. package/src/tools/ast-edit.ts +59 -35
  380. package/src/tools/ast-grep.ts +35 -16
  381. package/src/tools/bash.d.ts +7 -2
  382. package/src/tools/bash.ts +116 -47
  383. package/src/tools/daemon.d.ts +1 -1
  384. package/src/tools/daemon.ts +1 -1
  385. package/src/tools/default-renderer.ts +35 -19
  386. package/src/tools/edit.ts +142 -120
  387. package/src/tools/eval-format/index.ts +28 -2
  388. package/src/tools/eval-format/javascript.ts +7 -5
  389. package/src/tools/eval.d.ts +22 -3
  390. package/src/tools/eval.ts +364 -208
  391. package/src/tools/fetch.ts +5 -6
  392. package/src/tools/glob.d.ts +4 -0
  393. package/src/tools/glob.ts +33 -65
  394. package/src/tools/grep.ts +81 -48
  395. package/src/tools/index.ts +2 -0
  396. package/src/tools/lsp.ts +81 -59
  397. package/src/tools/mcp.ts +111 -68
  398. package/src/tools/native-view.d.ts +47 -0
  399. package/src/tools/native-view.ts +102 -1
  400. package/src/tools/output-meta.d.ts +11 -1
  401. package/src/tools/output-meta.ts +23 -3
  402. package/src/tools/read.ts +7 -5
  403. package/src/tools/renderer.d.ts +37 -2
  404. package/src/tools/renderer.ts +42 -2
  405. package/src/tools/streaming-output.d.ts +20 -1
  406. package/src/tools/streaming-output.ts +154 -50
  407. package/src/tools/task.d.ts +2 -3
  408. package/src/tools/task.ts +115 -62
  409. package/src/tools/todo.ts +119 -30
  410. package/src/tools/wait.ts +34 -24
  411. package/src/tools/web-search.ts +1 -1
  412. package/src/tools/write.d.ts +9 -2
  413. package/src/tools/write.ts +147 -95
  414. package/src/tools/yield.d.ts +22 -0
  415. package/src/tools/yield.ts +225 -0
  416. package/src/tui.d.ts +36 -0
  417. package/src/tui.ts +666 -217
  418. package/src/utils.d.ts +6 -0
  419. package/src/utils.ts +19 -0
  420. package/src/vim.d.ts +19 -5
  421. package/src/vim.ts +333 -18
  422. package/src/windows-input-mode.d.ts +33 -1
  423. package/src/windows-input-mode.ts +212 -6
@@ -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 {
@@ -74,6 +74,8 @@ import {
74
74
  } from "./hub-frame";
75
75
  import { renderSegmentTrack } from "../chrome/segment-track";
76
76
 
77
+ const MODEL_HUB_BODY_MIN_WIDTH = 28;
78
+
77
79
  /**
78
80
  * A row of the Roles view: a role, a model/wildcard chain-key header, one of a
79
81
  * chain's fallback entries, or the trailing "+ New role…". Fallback rows under
@@ -107,6 +109,11 @@ export interface ModelHubSource extends ModelBrowserSource {
107
109
  getProjectModelRole(role: string): string | undefined;
108
110
  getGlobalModelRole(role: string): string | undefined;
109
111
  getModelRoleSource(role: string): "global" | "project" | "default";
112
+ /**
113
+ * Saved model presets (in switch order) and the one the current role setup
114
+ * matches, if any. Absent hosts have no presets to switch between.
115
+ */
116
+ getModelPresets?(): { names: readonly string[]; active: string | undefined };
110
117
  }
111
118
 
112
119
  /** Catalog capabilities required by the model hub. */
@@ -155,11 +162,39 @@ export interface ModelHubCallbacks {
155
162
  onLoginRequest?: (providerId: string) => void;
156
163
  /** Save the current role assignments and default thinking level as a named model preset. */
157
164
  onSavePreset?: (name: string) => void;
165
+ /**
166
+ * Apply a saved model preset (ctrl+←/→ or p/⇧P in the Roles view) and report
167
+ * the outcome itself. A switch that writes nothing (a refused preset) still
168
+ * advances the hub's preset cursor, so the next press moves past it.
169
+ */
170
+ onSwitchPreset?: (name: string) => void | Promise<void>;
158
171
  /** Persist a new quick-switch cycle order (the ctrl+p role cycle). */
159
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;
160
192
  onCancel: () => void;
161
193
  }
162
194
 
195
+ /** Why {@link ModelHubCallbacks.onCompactionPointChange} kept the field open. */
196
+ export type CompactionPointChangeResult = { kind: "error"; message: string } | { kind: "confirm"; message: string };
197
+
163
198
  export interface ModelHubOptions {
164
199
  /** Preselect this provider's sidebar entry (e.g. when reopening after /login). */
165
200
  initialProviderId?: string;
@@ -211,10 +246,36 @@ type StripState =
211
246
  kind: "name";
212
247
  purpose: "role" | "preset";
213
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 };
214
263
  };
215
264
 
216
265
  /** A Roles-view command; keys and the picker's action bar both run {@link ModelHubComponent}'s `#runRolesAction`. */
217
- type RolesAction = "pick" | "clear" | "fallback" | "cycle" | "earlier" | "later" | "new" | "thinking" | "save";
266
+ type RolesAction =
267
+ | "pick"
268
+ | "clear"
269
+ | "fallback"
270
+ | "cycle"
271
+ | "earlier"
272
+ | "later"
273
+ | "new"
274
+ | "thinking"
275
+ | "compaction"
276
+ | "save"
277
+ | "nextPreset"
278
+ | "prevPreset";
218
279
 
219
280
  /** Printable keys of the Roles view and the command each runs. */
220
281
  const ROLES_ACTION_KEYS: Record<string, RolesAction> = {
@@ -225,7 +286,11 @@ const ROLES_ACTION_KEYS: Record<string, RolesAction> = {
225
286
  "]": "later",
226
287
  n: "new",
227
288
  t: "thinking",
289
+ k: "compaction",
228
290
  s: "save",
291
+ // Letter twins of ctrl+←/→, which macOS reserves for switching Spaces.
292
+ p: "nextPreset",
293
+ P: "prevPreset",
229
294
  };
230
295
 
231
296
  /** Picker fact columns of the Roles view. */
@@ -259,6 +324,41 @@ function providerInitials(providerId: string): string {
259
324
 
260
325
  const PROVIDER_REFRESH_DEBOUNCE_MS = 120;
261
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
+
262
362
  const MODEL_KIND_TABS: ReadonlyArray<"all" | ModelKind> = ["all", ...MODEL_KINDS];
263
363
  const ROLE_TABS = ["all", "chat", "kind"] as const;
264
364
  type RoleTab = (typeof ROLE_TABS)[number];
@@ -307,7 +407,15 @@ export class ModelHubComponent implements Component {
307
407
  #roles: RoleAssignments = {};
308
408
  #availableItems: ModelBrowserItem[] = [];
309
409
  #recentItems: ModelBrowserItem[] = [];
410
+ /** Selectors of {@link #recentItems}, rebuilt with it, for search hit counts. */
411
+ #recentSelectors: ReadonlySet<string> = new Set();
310
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;
311
419
  #modelKindTab: "all" | ModelKind = "all";
312
420
  #roleTab: RoleTab = "all";
313
421
  #configError: string | undefined;
@@ -343,6 +451,12 @@ export class ModelHubComponent implements Component {
343
451
  #assigning: AssignTarget | null = null;
344
452
  #strip: StripState | null = null;
345
453
  #assignmentPending = false;
454
+ /**
455
+ * Last preset ctrl+←/→ tried in this hub, with the settings revision after the
456
+ * attempt; steps from it while nothing matches the setup or nothing changed since.
457
+ */
458
+ #presetCursor: { name: string; revision: number } | undefined;
459
+ #presetsMemo: { revision: number; names: readonly string[]; active: string | undefined } | undefined;
346
460
  #disposed = false;
347
461
  /** Per-provider fuzzy match counts while a query is active; null when not searching. */
348
462
  #searchCounts: Map<string, number> | null = null;
@@ -376,6 +490,7 @@ export class ModelHubComponent implements Component {
376
490
  { min: 18, max: 26 },
377
491
  (width, rows) => this.#renderSidebar(width, rows),
378
492
  this.#renderBodyPane,
493
+ { bodyMinWidth: MODEL_HUB_BODY_MIN_WIDTH, preserveSidebar: true },
379
494
  );
380
495
  #lockedLoginLine: number | null = null;
381
496
  #rolesRowStart = 1;
@@ -394,6 +509,7 @@ export class ModelHubComponent implements Component {
394
509
  row: RolesRow | undefined;
395
510
  roles: RoleAssignments;
396
511
  rows: readonly RolesRow[];
512
+ revision: number;
397
513
  children: readonly NativeChild[];
398
514
  }
399
515
  | undefined;
@@ -484,22 +600,6 @@ export class ModelHubComponent implements Component {
484
600
  return this.#settings.knownRoleIds.filter(role => !this.#settings.getRoleInfo(role).hidden);
485
601
  }
486
602
 
487
- /**
488
- * Models a `--models`/`enabledModels` scope exposes. The scope only resolves
489
- * chat models (it feeds Ctrl+P cycling), so available non-chat runners
490
- * (judge, search, image, …) join from the registry — runtime role
491
- * resolution ignores the scope for them as well.
492
- */
493
- #scopedPool(): Model[] {
494
- const pool = this.#scopedModels.map(scoped => scoped.model);
495
- for (const model of this.#registry.getAvailable("all")) {
496
- if (modelKind(model) === "chat") continue;
497
- if (this.#scopedModels.some(scoped => modelsAreEqual(scoped.model, model))) continue;
498
- pool.push(model);
499
- }
500
- return pool;
501
- }
502
-
503
603
  /** Resolve every known role: configured values first, auto-selection for the rest. */
504
604
  #reloadRoles(autoCandidates: ReadonlyArray<Model>): void {
505
605
  const allModels = this.#scopedModels.length > 0 ? autoCandidates : this.#registry.getAll("all");
@@ -518,7 +618,10 @@ export class ModelHubComponent implements Component {
518
618
  if (this.#scopedModels.length > 0) {
519
619
  this.#configError = undefined;
520
620
  try {
521
- allModels = this.#scopedPool();
621
+ allModels = scopedRolePool(
622
+ this.#scopedModels.map(scoped => scoped.model),
623
+ this.#registry,
624
+ );
522
625
  } catch (error) {
523
626
  this.#configError = error instanceof Error ? error.message : String(error);
524
627
  allModels = this.#scopedModels.map(scoped => scoped.model);
@@ -553,6 +656,7 @@ export class ModelHubComponent implements Component {
553
656
  if (item) this.#recentItems.push(item);
554
657
  if (this.#recentItems.length >= RECENT_LIMIT) break;
555
658
  }
659
+ this.#recentSelectors = new Set(this.#recentItems.map(item => item.selector));
556
660
 
557
661
  this.#buildSidebar(allModels, availableModels);
558
662
  this.#restoreSidebarAnchor(anchor);
@@ -775,13 +879,27 @@ export class ModelHubComponent implements Component {
775
879
  }
776
880
  }
777
881
 
882
+ /**
883
+ * Push the active scope's items into the browser. While assigning a role,
884
+ * the role's `accepts` predicate is re-applied here so a scope hop
885
+ * (provider/all/recent) can never surface a model the role rejects — role
886
+ * resolution and the runtime candidate pool filter the same way, so an
887
+ * unaccepted pick would persist a selector that never resolves.
888
+ */
778
889
  #setCandidateItems(items: ReadonlyArray<ModelBrowserItem>): void {
779
- this.#candidateItems = [...items];
890
+ const assigning = this.#assigning;
891
+ const scoped =
892
+ assigning?.kind === "role"
893
+ ? items.filter(item => this.#settings.getRoleInfo(assigning.role).accepts(item.model))
894
+ : items;
895
+ this.#candidateItems = [...scoped];
896
+ this.#candidateCatalog = scoped === this.#availableItems ? this.#availableItems : undefined;
780
897
  this.#applyModelKind();
781
898
  }
782
899
 
783
900
  #applyModelKind(): void {
784
901
  const kind = this.#modelKindTab;
902
+ this.#browserCatalog = kind === "all" ? this.#candidateCatalog : undefined;
785
903
  this.#browser.setItems(
786
904
  kind === "all"
787
905
  ? [...this.#candidateItems]
@@ -890,16 +1008,14 @@ export class ModelHubComponent implements Component {
890
1008
  this.#composeEntries();
891
1009
  return;
892
1010
  }
893
- const matches = fuzzyFilter(this.#availableItems, query, modelSearchText);
1011
+ const matches = this.#catalogMatches(query);
894
1012
  const counts = new Map<string, number>();
1013
+ let recentCount = 0;
895
1014
  for (const item of matches) {
896
1015
  counts.set(item.provider, (counts.get(item.provider) ?? 0) + 1);
1016
+ if (this.#recentSelectors.has(item.selector)) recentCount++;
897
1017
  }
898
- const recentSelectors = new Set(this.#recentItems.map(item => item.selector));
899
- this.#recentSearchCount = matches.reduce(
900
- (total, item) => total + (recentSelectors.has(item.selector) ? 1 : 0),
901
- 0,
902
- );
1018
+ this.#recentSearchCount = recentCount;
903
1019
  this.#searchTotal = matches.length;
904
1020
  this.#searchCounts = counts;
905
1021
  this.#composeEntries();
@@ -913,6 +1029,24 @@ export class ModelHubComponent implements Component {
913
1029
  }
914
1030
  }
915
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
+
916
1050
  /**
917
1051
  * Entries the scope hop skips: separators always; while searching, also
918
1052
  * the Roles view (not a model scope), an empty Recent, locked providers,
@@ -1103,7 +1237,13 @@ export class ModelHubComponent implements Component {
1103
1237
  #roleForScope(role: string, scope: ModelRoleSelectionScope): ResolvedModelRoleValue {
1104
1238
  const roleValue =
1105
1239
  scope === "project" ? this.#settings.getProjectModelRole(role) : this.#settings.getGlobalModelRole(role);
1106
- 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");
1107
1247
  const roleLookup: ModelRoleLookup = {
1108
1248
  getModelRole: scopedRole =>
1109
1249
  scope === "project"
@@ -1521,14 +1661,15 @@ export class ModelHubComponent implements Component {
1521
1661
  }
1522
1662
  }
1523
1663
 
1524
- /** Switch the body into assign mode for `role`: full catalog, cleared query, current model preselected. */
1664
+ /**
1665
+ * Switch the body into assign mode for `role`: full catalog, cleared query, current model preselected.
1666
+ * Arrows land on the model rows (the next step of the flow); ← still reaches the provider scopes.
1667
+ */
1525
1668
  #startAssign(role: string): void {
1526
1669
  this.#assigning = { kind: "role", role };
1527
- this.#focus = "scope";
1670
+ this.#focus = "list";
1528
1671
  this.#browser.setShowProvider(true);
1529
- this.#setCandidateItems(
1530
- this.#availableItems.filter(item => this.#settings.getRoleInfo(role).accepts(item.model)),
1531
- );
1672
+ this.#setCandidateItems(this.#availableItems);
1532
1673
  this.#browser.setQuery("");
1533
1674
  const current = this.#roles[role];
1534
1675
  if (current) {
@@ -1539,7 +1680,7 @@ export class ModelHubComponent implements Component {
1539
1680
  /** Browse the catalog to fill a fallback-chain slot: `index` replaces an entry, `null` appends. */
1540
1681
  #startAssignFallback(role: string, index: number | null): void {
1541
1682
  this.#assigning = { kind: "fallback", role, index };
1542
- this.#focus = "scope";
1683
+ this.#focus = "list";
1543
1684
  this.#browser.setShowProvider(true);
1544
1685
  this.#setCandidateItems(this.#availableItems);
1545
1686
  this.#browser.setQuery("");
@@ -1557,7 +1698,7 @@ export class ModelHubComponent implements Component {
1557
1698
  /** Browse the catalog for the primary model a brand-new fallback chain protects. */
1558
1699
  #startAssignFallbackKey(): void {
1559
1700
  this.#assigning = { kind: "fallbackKey" };
1560
- this.#focus = "scope";
1701
+ this.#focus = "list";
1561
1702
  this.#browser.setShowProvider(true);
1562
1703
  this.#setCandidateItems(this.#availableItems);
1563
1704
  this.#browser.setQuery("");
@@ -1684,10 +1825,46 @@ export class ModelHubComponent implements Component {
1684
1825
  this.#strip = { kind: "name", purpose, input: new Input() };
1685
1826
  }
1686
1827
 
1687
- /** 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. */
1688
1847
  #submitNameStrip(): void {
1689
1848
  const strip = this.#strip;
1690
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
+ }
1691
1868
  if (strip.purpose === "preset") {
1692
1869
  this.#submitPresetName();
1693
1870
  return;
@@ -1739,6 +1916,52 @@ export class ModelHubComponent implements Component {
1739
1916
  this.#buildRolesRows();
1740
1917
  }
1741
1918
 
1919
+ /** Saved presets and the one the setup matches; empty unless the host can both list and switch. */
1920
+ #presets(): { names: readonly string[]; active: string | undefined } {
1921
+ if (!this.#callbacks.onSwitchPreset || !this.#settings.getModelPresets) return { names: [], active: undefined };
1922
+ const revision = this.#settings.revision;
1923
+ if (this.#presetsMemo?.revision !== revision) {
1924
+ const { names, active } = this.#settings.getModelPresets();
1925
+ this.#presetsMemo = { revision, names, active };
1926
+ }
1927
+ return this.#presetsMemo;
1928
+ }
1929
+
1930
+ /**
1931
+ * The preset ctrl+←/→ steps from: the last one tried here while settings are
1932
+ * unchanged since (a refused switch writes nothing, so the next press moves
1933
+ * past it) or nothing matches; otherwise the preset the setup matches.
1934
+ */
1935
+ #presetBase(active: string | undefined): string | undefined {
1936
+ const cursor = this.#presetCursor;
1937
+ if (cursor && (active === undefined || cursor.revision === this.#settings.revision)) return cursor.name;
1938
+ return active;
1939
+ }
1940
+
1941
+ /** Apply the previous/next saved preset (wrapping), as `/modelpreset switch` does. */
1942
+ #switchPreset(delta: -1 | 1): void {
1943
+ const onSwitchPreset = this.#callbacks.onSwitchPreset;
1944
+ const { names, active } = this.#presets();
1945
+ if (!onSwitchPreset || names.length === 0) return;
1946
+ const base = this.#presetBase(active);
1947
+ const current = base === undefined ? -1 : names.indexOf(base);
1948
+ const next = current < 0 ? (delta > 0 ? 0 : names.length - 1) : (current + delta + names.length) % names.length;
1949
+ const name = names[next];
1950
+ if (name === undefined) return;
1951
+ const settle = () => {
1952
+ this.#presetCursor = { name, revision: this.#settings.revision };
1953
+ this.#refreshAfterMutation();
1954
+ };
1955
+ // Stepping onto the preset already in effect has nothing to apply.
1956
+ if (name === active) {
1957
+ settle();
1958
+ return;
1959
+ }
1960
+ // The host reports refusals itself, so the cursor moves once the switch
1961
+ // resolves, refused or not (`#presetBase` sees a refusal left the revision alone).
1962
+ this.#finishAssignment(onSwitchPreset(name), settle);
1963
+ }
1964
+
1742
1965
  handleInput(data: string): void {
1743
1966
  this.#nativeVersion++;
1744
1967
  if (this.#assignmentPending) {
@@ -1774,6 +1997,11 @@ export class ModelHubComponent implements Component {
1774
1997
  }
1775
1998
  return;
1776
1999
  }
2000
+ // Ctrl+←/→ in the Roles view steps through saved model presets, wrapping.
2001
+ if (rolesView && (matchesKey(data, "ctrl+left") || matchesKey(data, "ctrl+right"))) {
2002
+ this.#switchPreset(matchesKey(data, "ctrl+left") ? -1 : 1);
2003
+ return;
2004
+ }
1777
2005
  // Alt+←/→ cycles whichever tab strip is on screen: role tabs in the
1778
2006
  // Roles view, kind tabs in every browser view. Ctrl+←/→ is unusable on
1779
2007
  // macOS (Spaces shortcut). macOS terminals (ghostty, Terminal.app,
@@ -1818,7 +2046,13 @@ export class ModelHubComponent implements Component {
1818
2046
 
1819
2047
  if (rolesView) {
1820
2048
  const printable = extractPrintableText(data);
1821
- 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) {
1822
2056
  this.#setActiveEntry("all");
1823
2057
  this.#focus = "list";
1824
2058
  this.#browser.handleInput(data);
@@ -1895,6 +2129,7 @@ export class ModelHubComponent implements Component {
1895
2129
  return;
1896
2130
  }
1897
2131
  strip.input.handleInput(data);
2132
+ if (strip.purpose === "compaction") strip.error = undefined;
1898
2133
  return;
1899
2134
  }
1900
2135
  if (moveStripSelection(strip, data)) return;
@@ -2054,6 +2289,13 @@ export class ModelHubComponent implements Component {
2054
2289
  case "save":
2055
2290
  if (this.#callbacks.onSavePreset) this.#openNameStrip("preset");
2056
2291
  return;
2292
+ case "nextPreset":
2293
+ case "prevPreset":
2294
+ this.#switchPreset(action === "nextPreset" ? 1 : -1);
2295
+ return;
2296
+ case "compaction":
2297
+ this.#openCompactionStrip();
2298
+ return;
2057
2299
  case "thinking":
2058
2300
  if (role) {
2059
2301
  const target = this.#roleThinkingTarget(role);
@@ -2252,8 +2494,18 @@ export class ModelHubComponent implements Component {
2252
2494
  ROLE_TABS.map(tab => ({ label: tab === "kind" ? "kinds" : tab })),
2253
2495
  Math.max(0, active),
2254
2496
  );
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"])}`;
2503
+ const preset =
2504
+ names.length > 0
2505
+ ? ` ${theme.fg("dim", "Preset:")} ${activePreset ? theme.fg("accent", activePreset) : theme.fg("muted", "custom")} ${theme.fg("dim", presetKeys)}`
2506
+ : "";
2255
2507
  return truncateToWidth(
2256
- ` ${theme.fg("dim", "Roles:")} ${track} ${theme.fg("dim", formatKeyHints(["alt+left", "alt+right"]))}`,
2508
+ ` ${theme.fg("dim", "Roles:")} ${track} ${theme.fg("dim", formatKeyHints(["alt+left", "alt+right"]))}${preset}`,
2257
2509
  width,
2258
2510
  );
2259
2511
  }
@@ -2524,6 +2776,9 @@ export class ModelHubComponent implements Component {
2524
2776
  const strip = this.#strip;
2525
2777
  if (strip) {
2526
2778
  if (strip.kind === "name") {
2779
+ if (strip.purpose === "compaction") {
2780
+ return `${enter} ${compactionConfirmPending(strip) ? "accept" : "set"} compaction point · ${cancel} cancel`;
2781
+ }
2527
2782
  if (strip.purpose === "preset") {
2528
2783
  return `${enter} save preset · ${cancel} cancel`;
2529
2784
  }
@@ -2550,16 +2805,26 @@ export class ModelHubComponent implements Component {
2550
2805
  const entry = this.#activeEntry();
2551
2806
  if (entry.kind === "roles") {
2552
2807
  if (this.#focus !== "list") {
2553
- return `${upDown} providers · ${enterRight} roles · ${altLeftRight} tabs · ${cancel} close`;
2808
+ const presets =
2809
+ this.#presets().names.length > 0 ? ` · ${formatKeyHints(["ctrl+left", "ctrl+right"])} preset` : "";
2810
+ return `${upDown} providers · ${enterRight} roles · ${altLeftRight} tabs${presets} · ${cancel} close`;
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`;
2554
2815
  }
2555
2816
  const row = this.#rolesRows[this.#roleIndex];
2817
+ const compaction =
2818
+ this.#callbacks.onCompactionPointChange && this.#roleRowModel()
2819
+ ? ` · ${formatKeyHint("k")} compaction limit`
2820
+ : "";
2556
2821
  if (row?.kind === "fallback") {
2557
2822
  // Advertise `t` only when the entry resolves: wildcards always
2558
2823
  // inherit and unknown models have no ladder to offer, so the
2559
2824
  // action would be inert there.
2560
2825
  const editable = this.#resolveFallbackEntry(row.role, row.chainIndex) !== undefined;
2561
2826
  const thinking = editable ? ` · ${formatKeyHint("t")} thinking` : "";
2562
- 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`;
2563
2828
  }
2564
2829
  if (row?.kind === "chainKey") {
2565
2830
  return `${upDown} rows · ${formatKeyHints(["enter", "f"])} add fallback · ${formatKeyHint("x")} clear chain · ${left} providers`;
@@ -2572,7 +2837,8 @@ export class ModelHubComponent implements Component {
2572
2837
  const editable = row?.kind === "role" && this.#roleThinkingTarget(row.role) !== undefined;
2573
2838
  const thinking = editable ? ` · ${formatKeyHint("t")} thinking` : "";
2574
2839
  const savePreset = this.#callbacks.onSavePreset ? ` · ${formatKeyHint("s")} save preset` : "";
2575
- return `${upDown} rows · ${enter} pick · ${formatKeyHint("f")} fallback · ${formatKeyHint("x")} clear${thinking} · ${formatKeyHint("c")} cycle · [/] reorder · ${formatKeyHint("n")} new${savePreset}`;
2840
+ const switchPreset = this.#presets().names.length > 0 ? ` · ${formatKeyHints(["p", "shift+p"])} preset` : "";
2841
+ return `${upDown} rows · ${enter} pick · ${formatKeyHint("f")} fallback · ${formatKeyHint("x")} clear${thinking}${compaction} · ${formatKeyHint("c")} cycle · [/] reorder · ${formatKeyHint("n")} new${savePreset}${switchPreset}`;
2576
2842
  }
2577
2843
  if (entry.kind === "provider" && entry.locked) {
2578
2844
  return entry.oauth
@@ -2597,12 +2863,26 @@ export class ModelHubComponent implements Component {
2597
2863
 
2598
2864
  #renderStrip(width: number, strip: StripState): string {
2599
2865
  if (strip.kind === "name") {
2600
- const preset = strip.purpose === "preset";
2601
- 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:";
2602
2872
  const label = theme.fg("accent", labelText);
2603
- 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));
2604
2877
  const inputLine = strip.input.render(inputWidth)[0] ?? "";
2605
- 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);
2606
2886
  }
2607
2887
 
2608
2888
  const prefix =
@@ -3024,7 +3304,9 @@ export class ModelHubComponent implements Component {
3024
3304
  size: "lg",
3025
3305
  layout: "rows",
3026
3306
  preview: "side",
3027
- 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,
3028
3310
  cursor: this.#browser.cursor,
3029
3311
  placeholder: "Search models…",
3030
3312
  scopes: this.#pickerScopes(),
@@ -3130,6 +3412,17 @@ export class ModelHubComponent implements Component {
3130
3412
  if (entry.kind === "provider" && entry.providerId && this.#refreshingProviders.has(entry.providerId)) {
3131
3413
  return `${entry.label} · refreshing model list…`;
3132
3414
  }
3415
+ const presets = rolesView ? this.#presets() : undefined;
3416
+ if (presets && presets.names.length > 0) {
3417
+ return [
3418
+ span("Preset ", "muted"),
3419
+ presets.active ? span(presets.active, "strong") : span("custom", "muted"),
3420
+ span(
3421
+ this.#scopedModels.length > 0 ? " · --models scope" : " · Cleared roles fall back to auto-selection",
3422
+ "muted",
3423
+ ),
3424
+ ];
3425
+ }
3133
3426
  if (this.#scopedModels.length > 0) return "--models scope";
3134
3427
  if (rolesView) return "Cleared roles fall back to auto-selection";
3135
3428
  return undefined;
@@ -3242,12 +3535,19 @@ export class ModelHubComponent implements Component {
3242
3535
  if (strip) {
3243
3536
  const apply =
3244
3537
  strip.kind === "name"
3245
- ? pickerAction(
3246
- strip.purpose === "preset" ? "presetName" : "roleName",
3247
- strip.purpose === "preset" ? "Save preset" : "Create role",
3248
- "enter",
3249
- { primary: true },
3250
- )
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
+ )
3251
3551
  : pickerAction(
3252
3552
  "stripApply",
3253
3553
  strip.kind === "thinking" ? "Apply" : strip.kind === "scope" ? "Save to scope" : "Assign / clear",
@@ -3269,9 +3569,20 @@ export class ModelHubComponent implements Component {
3269
3569
  }
3270
3570
  if (rolesView) {
3271
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;
3272
3574
  const roleAction = (action: RolesAction, label: string, key: string, primary = false) =>
3273
- 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
+ );
3274
3581
  const actions: (TspPickerAction | undefined)[] = [];
3582
+ const compaction =
3583
+ this.#callbacks.onCompactionPointChange && this.#roleRowModel()
3584
+ ? roleAction("compaction", "Compaction limit", "k")
3585
+ : undefined;
3275
3586
  switch (row?.kind) {
3276
3587
  case "role": {
3277
3588
  const assigned = this.#roles[row.role];
@@ -3280,9 +3591,11 @@ export class ModelHubComponent implements Component {
3280
3591
  roleAction("fallback", "Add fallback", "f"),
3281
3592
  assigned && !assigned.autoSelected ? roleAction("clear", "Clear", "x") : undefined,
3282
3593
  this.#roleThinkingTarget(row.role) ? roleAction("thinking", "Thinking", "t") : undefined,
3594
+ compaction,
3283
3595
  roleAction("cycle", this.#cycleOrder().includes(row.role) ? "Leave cycle" : "Add to cycle", "c"),
3284
3596
  roleAction("new", "New role", "n"),
3285
3597
  this.#callbacks.onSavePreset ? roleAction("save", "Save preset", "s") : undefined,
3598
+ this.#presets().names.length > 0 ? roleAction("nextPreset", "Next preset", "p") : undefined,
3286
3599
  );
3287
3600
  break;
3288
3601
  }
@@ -3294,6 +3607,7 @@ export class ModelHubComponent implements Component {
3294
3607
  this.#resolveFallbackEntry(row.role, row.chainIndex)
3295
3608
  ? roleAction("thinking", "Thinking", "t")
3296
3609
  : undefined,
3610
+ compaction,
3297
3611
  roleAction("earlier", "Earlier", "["),
3298
3612
  roleAction("later", "Later", "]"),
3299
3613
  );
@@ -3330,6 +3644,19 @@ export class ModelHubComponent implements Component {
3330
3644
  /** The open strip as the picker's chip strip (role assignment, save scope, thinking level, new role name). */
3331
3645
  #pickerStrip(strip: StripState): NonNullable<TspPickerProps["strip"]> {
3332
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
+ }
3333
3660
  return {
3334
3661
  label: [
3335
3662
  span(strip.purpose === "preset" ? "Preset name " : "New role name ", "muted"),
@@ -3469,7 +3796,14 @@ export class ModelHubComponent implements Component {
3469
3796
  /** The selected Roles-view row's preview: its model's facts and its fallback chain. */
3470
3797
  #rolePreview(row: RolesRow | undefined): readonly NativeChild[] {
3471
3798
  const memo = this.#pickerRolePreview;
3472
- 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
+ ) {
3473
3807
  return memo.children;
3474
3808
  }
3475
3809
  const children: NativeChild[] = [];
@@ -3543,7 +3877,7 @@ export class ModelHubComponent implements Component {
3543
3877
  );
3544
3878
  break;
3545
3879
  }
3546
- this.#pickerRolePreview = { row, roles: this.#roles, rows: this.#rolesRows, children };
3880
+ this.#pickerRolePreview = { row, roles: this.#roles, rows: this.#rolesRows, revision, children };
3547
3881
  return children;
3548
3882
  }
3549
3883
 
@@ -3643,6 +3977,9 @@ export class ModelHubComponent implements Component {
3643
3977
  case "presetName":
3644
3978
  this.#submitPresetName();
3645
3979
  return;
3980
+ case "compactionPoint":
3981
+ this.#submitNameStrip();
3982
+ return;
3646
3983
  case "cancel":
3647
3984
  if (this.#strip) this.#closeStrip();
3648
3985
  return;
@@ -3693,6 +4030,19 @@ export class ModelHubComponent implements Component {
3693
4030
  const strip = this.#strip;
3694
4031
  if (!strip) return undefined;
3695
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
+ }
3696
4046
  const preset = strip.purpose === "preset";
3697
4047
  return node(
3698
4048
  "row",
@@ -3746,9 +4096,17 @@ export class ModelHubComponent implements Component {
3746
4096
  if (strip) {
3747
4097
  switch (strip.kind) {
3748
4098
  case "name":
3749
- return strip.purpose === "preset"
3750
- ? [keys("save preset", "enter"), cancel("cancel")]
3751
- : [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")];
3752
4110
  case "role":
3753
4111
  return [keys("choose", "left", "right"), keys("assign/clear", "enter"), cancel("cancel")];
3754
4112
  case "scope":
@@ -3769,6 +4127,7 @@ export class ModelHubComponent implements Component {
3769
4127
  : "assign";
3770
4128
  return [keys(pick, "enter"), upDown("models"), keys("providers", "left"), search, kind, cancel("cancel")];
3771
4129
  }
4130
+ const presetHint = this.#presets().names.length > 0 ? keys("preset", "ctrl+left", "ctrl+right") : undefined;
3772
4131
  const entry = this.#activeEntry();
3773
4132
  if (entry.kind === "roles") {
3774
4133
  if (this.#focus !== "list") {
@@ -3776,10 +4135,24 @@ export class ModelHubComponent implements Component {
3776
4135
  upDown("providers"),
3777
4136
  keys("roles", "enter", "right"),
3778
4137
  keys("tabs", "alt+left", "alt+right"),
4138
+ presetHint,
4139
+ cancel("close"),
4140
+ ];
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,
3779
4150
  cancel("close"),
3780
4151
  ];
3781
4152
  }
3782
4153
  const row = this.#rolesRows[this.#roleIndex];
4154
+ const compaction =
4155
+ this.#callbacks.onCompactionPointChange && this.#roleRowModel() ? keys("compaction limit", "k") : undefined;
3783
4156
  if (row?.kind === "fallback") {
3784
4157
  // Advertise `t` only where a strip would open, as the ANSI footer does.
3785
4158
  const editable = this.#resolveFallbackEntry(row.role, row.chainIndex) !== undefined;
@@ -3789,6 +4162,7 @@ export class ModelHubComponent implements Component {
3789
4162
  keys("add another", "f"),
3790
4163
  keys("remove", "x"),
3791
4164
  editable ? keys("thinking", "t") : undefined,
4165
+ compaction,
3792
4166
  reorder,
3793
4167
  keys("providers", "left"),
3794
4168
  ];
@@ -3811,10 +4185,12 @@ export class ModelHubComponent implements Component {
3811
4185
  keys("fallback", "f"),
3812
4186
  keys("clear", "x"),
3813
4187
  editable ? keys("thinking", "t") : undefined,
4188
+ compaction,
3814
4189
  keys("cycle", "c"),
3815
4190
  reorder,
3816
4191
  keys("new", "n"),
3817
4192
  this.#callbacks.onSavePreset ? keys("save preset", "s") : undefined,
4193
+ this.#presets().names.length > 0 ? keys("preset", "ctrl+left", "ctrl+right", "p", "shift+p") : undefined,
3818
4194
  ];
3819
4195
  }
3820
4196
  if (entry.kind === "provider" && entry.locked) {