@oh-my-pi/pi-tui 18.4.3 → 18.4.4

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 (447) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/dist/types/apps/cleanse-board.d.ts +17 -0
  3. package/dist/types/apps/git/help.d.ts +16 -0
  4. package/dist/types/apps/git/sidebar.d.ts +18 -0
  5. package/dist/types/apps/live-visualizer.d.ts +10 -0
  6. package/dist/types/apps/ps-top.d.ts +15 -0
  7. package/dist/types/apps/standalone-picker.d.ts +3 -0
  8. package/dist/types/autocomplete.d.ts +8 -0
  9. package/dist/types/chat/advisor-message.d.ts +12 -2
  10. package/dist/types/chat/assistant-message.d.ts +20 -0
  11. package/dist/types/chat/bash-execution.d.ts +9 -0
  12. package/dist/types/chat/chat-transcript-builder.d.ts +6 -1
  13. package/dist/types/chat/collab-prompt-message.d.ts +4 -0
  14. package/dist/types/chat/compaction-summary-message.d.ts +15 -0
  15. package/dist/types/chat/eval-execution.d.ts +8 -0
  16. package/dist/types/chat/execution-shared.d.ts +58 -0
  17. package/dist/types/chat/late-diagnostics-message.d.ts +15 -1
  18. package/dist/types/chat/read-tool-group.d.ts +15 -0
  19. package/dist/types/chat/skill-message.d.ts +9 -0
  20. package/dist/types/chat/stripped-tool-calls-placeholder.d.ts +3 -0
  21. package/dist/types/chat/todo-reminder.d.ts +4 -1
  22. package/dist/types/chat/tool-execution.d.ts +25 -1
  23. package/dist/types/chat/transcript-actions.d.ts +37 -0
  24. package/dist/types/chat/transcript-entry.d.ts +3 -1
  25. package/dist/types/chat/user-message.d.ts +25 -0
  26. package/dist/types/chrome/collab-qrcode.d.ts +7 -0
  27. package/dist/types/chrome/context-thresholds.d.ts +11 -0
  28. package/dist/types/chrome/countdown-timer.d.ts +9 -3
  29. package/dist/types/chrome/diff.d.ts +12 -0
  30. package/dist/types/chrome/dynamic-border.d.ts +3 -0
  31. package/dist/types/chrome/format.d.ts +6 -0
  32. package/dist/types/chrome/message-divider.d.ts +13 -0
  33. package/dist/types/chrome/message-frame.d.ts +10 -0
  34. package/dist/types/chrome/message-notice.d.ts +23 -0
  35. package/dist/types/chrome/overlay-box.d.ts +11 -2
  36. package/dist/types/chrome/qrcode.d.ts +7 -0
  37. package/dist/types/chrome/segment-track.d.ts +7 -0
  38. package/dist/types/chrome/selector-helpers.d.ts +0 -6
  39. package/dist/types/chrome/status-notice.d.ts +14 -0
  40. package/dist/types/chrome/streaming-panel.d.ts +3 -0
  41. package/dist/types/chrome/tool-activity.d.ts +3 -0
  42. package/dist/types/chrome/transcript-container.d.ts +13 -0
  43. package/dist/types/chrome/transcript-status.d.ts +15 -3
  44. package/dist/types/components/box.d.ts +7 -0
  45. package/dist/types/components/disclosure.d.ts +9 -0
  46. package/dist/types/components/editor.d.ts +37 -0
  47. package/dist/types/components/form.d.ts +14 -0
  48. package/dist/types/components/image.d.ts +7 -0
  49. package/dist/types/components/input.d.ts +20 -1
  50. package/dist/types/components/key-value-list.d.ts +3 -0
  51. package/dist/types/components/layout/geometry.d.ts +16 -0
  52. package/dist/types/components/layout/row.d.ts +7 -0
  53. package/dist/types/components/layout/split-pane.d.ts +8 -0
  54. package/dist/types/components/layout/stack.d.ts +6 -0
  55. package/dist/types/components/loader.d.ts +48 -0
  56. package/dist/types/components/markdown.d.ts +8 -0
  57. package/dist/types/components/metric.d.ts +7 -0
  58. package/dist/types/components/progress-bar.d.ts +3 -0
  59. package/dist/types/components/scroll-view.d.ts +6 -0
  60. package/dist/types/components/section.d.ts +3 -0
  61. package/dist/types/components/select-list.d.ts +33 -0
  62. package/dist/types/components/settings-list.d.ts +68 -0
  63. package/dist/types/components/spacer.d.ts +4 -0
  64. package/dist/types/components/tab-bar.d.ts +9 -0
  65. package/dist/types/components/table.d.ts +7 -0
  66. package/dist/types/components/text.d.ts +6 -0
  67. package/dist/types/components/tree-view.d.ts +9 -0
  68. package/dist/types/components/truncated-text.d.ts +3 -0
  69. package/dist/types/components/wizard-step.d.ts +6 -0
  70. package/dist/types/hotkeys-markdown.d.ts +24 -0
  71. package/dist/types/index.d.ts +5 -0
  72. package/dist/types/latex-block.d.ts +7 -0
  73. package/dist/types/native/apply.d.ts +39 -0
  74. package/dist/types/native/backend.d.ts +94 -0
  75. package/dist/types/native/blobs.d.ts +29 -0
  76. package/dist/types/native/describe.d.ts +56 -0
  77. package/dist/types/native/encode.d.ts +50 -0
  78. package/dist/types/native/icons.d.ts +18 -0
  79. package/dist/types/native/memo.d.ts +27 -0
  80. package/dist/types/native/node.d.ts +107 -0
  81. package/dist/types/native/overlay.d.ts +64 -0
  82. package/dist/types/native/picker.d.ts +124 -0
  83. package/dist/types/native/reconcile.d.ts +41 -0
  84. package/dist/types/native/settle.d.ts +11 -0
  85. package/dist/types/native/spans.d.ts +21 -0
  86. package/dist/types/native/state.d.ts +6 -0
  87. package/dist/types/native/tone.d.ts +24 -0
  88. package/dist/types/overlays/advisor-config.d.ts +20 -0
  89. package/dist/types/overlays/agent-hub-renderer.d.ts +27 -11
  90. package/dist/types/overlays/agent-hub.d.ts +10 -0
  91. package/dist/types/overlays/agent-transcript-viewer.d.ts +7 -0
  92. package/dist/types/overlays/agents-hub.d.ts +8 -0
  93. package/dist/types/overlays/annotation-overlay.d.ts +3 -0
  94. package/dist/types/overlays/ask-dialog.d.ts +23 -0
  95. package/dist/types/overlays/bordered-loader.d.ts +3 -0
  96. package/dist/types/overlays/btw-history-panel.d.ts +9 -0
  97. package/dist/types/overlays/btw-panel.d.ts +3 -0
  98. package/dist/types/overlays/cleanse-panel.d.ts +5 -2
  99. package/dist/types/overlays/composer-shape-preview.d.ts +7 -1
  100. package/dist/types/overlays/copy-selector.d.ts +38 -0
  101. package/dist/types/overlays/error-banner.d.ts +9 -0
  102. package/dist/types/overlays/extensions/extension-list.d.ts +1 -0
  103. package/dist/types/overlays/history-search.d.ts +18 -0
  104. package/dist/types/overlays/hook-editor.d.ts +28 -1
  105. package/dist/types/overlays/hook-input.d.ts +3 -0
  106. package/dist/types/overlays/hook-selector.d.ts +10 -0
  107. package/dist/types/overlays/hub-frame.d.ts +18 -0
  108. package/dist/types/overlays/jobs-panel.d.ts +24 -0
  109. package/dist/types/overlays/login-dialog.d.ts +10 -0
  110. package/dist/types/overlays/logout-account-selector.d.ts +4 -0
  111. package/dist/types/overlays/mcp-add-wizard.d.ts +4 -0
  112. package/dist/types/overlays/model-browser.d.ts +69 -0
  113. package/dist/types/overlays/model-hub.d.ts +7 -0
  114. package/dist/types/overlays/model-picker.d.ts +7 -0
  115. package/dist/types/overlays/move-overlay.d.ts +3 -0
  116. package/dist/types/overlays/oauth-selector.d.ts +4 -0
  117. package/dist/types/overlays/omfg-panel.d.ts +3 -0
  118. package/dist/types/overlays/pause-screen.d.ts +12 -0
  119. package/dist/types/overlays/plan-review-overlay.d.ts +5 -0
  120. package/dist/types/overlays/plan-save-overlay.d.ts +10 -0
  121. package/dist/types/overlays/plugin-selector.d.ts +4 -0
  122. package/dist/types/overlays/plugin-settings.d.ts +30 -0
  123. package/dist/types/overlays/queue-mode-selector.d.ts +4 -0
  124. package/dist/types/overlays/reset-usage-selector.d.ts +8 -0
  125. package/dist/types/overlays/rewind-selector.d.ts +5 -0
  126. package/dist/types/overlays/session-info-overlay.d.ts +19 -1
  127. package/dist/types/overlays/session-selector.d.ts +64 -1
  128. package/dist/types/overlays/settings-defs.d.ts +14 -0
  129. package/dist/types/overlays/settings-selector.d.ts +21 -0
  130. package/dist/types/overlays/show-images-selector.d.ts +4 -0
  131. package/dist/types/overlays/snapcompact-shape-preview.d.ts +2 -0
  132. package/dist/types/overlays/stats-notice.d.ts +8 -0
  133. package/dist/types/overlays/theme-selector.d.ts +11 -0
  134. package/dist/types/overlays/thinking-selector.d.ts +4 -0
  135. package/dist/types/overlays/tree-selector.d.ts +60 -2
  136. package/dist/types/overlays/usage-dashboard.d.ts +26 -3
  137. package/dist/types/overlays/usage-row.d.ts +36 -2
  138. package/dist/types/prompt/attachment-chips.d.ts +8 -0
  139. package/dist/types/prompt/composer-attachments.d.ts +15 -3
  140. package/dist/types/prompt/composer.d.ts +18 -1
  141. package/dist/types/prompt/custom-editor.d.ts +48 -2
  142. package/dist/types/prompt/editor-top-gap.d.ts +2 -0
  143. package/dist/types/prompt/macos-spelling.d.ts +6 -0
  144. package/dist/types/prompt/magic-keywords.d.ts +9 -0
  145. package/dist/types/prompt/queued-messages.d.ts +23 -0
  146. package/dist/types/prompt/skill-tokens.d.ts +4 -2
  147. package/dist/types/prompt/welcome.d.ts +16 -0
  148. package/dist/types/render/code-cell.d.ts +10 -0
  149. package/dist/types/render/file-list.d.ts +8 -0
  150. package/dist/types/render/hyperlink.d.ts +14 -0
  151. package/dist/types/render/output-block.d.ts +43 -0
  152. package/dist/types/render/output-pane.d.ts +17 -0
  153. package/dist/types/render/tool-card.d.ts +8 -0
  154. package/dist/types/render/tree-list.d.ts +8 -0
  155. package/dist/types/render/width-aware-text.d.ts +7 -0
  156. package/dist/types/setup/scenes/outro.d.ts +7 -0
  157. package/dist/types/setup/scenes/sign-in.d.ts +7 -0
  158. package/dist/types/setup/scenes/splash.d.ts +8 -0
  159. package/dist/types/setup/scenes/types.d.ts +6 -1
  160. package/dist/types/setup/wizard-overlay.d.ts +9 -0
  161. package/dist/types/status-line/component.d.ts +49 -5
  162. package/dist/types/status-line/context-usage.d.ts +20 -0
  163. package/dist/types/status-line/footer.d.ts +8 -0
  164. package/dist/types/status-line/metrics.d.ts +1 -2
  165. package/dist/types/status-line/segments.d.ts +10 -1
  166. package/dist/types/status-line/types.d.ts +40 -1
  167. package/dist/types/terminal-capabilities.d.ts +1 -1
  168. package/dist/types/terminal.d.ts +27 -0
  169. package/dist/types/theme/defaults/index.d.ts +100 -0
  170. package/dist/types/theme/schema.d.ts +2 -0
  171. package/dist/types/theme/shimmer.d.ts +8 -0
  172. package/dist/types/theme/theme-class.d.ts +2 -0
  173. package/dist/types/theme/theme.d.ts +33 -1
  174. package/dist/types/tools/ask.d.ts +3 -0
  175. package/dist/types/tools/ast-edit.d.ts +10 -1
  176. package/dist/types/tools/ast-grep.d.ts +10 -1
  177. package/dist/types/tools/autoresearch.d.ts +21 -4
  178. package/dist/types/tools/bash.d.ts +30 -1
  179. package/dist/types/tools/cfg-render.d.ts +5 -1
  180. package/dist/types/tools/debug.d.ts +3 -1
  181. package/dist/types/tools/default-renderer.d.ts +6 -1
  182. package/dist/types/tools/edit.d.ts +14 -1
  183. package/dist/types/tools/eval.d.ts +12 -1
  184. package/dist/types/tools/fetch.d.ts +16 -1
  185. package/dist/types/tools/find.d.ts +15 -1
  186. package/dist/types/tools/github.d.ts +3 -0
  187. package/dist/types/tools/glob.d.ts +14 -1
  188. package/dist/types/tools/goal.d.ts +3 -1
  189. package/dist/types/tools/grep.d.ts +14 -1
  190. package/dist/types/tools/grouped-file-output.d.ts +58 -0
  191. package/dist/types/tools/json-tree.d.ts +20 -0
  192. package/dist/types/tools/lsp.d.ts +3 -10
  193. package/dist/types/tools/mcp.d.ts +16 -1
  194. package/dist/types/tools/memory.d.ts +26 -0
  195. package/dist/types/tools/native-view.d.ts +90 -0
  196. package/dist/types/tools/proc-render.d.ts +7 -1
  197. package/dist/types/tools/read.d.ts +23 -1
  198. package/dist/types/tools/renderer.d.ts +57 -0
  199. package/dist/types/tools/report-tool-issue.d.ts +3 -0
  200. package/dist/types/tools/resolve.d.ts +14 -1
  201. package/dist/types/tools/task.d.ts +3 -0
  202. package/dist/types/tools/think.d.ts +3 -0
  203. package/dist/types/tools/todo.d.ts +12 -0
  204. package/dist/types/tools/vibe.d.ts +3 -1
  205. package/dist/types/tools/wait.d.ts +3 -0
  206. package/dist/types/tools/web-search.d.ts +8 -0
  207. package/dist/types/tools/write.d.ts +14 -1
  208. package/dist/types/tools/xdev.d.ts +13 -0
  209. package/dist/types/tui.d.ts +49 -0
  210. package/package.json +9 -9
  211. package/src/apps/autoresearch-dashboard.ts +362 -4
  212. package/src/apps/cleanse-board.ts +219 -1
  213. package/src/apps/cleanse-picker.ts +9 -1
  214. package/src/apps/debug/log-viewer.ts +15 -15
  215. package/src/apps/debug/protocol-probe.ts +2 -20
  216. package/src/apps/git/avatar.ts +2 -22
  217. package/src/apps/git/git-tui.ts +406 -59
  218. package/src/apps/git/help.ts +130 -0
  219. package/src/apps/git/sidebar.ts +491 -0
  220. package/src/apps/live-visualizer.ts +65 -0
  221. package/src/apps/ps-top.ts +448 -17
  222. package/src/apps/session-picker.ts +1 -0
  223. package/src/apps/setup-model-picker.ts +5 -1
  224. package/src/apps/standalone-picker.ts +55 -1
  225. package/src/autocomplete.ts +44 -1
  226. package/src/chat/advisor-message.ts +72 -3
  227. package/src/chat/assistant-message.ts +346 -11
  228. package/src/chat/background-tan-message.ts +4 -4
  229. package/src/chat/bash-execution.ts +81 -0
  230. package/src/chat/cache-invalidation-marker.ts +8 -0
  231. package/src/chat/chat-transcript-builder.ts +37 -6
  232. package/src/chat/collab-prompt-message.ts +15 -3
  233. package/src/chat/compaction-summary-message.ts +74 -1
  234. package/src/chat/custom-message.ts +3 -1
  235. package/src/chat/eval-execution.ts +57 -0
  236. package/src/chat/execution-shared.ts +153 -1
  237. package/src/chat/hook-message.ts +1 -0
  238. package/src/chat/late-diagnostics-message.ts +85 -2
  239. package/src/chat/read-tool-group.ts +341 -1
  240. package/src/chat/served-model-marker.ts +8 -0
  241. package/src/chat/skill-message.ts +63 -0
  242. package/src/chat/stripped-tool-calls-placeholder.ts +18 -10
  243. package/src/chat/todo-reminder.ts +34 -4
  244. package/src/chat/tool-execution.ts +385 -10
  245. package/src/chat/transcript-actions.ts +38 -0
  246. package/src/chat/transcript-browser.ts +2 -9
  247. package/src/chat/transcript-entry.ts +10 -1
  248. package/src/chat/transcript-render-helpers.ts +16 -15
  249. package/src/chat/ttsr-notification.ts +38 -1
  250. package/src/chat/user-message.ts +167 -4
  251. package/src/chrome/collab-qrcode.ts +27 -2
  252. package/src/chrome/context-thresholds.ts +34 -0
  253. package/src/chrome/countdown-timer.ts +24 -1
  254. package/src/chrome/diff.ts +60 -1
  255. package/src/chrome/dynamic-border.ts +7 -0
  256. package/src/chrome/format.ts +12 -0
  257. package/src/chrome/message-divider.ts +33 -1
  258. package/src/chrome/message-frame.ts +53 -10
  259. package/src/chrome/message-notice.ts +71 -0
  260. package/src/chrome/overlay-box.ts +45 -2
  261. package/src/chrome/qrcode.ts +28 -0
  262. package/src/chrome/segment-track.ts +26 -0
  263. package/src/chrome/selector-helpers.ts +1 -17
  264. package/src/chrome/status-notice.ts +46 -0
  265. package/src/chrome/streaming-panel.ts +25 -1
  266. package/src/chrome/tool-activity.ts +20 -0
  267. package/src/chrome/transcript-container.ts +37 -0
  268. package/src/chrome/transcript-status.ts +40 -11
  269. package/src/components/box.ts +51 -0
  270. package/src/components/disclosure.ts +33 -0
  271. package/src/components/editor.ts +216 -27
  272. package/src/components/form.ts +93 -9
  273. package/src/components/image.ts +30 -0
  274. package/src/components/input.ts +77 -26
  275. package/src/components/key-value-list.ts +18 -0
  276. package/src/components/layout/geometry.ts +44 -0
  277. package/src/components/layout/row.ts +30 -0
  278. package/src/components/layout/split-pane.ts +65 -0
  279. package/src/components/layout/stack.ts +27 -0
  280. package/src/components/loader.ts +183 -1
  281. package/src/components/markdown.ts +21 -0
  282. package/src/components/metric.ts +39 -0
  283. package/src/components/progress-bar.ts +28 -0
  284. package/src/components/scroll-view.ts +23 -0
  285. package/src/components/section.ts +16 -0
  286. package/src/components/select-list.ts +174 -25
  287. package/src/components/settings-list.ts +264 -7
  288. package/src/components/spacer.ts +17 -0
  289. package/src/components/tab-bar.ts +42 -0
  290. package/src/components/table.ts +34 -0
  291. package/src/components/text.ts +23 -0
  292. package/src/components/tree-view.ts +65 -0
  293. package/src/components/truncated-text.ts +14 -0
  294. package/src/components/wizard-step.ts +18 -9
  295. package/src/debug-server.ts +14 -7
  296. package/src/hotkeys-markdown.ts +251 -69
  297. package/src/index.ts +6 -0
  298. package/src/latex-block.ts +11 -0
  299. package/src/native/apply.ts +275 -0
  300. package/src/native/backend.ts +682 -0
  301. package/src/native/blobs.ts +70 -0
  302. package/src/native/describe.ts +124 -0
  303. package/src/native/encode.ts +233 -0
  304. package/src/native/icons.ts +184 -0
  305. package/src/native/memo.ts +64 -0
  306. package/src/native/node.ts +112 -0
  307. package/src/native/overlay.ts +131 -0
  308. package/src/native/picker.ts +333 -0
  309. package/src/native/reconcile.ts +1063 -0
  310. package/src/native/settle.ts +23 -0
  311. package/src/native/spans.ts +308 -0
  312. package/src/native/state.ts +28 -0
  313. package/src/native/tone.ts +79 -0
  314. package/src/overlays/advisor-config.ts +420 -0
  315. package/src/overlays/agent-hub-renderer.ts +155 -18
  316. package/src/overlays/agent-hub.ts +1037 -44
  317. package/src/overlays/agent-transcript-viewer.ts +129 -0
  318. package/src/overlays/agents-hub.ts +688 -53
  319. package/src/overlays/annotation-overlay.ts +391 -52
  320. package/src/overlays/ask-dialog.ts +435 -24
  321. package/src/overlays/bordered-loader.ts +17 -0
  322. package/src/overlays/btw-history-panel.ts +182 -0
  323. package/src/overlays/btw-panel.ts +68 -2
  324. package/src/overlays/cleanse-panel.ts +105 -15
  325. package/src/overlays/codex-reset-fireworks.ts +65 -5
  326. package/src/overlays/composer-shape-preview.ts +26 -1
  327. package/src/overlays/copy-selector.ts +545 -23
  328. package/src/overlays/error-banner.ts +44 -0
  329. package/src/overlays/extensions/extension-list.ts +23 -17
  330. package/src/overlays/history-search.ts +175 -33
  331. package/src/overlays/hook-editor.ts +113 -6
  332. package/src/overlays/hook-input.ts +22 -1
  333. package/src/overlays/hook-selector.ts +311 -22
  334. package/src/overlays/hub-frame.ts +80 -0
  335. package/src/overlays/jobs-panel.ts +112 -0
  336. package/src/overlays/login-dialog.ts +183 -1
  337. package/src/overlays/logout-account-selector.ts +102 -4
  338. package/src/overlays/mcp-add-wizard.ts +232 -53
  339. package/src/overlays/model-browser.ts +681 -22
  340. package/src/overlays/model-hub.ts +1402 -78
  341. package/src/overlays/model-picker.ts +175 -1
  342. package/src/overlays/move-overlay.ts +45 -0
  343. package/src/overlays/oauth-selector.ts +227 -23
  344. package/src/overlays/omfg-panel.ts +86 -2
  345. package/src/overlays/pause-screen.ts +55 -4
  346. package/src/overlays/plan-review-overlay.ts +352 -0
  347. package/src/overlays/plan-save-overlay.ts +45 -0
  348. package/src/overlays/plugin-selector.ts +21 -1
  349. package/src/overlays/plugin-settings.ts +195 -5
  350. package/src/overlays/queue-mode-selector.ts +19 -1
  351. package/src/overlays/reset-usage-selector.ts +110 -40
  352. package/src/overlays/rewind-selector.ts +458 -44
  353. package/src/overlays/session-account-selector.ts +1 -1
  354. package/src/overlays/session-info-overlay.ts +137 -1
  355. package/src/overlays/session-selector.ts +807 -46
  356. package/src/overlays/settings-defs.ts +41 -0
  357. package/src/overlays/settings-selector.ts +583 -23
  358. package/src/overlays/show-images-selector.ts +18 -1
  359. package/src/overlays/snapcompact-shape-preview.ts +48 -3
  360. package/src/overlays/stats-notice.ts +31 -0
  361. package/src/overlays/theme-selector.ts +31 -1
  362. package/src/overlays/thinking-selector.ts +22 -1
  363. package/src/overlays/tree-selector.ts +570 -110
  364. package/src/overlays/usage-dashboard.ts +587 -12
  365. package/src/overlays/usage-row.ts +161 -25
  366. package/src/prompt/attachment-chips.ts +60 -0
  367. package/src/prompt/composer-attachments.ts +31 -9
  368. package/src/prompt/composer.ts +55 -2
  369. package/src/prompt/custom-editor.ts +302 -4
  370. package/src/prompt/editor-top-gap.ts +8 -0
  371. package/src/prompt/macos-spelling.ts +24 -9
  372. package/src/prompt/magic-keywords.ts +22 -1
  373. package/src/prompt/model-mention-autocomplete.ts +1 -0
  374. package/src/prompt/queued-messages.ts +101 -0
  375. package/src/prompt/skill-tokens.ts +5 -3
  376. package/src/prompt/welcome.ts +213 -6
  377. package/src/render/code-cell.ts +103 -2
  378. package/src/render/file-list.ts +29 -0
  379. package/src/render/hyperlink.ts +32 -0
  380. package/src/render/output-block.ts +110 -0
  381. package/src/render/output-pane.ts +37 -0
  382. package/src/render/tool-card.ts +137 -0
  383. package/src/render/tree-list.ts +39 -0
  384. package/src/render/width-aware-text.ts +20 -0
  385. package/src/setup/scenes/composer.ts +29 -0
  386. package/src/setup/scenes/glyph.ts +15 -0
  387. package/src/setup/scenes/model.ts +36 -8
  388. package/src/setup/scenes/outro.ts +28 -1
  389. package/src/setup/scenes/sign-in.ts +122 -13
  390. package/src/setup/scenes/splash.ts +28 -1
  391. package/src/setup/scenes/theme.ts +88 -3
  392. package/src/setup/scenes/types.ts +7 -1
  393. package/src/setup/startup-splash.ts +46 -9
  394. package/src/setup/wizard-overlay.ts +140 -23
  395. package/src/status-line/component.ts +398 -16
  396. package/src/status-line/context-usage.ts +279 -50
  397. package/src/status-line/footer.ts +94 -1
  398. package/src/status-line/metrics.ts +3 -6
  399. package/src/status-line/segments.ts +463 -97
  400. package/src/status-line/separators.ts +3 -16
  401. package/src/status-line/types.ts +43 -1
  402. package/src/terminal-capabilities.ts +7 -0
  403. package/src/terminal.ts +130 -2
  404. package/src/theme/defaults/dark-neon-noir.json +100 -0
  405. package/src/theme/defaults/index.ts +2 -0
  406. package/src/theme/defaults/light-prism.json +5 -5
  407. package/src/theme/defaults/onyx.json +62 -62
  408. package/src/theme/schema.ts +15 -0
  409. package/src/theme/shimmer.ts +33 -0
  410. package/src/theme/theme-class.ts +24 -19
  411. package/src/theme/theme.ts +111 -9
  412. package/src/tools/ask.ts +157 -1
  413. package/src/tools/ast-edit.ts +174 -1
  414. package/src/tools/ast-grep.ts +82 -2
  415. package/src/tools/autoresearch.ts +149 -1
  416. package/src/tools/bash.ts +180 -49
  417. package/src/tools/cfg-render.ts +66 -1
  418. package/src/tools/debug.ts +78 -1
  419. package/src/tools/default-renderer.ts +35 -1
  420. package/src/tools/edit.ts +247 -2
  421. package/src/tools/eval.ts +257 -1
  422. package/src/tools/fetch.ts +102 -3
  423. package/src/tools/find.ts +127 -1
  424. package/src/tools/github.ts +119 -10
  425. package/src/tools/glob.ts +116 -1
  426. package/src/tools/goal.ts +56 -1
  427. package/src/tools/grep.ts +83 -2
  428. package/src/tools/grouped-file-output.ts +205 -0
  429. package/src/tools/json-tree.ts +116 -1
  430. package/src/tools/lsp.ts +318 -1
  431. package/src/tools/mcp.ts +80 -1
  432. package/src/tools/memory.ts +93 -11
  433. package/src/tools/native-view.ts +292 -0
  434. package/src/tools/proc-render.ts +221 -1
  435. package/src/tools/read.ts +199 -4
  436. package/src/tools/renderer.ts +61 -0
  437. package/src/tools/report-tool-issue.ts +7 -1
  438. package/src/tools/resolve.ts +63 -1
  439. package/src/tools/task.ts +406 -4
  440. package/src/tools/think.ts +19 -1
  441. package/src/tools/todo.ts +89 -0
  442. package/src/tools/vibe.ts +230 -2
  443. package/src/tools/wait.ts +181 -1
  444. package/src/tools/web-search.ts +113 -1
  445. package/src/tools/write.ts +144 -5
  446. package/src/tools/xdev.ts +64 -1
  447. package/src/tui.ts +294 -3
@@ -20,7 +20,12 @@ import { matchesKey } from "../keys";
20
20
  import type { SgrMouseEvent } from "../mouse";
21
21
  import { replaceTabs, truncateToWidth, visibleWidth } from "../utils";
22
22
  import { formatNumber, sanitizeText } from "@oh-my-pi/pi-utils";
23
- import { type ConfiguredThinkingLevel, parseConfiguredThinkingLevel } from "../thinking";
23
+ import {
24
+ AUTO_THINKING,
25
+ type ConfiguredThinkingLevel,
26
+ getConfiguredThinkingLevelMetadata,
27
+ parseConfiguredThinkingLevel,
28
+ } from "../thinking";
24
29
  import { thinkingLevelGlyph } from "../render/render-utils";
25
30
  import { type ThemeColor, theme } from "../theme/theme";
26
31
  import {
@@ -32,6 +37,12 @@ import {
32
37
  } from "../keybinding-matchers";
33
38
  import { MenuSelection } from "../components/menu-selection";
34
39
  import { clampScrollOffset, scrollOffsetForRow } from "../components/scroll-viewport";
40
+ import type { TspPickerColumn, TspPickerGroup, TspPickerItem, TspSpan, TspText } from "@oh-my-pi/pi-wire";
41
+ import { col, md, node, row, span, text } from "../native/describe";
42
+ import { pickerFuzzyHits } from "../native/picker";
43
+ import type { NativeChild, NativeNode, NativeUiEvent } from "../native/node";
44
+ import { sameItems } from "../native/memo";
45
+ import { plainText } from "../native/spans";
35
46
 
36
47
  /** Canonical display ordering of built-in model roles. */
37
48
  export type ModelRole =
@@ -391,15 +402,6 @@ export function buildSessionModelScope(
391
402
  return scopeFromInputs(settings, readSessionModelScopeInputs(registry, scopedModels));
392
403
  }
393
404
 
394
- function sameEntries<T>(a: ReadonlyArray<T>, b: ReadonlyArray<T>): boolean {
395
- if (a === b) return true;
396
- if (a.length !== b.length) return false;
397
- for (let i = 0; i < a.length; i++) {
398
- if (a[i] !== b[i]) return false;
399
- }
400
- return true;
401
- }
402
-
403
405
  /**
404
406
  * {@link buildSessionModelScope} for per-keystroke callers: returns the same
405
407
  * scope until the source revision, MRU order, scoped models, or the registry's
@@ -427,9 +429,9 @@ export class SessionModelScopeCache {
427
429
  cachedInputs &&
428
430
  revision === this.#revision &&
429
431
  inputs.error === cachedInputs.error &&
430
- sameEntries(inputs.models, cachedInputs.models) &&
431
- sameEntries(inputs.allModels, cachedInputs.allModels) &&
432
- sameEntries(this.#settings.mruOrder, cached.mruOrder)
432
+ sameItems(inputs.models, cachedInputs.models) &&
433
+ sameItems(inputs.allModels, cachedInputs.allModels) &&
434
+ sameItems(this.#settings.mruOrder, cached.mruOrder)
433
435
  ) {
434
436
  return cached;
435
437
  }
@@ -605,26 +607,66 @@ export function formatRoleChip(role: string, assignment: RoleAssignment, setting
605
607
  return theme.fg(info.color ?? "muted", `${theme.status.enabled} ${label}`) + suffix;
606
608
  }
607
609
 
610
+ /** {@link formatRoleChip} as styled spans for a described node. */
611
+ function roleChipSpans(role: string, assignment: RoleAssignment, settings: ModelBrowserSource): TspSpan[] {
612
+ const info = settings.getRoleInfo(role);
613
+ const label = (info.tag ?? info.name ?? role).toLowerCase();
614
+ const glyph = thinkingLevelGlyph(assignment.thinkingLevel, theme);
615
+ const spans = assignment.autoSelected
616
+ ? [span(`${theme.status.shadowed} ${label}`, "dim")]
617
+ : [span(`${theme.status.enabled} ${label}`, info.color ?? "muted")];
618
+ if (glyph) spans.push(span(` ${glyph}`, "dim"));
619
+ return spans;
620
+ }
621
+
608
622
  /** Both token legs at zero cost — the condition {@link formatCostPair} renders as `free`. */
609
623
  function isFreeModel(model: Model): boolean {
610
624
  const cost = model.cost;
611
625
  return !cost || (cost.input === 0 && cost.output === 0);
612
626
  }
613
627
 
628
+ /** One per-million price leg: `3`, `0.25`, `12.5`; `?` when unknown. */
629
+ function formatCostLeg(n: number): string {
630
+ if (!Number.isFinite(n) || n < 0) return "?";
631
+ if (n > 0 && n < 0.01) {
632
+ return n.toLocaleString("en-US", { useGrouping: false, maximumFractionDigits: 20 });
633
+ }
634
+ const s = n >= 100 ? String(Math.round(n)) : n >= 10 ? n.toFixed(1) : n.toFixed(2);
635
+ return s.includes(".") ? s.replace(/\.?0+$/, "") : s;
636
+ }
637
+
614
638
  /** `$in/out` per-million cost pair; `free` when both legs are zero. */
615
639
  function formatCostPair(model: Model): string {
616
640
  if (isFreeModel(model)) return "free";
641
+ return `$${formatCostLeg(model.cost.input)}/${formatCostLeg(model.cost.output)}`;
642
+ }
643
+
644
+ /** Fact columns of a model picker (Stencil `NATIVE_REDESIGN.md` §4.7); the lowest priority hides first. */
645
+ export const MODEL_PICKER_COLUMNS: readonly TspPickerColumn[] = [
646
+ { id: "int", head: "Int", format: "num", priority: 1 },
647
+ { id: "speed", head: "t/s", format: "num", priority: 2 },
648
+ { id: "ctx", head: "Ctx", format: "num", priority: 4 },
649
+ { id: "price", head: "$/M", format: "price", priority: 3 },
650
+ ];
651
+
652
+ /** `$3·15` price fact of a picker row (`free` at zero cost). */
653
+ function pickerPrice(model: Model): string {
654
+ if (isFreeModel(model)) return "free";
655
+ return `$${formatCostLeg(model.cost.input)}·${formatCostLeg(model.cost.output)}`;
656
+ }
657
+
658
+ /** `$2 in · $10 out · $0.2 cache` for a model preview. */
659
+ function previewPrice(model: Model): string {
617
660
  const cost = model.cost;
661
+ const parts = [`$${formatCostLeg(cost.input)} in`, `$${formatCostLeg(cost.output)} out`];
662
+ if (cost.cacheRead > 0) parts.push(`$${formatCostLeg(cost.cacheRead)} cache`);
663
+ return parts.join(" · ");
664
+ }
618
665
 
619
- const fmt = (n: number): string => {
620
- if (!Number.isFinite(n) || n < 0) return "?";
621
- if (n > 0 && n < 0.01) {
622
- return n.toLocaleString("en-US", { useGrouping: false, maximumFractionDigits: 20 });
623
- }
624
- const s = n >= 100 ? String(Math.round(n)) : n >= 10 ? n.toFixed(1) : n.toFixed(2);
625
- return s.includes(".") ? s.replace(/\.?0+$/, "") : s;
626
- };
627
- return `$${fmt(cost.input)}/${fmt(cost.output)}`;
666
+ /** The omp theme token of a thinking level's dot (`thinkingHigh`); none for inherit and auto. */
667
+ export function thinkingDotToken(level: ConfiguredThinkingLevel): string | undefined {
668
+ if (level === ThinkingLevel.Inherit || level === AUTO_THINKING) return undefined;
669
+ return `thinking${level.charAt(0).toUpperCase()}${level.slice(1)}`;
628
670
  }
629
671
 
630
672
  /**
@@ -684,6 +726,33 @@ function padLeftVisible(text: string, width: number): string {
684
726
  return missing > 0 ? " ".repeat(missing) + text : text;
685
727
  }
686
728
 
729
+ /** A model browser's visible rows as picker `order` plus the query's hit ranges. */
730
+ export interface ModelPickerOrder {
731
+ readonly order: readonly (string | TspPickerGroup)[];
732
+ readonly hits: Readonly<Record<string, readonly (readonly [number, number])[]>> | undefined;
733
+ /** Selectable rows in `order`. */
734
+ readonly count: number;
735
+ }
736
+
737
+ /**
738
+ * A model picker's catalogue props: `items` as last sent whole, plus the
739
+ * rows changed or added since (`itemsAdd`) and the ids gone (`itemsDel`), so
740
+ * a discovery refresh that touches a few rows does not resend a thousand.
741
+ */
742
+ export interface ModelPickerCatalogue {
743
+ readonly items: readonly TspPickerItem[];
744
+ readonly itemsAdd?: readonly TspPickerItem[];
745
+ readonly itemsDel?: readonly string[];
746
+ }
747
+
748
+ /** How {@link ModelBrowser.pickerOrder} heads its rows. */
749
+ export interface ModelPickerGrouping {
750
+ /** One group per provider after the Recent block (the unfiltered All models view). */
751
+ readonly providers: boolean;
752
+ /** Label of the rows after the Recent block when they are not split by provider; null drops every head. */
753
+ readonly rest: string | null;
754
+ }
755
+
687
756
  /** Behavior switches for {@link ModelBrowser}. */
688
757
  export interface ModelBrowserOptions {
689
758
  /** Render the dim `provider/` prefix before model ids. Default true. */
@@ -740,6 +809,54 @@ export class ModelBrowser implements Component {
740
809
  #focused = true;
741
810
  /** `provider/id` of the session's active model; marked in rows and detail. */
742
811
  #currentSelector: string | undefined;
812
+ /**
813
+ * Bumped whenever a per-row or detail input (provider prefix, current mark,
814
+ * over-context flagging, perf) changes; described nodes rebuild on a new epoch.
815
+ */
816
+ #nativeEpoch = 0;
817
+ /** Described item nodes by selector, valid for {@link #nativeEpoch} and the same item object. */
818
+ #itemNodes = new Map<string, { item: ModelBrowserItem; node: NativeNode }>();
819
+ #nativeSearch: NativeNode | undefined;
820
+ #nativeList: { items: readonly ModelBrowserItem[]; epoch: number; children: NativeChild[] } | undefined;
821
+ #nativeListNode:
822
+ | { children: NativeChild[]; selected: string | null; filter: string; empty: string; node: NativeNode }
823
+ | undefined;
824
+ #nativeDetail:
825
+ | { item: ModelBrowserItem | undefined; epoch: number; roles: RoleAssignments; node: NativeNode }
826
+ | undefined;
827
+ #nativeRoot: { list: NativeNode; detail: NativeNode; node: NativeNode } | undefined;
828
+ /** Bumped when a picker row input other than roles changes (perf, over-context flagging). */
829
+ #pickerEpoch = 0;
830
+ /** Picker rows by selector, reused while the model and the roles it holds are unchanged. */
831
+ #pickerItemCache = new Map<
832
+ string,
833
+ { model: Model; label: string; held: string; epoch: number; value: TspPickerItem }
834
+ >();
835
+ #pickerItems:
836
+ | {
837
+ catalogue: readonly ModelBrowserItem[];
838
+ epoch: number;
839
+ roles: RoleAssignments;
840
+ value: ModelPickerCatalogue;
841
+ }
842
+ | undefined;
843
+ /** The rows last sent as a whole `items`; later catalogues patch it. */
844
+ #pickerBase: readonly TspPickerItem[] | undefined;
845
+ /**
846
+ * Ids patched since {@link #pickerBase} went out. The terminal keeps
847
+ * applied patches, so a row that returns to its base value is still sent.
848
+ */
849
+ #pickerPatched = new Set<string>();
850
+ #pickerOrder: { visible: readonly ModelBrowserItem[]; key: string; value: ModelPickerOrder } | undefined;
851
+ #pickerPreview:
852
+ | {
853
+ item: ModelBrowserItem | undefined;
854
+ epoch: number;
855
+ roles: RoleAssignments;
856
+ key: string;
857
+ children: readonly NativeChild[];
858
+ }
859
+ | undefined;
743
860
 
744
861
  /** Enter or click-on-selected. */
745
862
  onActivate?: (item: ModelBrowserItem) => void;
@@ -760,6 +877,7 @@ export class ModelBrowser implements Component {
760
877
 
761
878
  /** Mark `selector` as the session's active model (undefined clears the mark). */
762
879
  setCurrentSelector(selector: string | undefined): void {
880
+ if (selector !== this.#currentSelector) this.#bumpNativeEpoch();
763
881
  this.#currentSelector = selector;
764
882
  }
765
883
 
@@ -789,6 +907,7 @@ export class ModelBrowser implements Component {
789
907
 
790
908
  /** Measured TPS/TTFT averages keyed by `provider/id` selector (see AgentStorage.getModelPerf). */
791
909
  setPerfStats(perf: ReadonlyMap<string, ModelBrowserPerf>): void {
910
+ if (perf !== this.#perf) this.#bumpPickerEpoch();
792
911
  this.#perf = perf;
793
912
  }
794
913
 
@@ -799,6 +918,7 @@ export class ModelBrowser implements Component {
799
918
  }
800
919
 
801
920
  setShowProvider(show: boolean): void {
921
+ if (show !== this.#showProvider) this.#bumpNativeEpoch();
802
922
  this.#showProvider = show;
803
923
  }
804
924
  /** Keep the source order after fuzzy filtering instead of applying model-specific ranking. */
@@ -807,6 +927,7 @@ export class ModelBrowser implements Component {
807
927
  }
808
928
  /** Allow hosts to toggle context-window flagging between browser modes. */
809
929
  setMarkOverContext(mark: boolean): void {
930
+ if (mark !== this.#markOverContext) this.#bumpPickerEpoch();
810
931
  this.#markOverContext = mark;
811
932
  }
812
933
  /** Focused: accent cursor + selected-row background band. Unfocused: dim cursor, no band. */
@@ -823,6 +944,11 @@ export class ModelBrowser implements Component {
823
944
  return this.#searchInput.getValue();
824
945
  }
825
946
 
947
+ /** Caret into {@link query} (UTF-16 offset), for native picker `cursor`. */
948
+ get cursor(): number {
949
+ return this.#searchInput.getCursor();
950
+ }
951
+
826
952
  setQuery(query: string): void {
827
953
  this.#searchInput.setValue(query);
828
954
  this.#applyQuery("reset-changed-prefix");
@@ -1295,4 +1421,537 @@ export class ModelBrowser implements Component {
1295
1421
  }
1296
1422
 
1297
1423
  invalidate(): void {}
1424
+
1425
+ #bumpNativeEpoch(): void {
1426
+ this.#nativeEpoch++;
1427
+ this.#itemNodes.clear();
1428
+ }
1429
+
1430
+ /** Perf and over-context flags feed both the generic rows and the picker rows. */
1431
+ #bumpPickerEpoch(): void {
1432
+ this.#bumpNativeEpoch();
1433
+ // Rows are rebuilt on the next read; equal ones keep their identity (see #pickerItem).
1434
+ this.#pickerEpoch++;
1435
+ }
1436
+
1437
+ /**
1438
+ * `col[search row, list, detail]`: the query field (the embedded `Input`),
1439
+ * every visible model as a keyed `item` (the terminal virtualizes and
1440
+ * scrolls), and the selection's facts and role chips.
1441
+ */
1442
+ describe(): NativeNode {
1443
+ this.#nativeSearch ??= row([text([span(theme.symbol("icon.search"), "accent")]), this.#searchInput], {
1444
+ gap: "sm",
1445
+ align: "center",
1446
+ });
1447
+ const list = this.#describeList();
1448
+ const detail = this.#describeDetail();
1449
+ const root = this.#nativeRoot;
1450
+ if (root?.list === list && root.detail === detail) return root.node;
1451
+ const node = col([this.#nativeSearch, list, detail], { gap: "sm" });
1452
+ this.#nativeRoot = { list, detail, node };
1453
+ return node;
1454
+ }
1455
+
1456
+ /** List `select`/`activate` on a model = highlight it, then Enter. */
1457
+ handleNativeEvent(event: NativeUiEvent): void {
1458
+ if ((event.type !== "select" && event.type !== "activate") || event.key !== "list") return;
1459
+ const index = this.#menu.visibleItems.findIndex(item => item.selector === event.item);
1460
+ const item = this.#menu.visibleItems[index];
1461
+ if (!item || this.#isDisabled(item)) return;
1462
+ if (this.#menu.setSelectedIndex(index)) {
1463
+ this.#ensureSelectedVisible();
1464
+ this.onSelectionChange?.(this.getSelected());
1465
+ }
1466
+ this.onActivate?.(item);
1467
+ }
1468
+
1469
+ #describeList(): NativeNode {
1470
+ const items = this.#menu.visibleItems;
1471
+ let cached = this.#nativeList;
1472
+ if (cached?.items !== items || cached.epoch !== this.#nativeEpoch) {
1473
+ cached = { items, epoch: this.#nativeEpoch, children: items.map(item => this.#describeItem(item)) };
1474
+ this.#nativeList = cached;
1475
+ }
1476
+ const selected = this.getSelected()?.selector ?? null;
1477
+ const filter = this.query.trim();
1478
+ const empty =
1479
+ items.length > 0
1480
+ ? ""
1481
+ : plainText(this.#emptyText?.() ?? "").trim() ||
1482
+ (filter ? "No matching models" : "No models available in this scope");
1483
+ const prev = this.#nativeListNode;
1484
+ if (
1485
+ prev?.children === cached.children &&
1486
+ prev.selected === selected &&
1487
+ prev.filter === filter &&
1488
+ prev.empty === empty
1489
+ ) {
1490
+ return prev.node;
1491
+ }
1492
+ const listNode = node(
1493
+ "list",
1494
+ {
1495
+ role: "omp.model-browser.list",
1496
+ selected,
1497
+ filter: filter || undefined,
1498
+ empty: empty ? [span(empty, "muted")] : undefined,
1499
+ virtual: true,
1500
+ grow: 1,
1501
+ },
1502
+ cached.children,
1503
+ "list",
1504
+ );
1505
+ this.#nativeListNode = { children: cached.children, selected, filter, empty, node: listNode };
1506
+ return listNode;
1507
+ }
1508
+
1509
+ #describeItem(item: ModelBrowserItem): NativeNode {
1510
+ if (item.id === "separator") return node("rule", undefined, undefined, "separator");
1511
+ const cached = this.#itemNodes.get(item.selector);
1512
+ if (cached?.item === item) return cached.node;
1513
+
1514
+ const label: TspSpan[] = [];
1515
+ if (this.#showProvider) label.push(span(`${item.provider}/`, "dim"));
1516
+ label.push(span(item.id, item.labelColor));
1517
+ if (item.selector === this.#currentSelector) label.push(span(` ${theme.status.enabled}`, "success"));
1518
+ const metrics = [
1519
+ formatIntelligence(item.model),
1520
+ this.#perfCell(item, "full"),
1521
+ formatContext(item.model),
1522
+ formatCostPair(item.model),
1523
+ ].filter(Boolean);
1524
+ const overContext = this.isOverContext(item);
1525
+ const itemNode = node(
1526
+ "item",
1527
+ {
1528
+ label,
1529
+ value: [span(metrics.join(" "), "dim")],
1530
+ detail: overContext
1531
+ ? [
1532
+ span(
1533
+ `${theme.status.disabled} context>${formatNumber(item.model.contextWindow ?? 0).toLowerCase()}`,
1534
+ "warning",
1535
+ ),
1536
+ ]
1537
+ : undefined,
1538
+ tone: overContext ? "muted" : undefined,
1539
+ },
1540
+ undefined,
1541
+ item.selector,
1542
+ );
1543
+ this.#itemNodes.set(item.selector, { item, node: itemNode });
1544
+ return itemNode;
1545
+ }
1546
+
1547
+ /** Facts, upstream badges, and the over-context warning or role chips for the selection. */
1548
+ #describeDetail(): NativeNode {
1549
+ const selected = this.getSelected();
1550
+ const prev = this.#nativeDetail;
1551
+ if (prev && prev.item === selected && prev.epoch === this.#nativeEpoch && prev.roles === this.#roles) {
1552
+ return prev.node;
1553
+ }
1554
+
1555
+ const children: NativeChild[] = [];
1556
+ if (selected) {
1557
+ const model = selected.model;
1558
+ const head: NativeChild[] = [text([span(model.name, "strong")])];
1559
+ if (model.isNew) head.push(node("badge", { text: "new", tone: "accent" }));
1560
+ if (model.isBeta) head.push(node("badge", { text: "beta", tone: "warning" }));
1561
+ if (model.isRecommended) head.push(node("badge", { text: "recommended", tone: "success" }));
1562
+ children.push(row(head, { gap: "sm", align: "center", wrap: true }));
1563
+
1564
+ const facts: string[] = [];
1565
+ if (model.contextWindow) facts.push(`${formatNumber(model.contextWindow).toLowerCase()} ctx`);
1566
+ if (model.maxTokens) facts.push(`${formatNumber(model.maxTokens).toLowerCase()} out`);
1567
+ facts.push(`${formatCostPair(model)} per M`);
1568
+ if (model.reasoning) facts.push("reasoning");
1569
+ if (model.input.includes("image")) facts.push("vision");
1570
+ const intelligence = formatIntelligence(model);
1571
+ if (intelligence) facts.push(intelligence);
1572
+ const perf = this.#perf.get(selected.selector);
1573
+ if (perf) {
1574
+ facts.push(`~${formatTps(perf.tps)}`);
1575
+ if (perf.ttftMs !== null) facts.push(`${formatTtft(perf.ttftMs)} ttft`);
1576
+ } else if (model.tps != null && Number.isFinite(model.tps) && model.tps > 0) {
1577
+ facts.push(`~${formatTps(model.tps)}`);
1578
+ }
1579
+ children.push(text([span(facts.join(" · "), "muted")], { wrap: "word" }));
1580
+ const description = model.description ? formatDescription(model.description) : "";
1581
+ if (description) children.push(text([span(description, "dim")], { wrap: "word", lines: 2 }));
1582
+
1583
+ if (this.isOverContext(selected)) {
1584
+ const warning = `${theme.status.disabled} context ${formatNumber(this.#currentContextTokens).toLowerCase()} exceeds ${formatNumber(model.contextWindow ?? 0).toLowerCase()} limit · compacts with current model, then switches`;
1585
+ children.push(text([span(warning, "warning")], { wrap: "word" }));
1586
+ } else {
1587
+ const chips: TspSpan[] = [];
1588
+ if (selected.selector === this.#currentSelector) {
1589
+ chips.push(span(`${theme.status.enabled} current`, "success"));
1590
+ }
1591
+ const seen = new Set<string>();
1592
+ const pushRole = (role: string) => {
1593
+ if (seen.has(role)) return;
1594
+ seen.add(role);
1595
+ const assignment = this.#roles[role];
1596
+ if (!assignment || !modelsAreEqual(assignment.model, model)) return;
1597
+ if (this.#settings.getRoleInfo(role).hidden) return;
1598
+ if (chips.length > 0) chips.push(span(" · ", "dim"));
1599
+ chips.push(...roleChipSpans(role, assignment, this.#settings));
1600
+ };
1601
+ for (const role of MODEL_ROLE_IDS) pushRole(role);
1602
+ for (const role in this.#roles) pushRole(role);
1603
+ if (chips.length > 0) children.push(text(chips, { wrap: "word" }));
1604
+ }
1605
+ }
1606
+ const detailNode = node("col", { role: "omp.model-browser.detail", gap: "none" }, children, "detail");
1607
+ this.#nativeDetail = { item: selected, epoch: this.#nativeEpoch, roles: this.#roles, node: detailNode };
1608
+ return detailNode;
1609
+ }
1610
+
1611
+ // ─── Picker (data-first `picker` kind) ────────────────────────────────
1612
+
1613
+ /** Base rows of the current scope, before the query (the head's "of N"). */
1614
+ get baseCount(): number {
1615
+ let count = 0;
1616
+ for (const item of this.#menu.items) if (!this.#isDisabled(item)) count++;
1617
+ return count;
1618
+ }
1619
+
1620
+ /** The selected row's selector, null when nothing selectable is selected. */
1621
+ get pickerSelected(): string | null {
1622
+ const selected = this.getSelected();
1623
+ return selected && !this.#isDisabled(selected) ? selected.selector : null;
1624
+ }
1625
+
1626
+ /**
1627
+ * Picker rows for `catalogue`: the same object while the catalogue, roles
1628
+ * and row inputs are unchanged, so typing and scope hops never resend it.
1629
+ * A rebuilt catalogue that differs in a few rows (a provider refresh, a
1630
+ * role moving) keeps the last whole `items` and patches it.
1631
+ */
1632
+ pickerItems(catalogue: readonly ModelBrowserItem[]): ModelPickerCatalogue {
1633
+ const memo = this.#pickerItems;
1634
+ if (memo?.catalogue === catalogue && memo.epoch === this.#pickerEpoch && memo.roles === this.#roles) {
1635
+ return memo.value;
1636
+ }
1637
+ const rows = catalogue.map(item => this.#pickerItem(item));
1638
+ const value = this.#patchCatalogue(rows);
1639
+ this.#pickerItems = { catalogue, epoch: this.#pickerEpoch, roles: this.#roles, value };
1640
+ return value;
1641
+ }
1642
+
1643
+ #patchCatalogue(rows: readonly TspPickerItem[]): ModelPickerCatalogue {
1644
+ const base = this.#pickerBase;
1645
+ if (base) {
1646
+ const baseById = new Map(base.map(row => [row.id, row]));
1647
+ const current = new Set(rows.map(row => row.id));
1648
+ const add = rows.filter(row => baseById.get(row.id) !== row || this.#pickerPatched.has(row.id));
1649
+ const del: string[] = [];
1650
+ for (const row of base) if (!current.has(row.id)) del.push(row.id);
1651
+ for (const id of this.#pickerPatched) if (!baseById.has(id) && !current.has(id)) del.push(id);
1652
+ if (add.length + del.length === 0) return { items: base };
1653
+ // Past a quarter of the catalogue a whole resend is cheaper than the patch.
1654
+ if ((add.length + del.length) * 4 <= base.length) {
1655
+ for (const row of add) this.#pickerPatched.add(row.id);
1656
+ for (const id of del) this.#pickerPatched.add(id);
1657
+ return {
1658
+ items: base,
1659
+ ...(add.length > 0 ? { itemsAdd: add } : {}),
1660
+ ...(del.length > 0 ? { itemsDel: del } : {}),
1661
+ };
1662
+ }
1663
+ }
1664
+ this.#pickerBase = rows;
1665
+ this.#pickerPatched.clear();
1666
+ return { items: rows };
1667
+ }
1668
+
1669
+ #pickerItem(item: ModelBrowserItem): TspPickerItem {
1670
+ const model = item.model;
1671
+ const heldRoles = this.#heldRoles(model);
1672
+ const held = heldRoles
1673
+ .map(({ role, assignment }) => `${role}:${assignment.autoSelected}:${assignment.thinkingLevel}`)
1674
+ .join(",");
1675
+ const cached = this.#pickerItemCache.get(item.selector);
1676
+ if (
1677
+ cached?.model === model &&
1678
+ cached.label === item.id &&
1679
+ cached.held === held &&
1680
+ cached.epoch === this.#pickerEpoch
1681
+ ) {
1682
+ return cached.value;
1683
+ }
1684
+ const facts: Record<string, TspText | number> = {};
1685
+ const int = model.int != null && Number.isFinite(model.int) ? model.int : undefined;
1686
+ if (int !== undefined) facts.int = String(Math.round(int));
1687
+ const speed = this.#perfCell(item, "tps").replace(/t\/s$/, "");
1688
+ if (speed) facts.speed = speed;
1689
+ if (model.contextWindow) facts.ctx = model.contextWindow;
1690
+ facts.price = pickerPrice(model);
1691
+ const badges: { text: string; tone?: "accent" | "warning" | "success"; title?: string }[] = [];
1692
+ if (this.isOverContext(item)) {
1693
+ badges.push({
1694
+ text: `ctx>${formatNumber(model.contextWindow ?? 0).toLowerCase()}`,
1695
+ tone: "warning",
1696
+ title: `Context ${formatNumber(this.#currentContextTokens).toLowerCase()} exceeds this model's limit; picking it compacts first`,
1697
+ });
1698
+ }
1699
+ if (model.isNew) badges.push({ text: "new", tone: "accent" });
1700
+ if (model.isBeta) badges.push({ text: "beta", tone: "warning" });
1701
+ if (isFreeModel(model)) badges.push({ text: "free", tone: "success" });
1702
+ const chips = heldRoles.map(({ role, assignment }) => {
1703
+ const info = this.#settings.getRoleInfo(role);
1704
+ const dot = thinkingDotToken(assignment.thinkingLevel);
1705
+ return {
1706
+ text: (info.tag ?? info.name ?? role).toLowerCase(),
1707
+ on: !assignment.autoSelected,
1708
+ ...(assignment.autoSelected ? { auto: true } : {}),
1709
+ ...(dot ? { dot } : {}),
1710
+ };
1711
+ });
1712
+ // Quick-role rows (`@role`) name the role and trail the model they apply.
1713
+ const quickRole = item.provider === "" && item.id.startsWith("@");
1714
+ const value: TspPickerItem = {
1715
+ id: item.selector,
1716
+ label: quickRole ? item.id : item.selector,
1717
+ mono: true,
1718
+ ...(quickRole ? { detail: `${item.model.provider}/${item.model.id}` } : {}),
1719
+ facts,
1720
+ ...(badges.length > 0 ? { badges: badges.slice(0, 3) } : {}),
1721
+ ...(chips.length > 0 ? { chips } : {}),
1722
+ ...(int !== undefined ? { title: `Intelligence ${Math.round(int)}` } : {}),
1723
+ };
1724
+ // Discovery re-mints Model objects for unchanged models; an equal row keeps its identity.
1725
+ const kept = cached && Bun.deepEquals(cached.value, value) ? cached.value : value;
1726
+ this.#pickerItemCache.set(item.selector, { model, label: item.id, held, epoch: this.#pickerEpoch, value: kept });
1727
+ return kept;
1728
+ }
1729
+
1730
+ /** Visible roles `model` holds, built-in order first (the detail line's chips). */
1731
+ #heldRoles(model: Model): { role: string; assignment: RoleAssignment }[] {
1732
+ const held: { role: string; assignment: RoleAssignment }[] = [];
1733
+ const seen = new Set<string>();
1734
+ const push = (role: string) => {
1735
+ if (seen.has(role)) return;
1736
+ seen.add(role);
1737
+ const assignment = this.#roles[role];
1738
+ if (!assignment || !modelsAreEqual(assignment.model, model)) return;
1739
+ if (this.#settings.getRoleInfo(role).hidden) return;
1740
+ held.push({ role, assignment });
1741
+ };
1742
+ for (const role of MODEL_ROLE_IDS) push(role);
1743
+ for (const role in this.#roles) push(role);
1744
+ return held;
1745
+ }
1746
+
1747
+ /**
1748
+ * The visible rows as picker `order`: the Recent block (recent and
1749
+ * role-assigned models, the ANSI separator's upper side) under "Recent",
1750
+ * then the rest per provider or under `grouping.rest`. Memoized per
1751
+ * result list and query, so selection moves reuse it.
1752
+ */
1753
+ pickerOrder(grouping: ModelPickerGrouping): ModelPickerOrder {
1754
+ const visible = this.#menu.visibleItems;
1755
+ const query = this.query.trim();
1756
+ const key = `${grouping.providers}\0${grouping.rest}\0${query}`;
1757
+ const memo = this.#pickerOrder;
1758
+ if (memo?.visible === visible && memo.key === key) return memo.value;
1759
+
1760
+ const split = visible.findIndex(item => this.#isDisabled(item));
1761
+ const order: (string | TspPickerGroup)[] = [];
1762
+ let count = 0;
1763
+ const pushRows = (rows: readonly ModelBrowserItem[]) => {
1764
+ for (const item of rows) {
1765
+ if (this.#isDisabled(item)) continue;
1766
+ order.push(item.selector);
1767
+ count++;
1768
+ }
1769
+ };
1770
+ const recent = split > 0 ? visible.slice(0, split) : [];
1771
+ const rest = split > 0 ? visible.slice(split + 1) : visible;
1772
+ if (grouping.rest === null) {
1773
+ pushRows(visible);
1774
+ } else {
1775
+ if (recent.length > 0) {
1776
+ order.push({ group: "recent", label: "Recent", count: recent.length });
1777
+ pushRows(recent);
1778
+ }
1779
+ if (grouping.providers) this.#pushProviderGroups(order, rest, pushRows);
1780
+ else {
1781
+ if (recent.length > 0 && rest.length > 0) {
1782
+ order.push({ group: "rest", label: grouping.rest, count: rest.length });
1783
+ }
1784
+ pushRows(rest);
1785
+ }
1786
+ }
1787
+
1788
+ let hits: Record<string, [number, number][]> | undefined;
1789
+ if (query) {
1790
+ hits = {};
1791
+ for (const item of visible) {
1792
+ if (this.#isDisabled(item)) continue;
1793
+ const label = item.provider === "" ? item.id : item.selector;
1794
+ const ranges = pickerFuzzyHits(label, query);
1795
+ if (ranges) hits[item.selector] = ranges;
1796
+ }
1797
+ }
1798
+ const value: ModelPickerOrder = { order, hits, count };
1799
+ this.#pickerOrder = { visible, key, value };
1800
+ return value;
1801
+ }
1802
+
1803
+ /** One group head per run of same-provider rows (the unfiltered list is provider-sorted past the Recent block). */
1804
+ #pushProviderGroups(
1805
+ order: (string | TspPickerGroup)[],
1806
+ rest: readonly ModelBrowserItem[],
1807
+ pushRows: (rows: readonly ModelBrowserItem[]) => void,
1808
+ ): void {
1809
+ let start = 0;
1810
+ while (start < rest.length) {
1811
+ const provider = rest[start]!.provider;
1812
+ let end = start + 1;
1813
+ while (end < rest.length && rest[end]!.provider === provider) end++;
1814
+ order.push({ group: `provider:${provider}:${start}`, label: provider, count: end - start });
1815
+ pushRows(rest.slice(start, end));
1816
+ start = end;
1817
+ }
1818
+ }
1819
+
1820
+ /**
1821
+ * Preview children for the selection. `full` (the model hub's side pane):
1822
+ * title, copyable id, badges, a fact grid, every role the model can fill
1823
+ * (held ones on) and the description. `compact` (the quick picker's strip
1824
+ * below the list): one inline fact line and the held-role chips.
1825
+ */
1826
+ pickerPreview(mode: "full" | "compact", current?: string): readonly NativeChild[] {
1827
+ const selected = this.getSelected();
1828
+ const item = selected && !this.#isDisabled(selected) ? selected : undefined;
1829
+ const key = `${mode}\0${current ?? ""}`;
1830
+ const memo = this.#pickerPreview;
1831
+ if (
1832
+ memo !== undefined &&
1833
+ memo.item === item &&
1834
+ memo.epoch === this.#pickerEpoch &&
1835
+ memo.roles === this.#roles &&
1836
+ memo.key === key
1837
+ ) {
1838
+ return memo.children;
1839
+ }
1840
+ const children = item ? this.modelPreview(item, mode, current) : [];
1841
+ this.#pickerPreview = { item, epoch: this.#pickerEpoch, roles: this.#roles, key, children };
1842
+ return children;
1843
+ }
1844
+
1845
+ /** Preview children for any model row (the hub's Roles view previews assigned models with it); unmemoized. */
1846
+ modelPreview(item: ModelBrowserItem, mode: "full" | "compact", current: string | undefined): NativeChild[] {
1847
+ const model = item.model;
1848
+ const selector = `${model.provider}/${model.id}`;
1849
+ const perf = this.#perf.get(selector);
1850
+ const ctx = model.contextWindow ?? 0;
1851
+ const out = model.maxTokens ?? 0;
1852
+ const overContext = this.isOverContext(item);
1853
+ const warning = overContext
1854
+ ? text(
1855
+ [
1856
+ span(
1857
+ `Context ${formatNumber(this.#currentContextTokens).toLowerCase()} exceeds the ${formatNumber(ctx).toLowerCase()} limit · compacts with the current model, then switches`,
1858
+ "warning",
1859
+ ),
1860
+ ],
1861
+ { wrap: "word" },
1862
+ )
1863
+ : undefined;
1864
+ const held = this.#heldRoles(model);
1865
+ const roleBadge = (role: string, assignment: RoleAssignment | undefined): NativeNode => {
1866
+ const info = this.#settings.getRoleInfo(role);
1867
+ const level =
1868
+ assignment && assignment.thinkingLevel !== ThinkingLevel.Inherit
1869
+ ? getConfiguredThinkingLevelMetadata(assignment.thinkingLevel).label
1870
+ : undefined;
1871
+ return node("badge", {
1872
+ text: (info.tag ?? info.name ?? role).toLowerCase(),
1873
+ ...(assignment ? { tone: assignment.autoSelected ? "muted" : "accent" } : {}),
1874
+ title: assignment
1875
+ ? `${info.name}${assignment.autoSelected ? " (auto-selected)" : ""}${level ? ` · thinking ${level}` : ""}`
1876
+ : `${info.name}: not assigned`,
1877
+ });
1878
+ };
1879
+
1880
+ if (mode === "compact") {
1881
+ const facts: { k: TspText; v: TspText }[] = [];
1882
+ if (ctx > 0) facts.push({ k: "ctx", v: formatNumber(ctx).toLowerCase() });
1883
+ if (out > 0) facts.push({ k: "out", v: formatNumber(out).toLowerCase() });
1884
+ facts.push({ k: "price", v: isFreeModel(model) ? "free" : `${formatCostPair(model)} per M` });
1885
+ facts.push({ k: "reasoning", v: model.reasoning ? "yes" : "no" });
1886
+ const children: NativeChild[] = [node("kv", { items: facts, layout: "inline" })];
1887
+ const chips: NativeChild[] = [];
1888
+ if (item.selector === current) chips.push(node("badge", { text: "current", tone: "success" }));
1889
+ for (const { role, assignment } of held) chips.push(roleBadge(role, assignment));
1890
+ if (chips.length > 0) children.push(row(chips, { gap: "xs", wrap: true }));
1891
+ if (warning) children.push(warning);
1892
+ return children;
1893
+ }
1894
+
1895
+ const children: NativeChild[] = [
1896
+ text(model.name, { role: "omp.picker.title" }),
1897
+ text([span(selector, "mono")], { actions: { click: "copy" }, title: "Copy model id", truncate: "middle" }),
1898
+ ];
1899
+ const badges: NativeChild[] = [];
1900
+ if (item.selector === current) badges.push(node("badge", { text: "current", tone: "success" }));
1901
+ if (model.isNew) badges.push(node("badge", { text: "new", tone: "accent" }));
1902
+ if (model.isBeta) badges.push(node("badge", { text: "beta", tone: "warning" }));
1903
+ if (model.isRecommended) badges.push(node("badge", { text: "recommended", tone: "success" }));
1904
+ if (model.reasoning) badges.push(node("badge", { text: "reasoning" }));
1905
+ if (model.input.includes("image")) badges.push(node("badge", { text: "vision" }));
1906
+ if (badges.length > 0) children.push(row(badges, { gap: "xs", wrap: true }));
1907
+
1908
+ const speed: string[] = [];
1909
+ if (perf) {
1910
+ speed.push(`${formatTps(perf.tps).replace("t/s", " t/s")}`);
1911
+ if (perf.ttftMs !== null) speed.push(`${formatTtft(perf.ttftMs).replace("s", " s")} TTFT`);
1912
+ speed.push(`${perf.samples} ${perf.samples === 1 ? "sample" : "samples"}`);
1913
+ } else if (model.tps != null && Number.isFinite(model.tps) && model.tps > 0) {
1914
+ speed.push(`~${formatTps(model.tps).replace("t/s", " t/s")} (catalog)`);
1915
+ }
1916
+ const facts: { k: TspText; v: TspText }[] = [];
1917
+ const fact = (k: string, v: string | undefined) => {
1918
+ if (v) facts.push({ k: [span(k, "muted")], v: [span(v, "mono")] });
1919
+ };
1920
+ fact("Context", ctx > 0 ? ctx.toLocaleString("en-US") : undefined);
1921
+ fact("Max output", out > 0 ? out.toLocaleString("en-US") : undefined);
1922
+ fact("Price", isFreeModel(model) ? "free" : `${previewPrice(model)} per M`);
1923
+ fact("Speed", speed.length > 0 ? speed.join(" · ") : undefined);
1924
+ fact("Intelligence", model.int != null && Number.isFinite(model.int) ? String(Math.round(model.int)) : undefined);
1925
+ fact("Input", model.input.join(" · "));
1926
+ fact("Reasoning", model.reasoning ? "yes" : "no");
1927
+ children.push(node("kv", { items: facts }));
1928
+ if (warning) children.push(warning);
1929
+
1930
+ const heldBy = new Map(held.map(entry => [entry.role, entry.assignment]));
1931
+ const roleBadges: NativeChild[] = [];
1932
+ for (const role of this.#settings.knownRoleIds) {
1933
+ const info = this.#settings.getRoleInfo(role);
1934
+ if (info.hidden || !info.accepts(model)) continue;
1935
+ roleBadges.push(roleBadge(role, heldBy.get(role)));
1936
+ }
1937
+ if (roleBadges.length > 0) {
1938
+ children.push(node("section", { head: "Roles" }, [row(roleBadges, { gap: "xs", wrap: true })]));
1939
+ }
1940
+ const description = model.description ? sanitizeText(model.description).trim() : "";
1941
+ if (description) children.push(md(description));
1942
+ return children;
1943
+ }
1944
+
1945
+ /** A picker row click: `select` highlights it, `activate` also runs Enter's path. False when not a visible row. */
1946
+ routePickerItem(selector: string, activate: boolean): boolean {
1947
+ const index = this.#menu.visibleItems.findIndex(item => item.selector === selector);
1948
+ const item = this.#menu.visibleItems[index];
1949
+ if (!item || this.#isDisabled(item)) return false;
1950
+ if (this.#menu.setSelectedIndex(index)) {
1951
+ this.#ensureSelectedVisible();
1952
+ this.onSelectionChange?.(this.getSelected());
1953
+ }
1954
+ if (activate) this.onActivate?.(item);
1955
+ return true;
1956
+ }
1298
1957
  }