@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.
- package/CHANGELOG.md +36 -0
- package/dist/types/apps/cleanse-board.d.ts +17 -0
- package/dist/types/apps/git/help.d.ts +16 -0
- package/dist/types/apps/git/sidebar.d.ts +18 -0
- package/dist/types/apps/live-visualizer.d.ts +10 -0
- package/dist/types/apps/ps-top.d.ts +15 -0
- package/dist/types/apps/standalone-picker.d.ts +3 -0
- package/dist/types/autocomplete.d.ts +8 -0
- package/dist/types/chat/advisor-message.d.ts +12 -2
- package/dist/types/chat/assistant-message.d.ts +20 -0
- package/dist/types/chat/bash-execution.d.ts +9 -0
- package/dist/types/chat/chat-transcript-builder.d.ts +6 -1
- package/dist/types/chat/collab-prompt-message.d.ts +4 -0
- package/dist/types/chat/compaction-summary-message.d.ts +15 -0
- package/dist/types/chat/eval-execution.d.ts +8 -0
- package/dist/types/chat/execution-shared.d.ts +58 -0
- package/dist/types/chat/late-diagnostics-message.d.ts +15 -1
- package/dist/types/chat/read-tool-group.d.ts +15 -0
- package/dist/types/chat/skill-message.d.ts +9 -0
- package/dist/types/chat/stripped-tool-calls-placeholder.d.ts +3 -0
- package/dist/types/chat/todo-reminder.d.ts +4 -1
- package/dist/types/chat/tool-execution.d.ts +25 -1
- package/dist/types/chat/transcript-actions.d.ts +37 -0
- package/dist/types/chat/transcript-entry.d.ts +3 -1
- package/dist/types/chat/user-message.d.ts +25 -0
- package/dist/types/chrome/collab-qrcode.d.ts +7 -0
- package/dist/types/chrome/context-thresholds.d.ts +11 -0
- package/dist/types/chrome/countdown-timer.d.ts +9 -3
- package/dist/types/chrome/diff.d.ts +12 -0
- package/dist/types/chrome/dynamic-border.d.ts +3 -0
- package/dist/types/chrome/format.d.ts +6 -0
- package/dist/types/chrome/message-divider.d.ts +13 -0
- package/dist/types/chrome/message-frame.d.ts +10 -0
- package/dist/types/chrome/message-notice.d.ts +23 -0
- package/dist/types/chrome/overlay-box.d.ts +11 -2
- package/dist/types/chrome/qrcode.d.ts +7 -0
- package/dist/types/chrome/segment-track.d.ts +7 -0
- package/dist/types/chrome/selector-helpers.d.ts +0 -6
- package/dist/types/chrome/status-notice.d.ts +14 -0
- package/dist/types/chrome/streaming-panel.d.ts +3 -0
- package/dist/types/chrome/tool-activity.d.ts +3 -0
- package/dist/types/chrome/transcript-container.d.ts +13 -0
- package/dist/types/chrome/transcript-status.d.ts +15 -3
- package/dist/types/components/box.d.ts +7 -0
- package/dist/types/components/disclosure.d.ts +9 -0
- package/dist/types/components/editor.d.ts +37 -0
- package/dist/types/components/form.d.ts +14 -0
- package/dist/types/components/image.d.ts +7 -0
- package/dist/types/components/input.d.ts +20 -1
- package/dist/types/components/key-value-list.d.ts +3 -0
- package/dist/types/components/layout/geometry.d.ts +16 -0
- package/dist/types/components/layout/row.d.ts +7 -0
- package/dist/types/components/layout/split-pane.d.ts +8 -0
- package/dist/types/components/layout/stack.d.ts +6 -0
- package/dist/types/components/loader.d.ts +48 -0
- package/dist/types/components/markdown.d.ts +8 -0
- package/dist/types/components/metric.d.ts +7 -0
- package/dist/types/components/progress-bar.d.ts +3 -0
- package/dist/types/components/scroll-view.d.ts +6 -0
- package/dist/types/components/section.d.ts +3 -0
- package/dist/types/components/select-list.d.ts +33 -0
- package/dist/types/components/settings-list.d.ts +68 -0
- package/dist/types/components/spacer.d.ts +4 -0
- package/dist/types/components/tab-bar.d.ts +9 -0
- package/dist/types/components/table.d.ts +7 -0
- package/dist/types/components/text.d.ts +6 -0
- package/dist/types/components/tree-view.d.ts +9 -0
- package/dist/types/components/truncated-text.d.ts +3 -0
- package/dist/types/components/wizard-step.d.ts +6 -0
- package/dist/types/hotkeys-markdown.d.ts +24 -0
- package/dist/types/index.d.ts +5 -0
- package/dist/types/latex-block.d.ts +7 -0
- package/dist/types/native/apply.d.ts +39 -0
- package/dist/types/native/backend.d.ts +94 -0
- package/dist/types/native/blobs.d.ts +29 -0
- package/dist/types/native/describe.d.ts +56 -0
- package/dist/types/native/encode.d.ts +50 -0
- package/dist/types/native/icons.d.ts +18 -0
- package/dist/types/native/memo.d.ts +27 -0
- package/dist/types/native/node.d.ts +107 -0
- package/dist/types/native/overlay.d.ts +64 -0
- package/dist/types/native/picker.d.ts +124 -0
- package/dist/types/native/reconcile.d.ts +41 -0
- package/dist/types/native/settle.d.ts +11 -0
- package/dist/types/native/spans.d.ts +21 -0
- package/dist/types/native/state.d.ts +6 -0
- package/dist/types/native/tone.d.ts +24 -0
- package/dist/types/overlays/advisor-config.d.ts +20 -0
- package/dist/types/overlays/agent-hub-renderer.d.ts +27 -11
- package/dist/types/overlays/agent-hub.d.ts +10 -0
- package/dist/types/overlays/agent-transcript-viewer.d.ts +7 -0
- package/dist/types/overlays/agents-hub.d.ts +8 -0
- package/dist/types/overlays/annotation-overlay.d.ts +3 -0
- package/dist/types/overlays/ask-dialog.d.ts +23 -0
- package/dist/types/overlays/bordered-loader.d.ts +3 -0
- package/dist/types/overlays/btw-history-panel.d.ts +9 -0
- package/dist/types/overlays/btw-panel.d.ts +3 -0
- package/dist/types/overlays/cleanse-panel.d.ts +5 -2
- package/dist/types/overlays/composer-shape-preview.d.ts +7 -1
- package/dist/types/overlays/copy-selector.d.ts +38 -0
- package/dist/types/overlays/error-banner.d.ts +9 -0
- package/dist/types/overlays/extensions/extension-list.d.ts +1 -0
- package/dist/types/overlays/history-search.d.ts +18 -0
- package/dist/types/overlays/hook-editor.d.ts +28 -1
- package/dist/types/overlays/hook-input.d.ts +3 -0
- package/dist/types/overlays/hook-selector.d.ts +10 -0
- package/dist/types/overlays/hub-frame.d.ts +18 -0
- package/dist/types/overlays/jobs-panel.d.ts +24 -0
- package/dist/types/overlays/login-dialog.d.ts +10 -0
- package/dist/types/overlays/logout-account-selector.d.ts +4 -0
- package/dist/types/overlays/mcp-add-wizard.d.ts +4 -0
- package/dist/types/overlays/model-browser.d.ts +69 -0
- package/dist/types/overlays/model-hub.d.ts +7 -0
- package/dist/types/overlays/model-picker.d.ts +7 -0
- package/dist/types/overlays/move-overlay.d.ts +3 -0
- package/dist/types/overlays/oauth-selector.d.ts +4 -0
- package/dist/types/overlays/omfg-panel.d.ts +3 -0
- package/dist/types/overlays/pause-screen.d.ts +12 -0
- package/dist/types/overlays/plan-review-overlay.d.ts +5 -0
- package/dist/types/overlays/plan-save-overlay.d.ts +10 -0
- package/dist/types/overlays/plugin-selector.d.ts +4 -0
- package/dist/types/overlays/plugin-settings.d.ts +30 -0
- package/dist/types/overlays/queue-mode-selector.d.ts +4 -0
- package/dist/types/overlays/reset-usage-selector.d.ts +8 -0
- package/dist/types/overlays/rewind-selector.d.ts +5 -0
- package/dist/types/overlays/session-info-overlay.d.ts +19 -1
- package/dist/types/overlays/session-selector.d.ts +64 -1
- package/dist/types/overlays/settings-defs.d.ts +14 -0
- package/dist/types/overlays/settings-selector.d.ts +21 -0
- package/dist/types/overlays/show-images-selector.d.ts +4 -0
- package/dist/types/overlays/snapcompact-shape-preview.d.ts +2 -0
- package/dist/types/overlays/stats-notice.d.ts +8 -0
- package/dist/types/overlays/theme-selector.d.ts +11 -0
- package/dist/types/overlays/thinking-selector.d.ts +4 -0
- package/dist/types/overlays/tree-selector.d.ts +60 -2
- package/dist/types/overlays/usage-dashboard.d.ts +26 -3
- package/dist/types/overlays/usage-row.d.ts +36 -2
- package/dist/types/prompt/attachment-chips.d.ts +8 -0
- package/dist/types/prompt/composer-attachments.d.ts +15 -3
- package/dist/types/prompt/composer.d.ts +18 -1
- package/dist/types/prompt/custom-editor.d.ts +48 -2
- package/dist/types/prompt/editor-top-gap.d.ts +2 -0
- package/dist/types/prompt/macos-spelling.d.ts +6 -0
- package/dist/types/prompt/magic-keywords.d.ts +9 -0
- package/dist/types/prompt/queued-messages.d.ts +23 -0
- package/dist/types/prompt/skill-tokens.d.ts +4 -2
- package/dist/types/prompt/welcome.d.ts +16 -0
- package/dist/types/render/code-cell.d.ts +10 -0
- package/dist/types/render/file-list.d.ts +8 -0
- package/dist/types/render/hyperlink.d.ts +14 -0
- package/dist/types/render/output-block.d.ts +43 -0
- package/dist/types/render/output-pane.d.ts +17 -0
- package/dist/types/render/tool-card.d.ts +8 -0
- package/dist/types/render/tree-list.d.ts +8 -0
- package/dist/types/render/width-aware-text.d.ts +7 -0
- package/dist/types/setup/scenes/outro.d.ts +7 -0
- package/dist/types/setup/scenes/sign-in.d.ts +7 -0
- package/dist/types/setup/scenes/splash.d.ts +8 -0
- package/dist/types/setup/scenes/types.d.ts +6 -1
- package/dist/types/setup/wizard-overlay.d.ts +9 -0
- package/dist/types/status-line/component.d.ts +49 -5
- package/dist/types/status-line/context-usage.d.ts +20 -0
- package/dist/types/status-line/footer.d.ts +8 -0
- package/dist/types/status-line/metrics.d.ts +1 -2
- package/dist/types/status-line/segments.d.ts +10 -1
- package/dist/types/status-line/types.d.ts +40 -1
- package/dist/types/terminal-capabilities.d.ts +1 -1
- package/dist/types/terminal.d.ts +27 -0
- package/dist/types/theme/defaults/index.d.ts +100 -0
- package/dist/types/theme/schema.d.ts +2 -0
- package/dist/types/theme/shimmer.d.ts +8 -0
- package/dist/types/theme/theme-class.d.ts +2 -0
- package/dist/types/theme/theme.d.ts +33 -1
- package/dist/types/tools/ask.d.ts +3 -0
- package/dist/types/tools/ast-edit.d.ts +10 -1
- package/dist/types/tools/ast-grep.d.ts +10 -1
- package/dist/types/tools/autoresearch.d.ts +21 -4
- package/dist/types/tools/bash.d.ts +30 -1
- package/dist/types/tools/cfg-render.d.ts +5 -1
- package/dist/types/tools/debug.d.ts +3 -1
- package/dist/types/tools/default-renderer.d.ts +6 -1
- package/dist/types/tools/edit.d.ts +14 -1
- package/dist/types/tools/eval.d.ts +12 -1
- package/dist/types/tools/fetch.d.ts +16 -1
- package/dist/types/tools/find.d.ts +15 -1
- package/dist/types/tools/github.d.ts +3 -0
- package/dist/types/tools/glob.d.ts +14 -1
- package/dist/types/tools/goal.d.ts +3 -1
- package/dist/types/tools/grep.d.ts +14 -1
- package/dist/types/tools/grouped-file-output.d.ts +58 -0
- package/dist/types/tools/json-tree.d.ts +20 -0
- package/dist/types/tools/lsp.d.ts +3 -10
- package/dist/types/tools/mcp.d.ts +16 -1
- package/dist/types/tools/memory.d.ts +26 -0
- package/dist/types/tools/native-view.d.ts +90 -0
- package/dist/types/tools/proc-render.d.ts +7 -1
- package/dist/types/tools/read.d.ts +23 -1
- package/dist/types/tools/renderer.d.ts +57 -0
- package/dist/types/tools/report-tool-issue.d.ts +3 -0
- package/dist/types/tools/resolve.d.ts +14 -1
- package/dist/types/tools/task.d.ts +3 -0
- package/dist/types/tools/think.d.ts +3 -0
- package/dist/types/tools/todo.d.ts +12 -0
- package/dist/types/tools/vibe.d.ts +3 -1
- package/dist/types/tools/wait.d.ts +3 -0
- package/dist/types/tools/web-search.d.ts +8 -0
- package/dist/types/tools/write.d.ts +14 -1
- package/dist/types/tools/xdev.d.ts +13 -0
- package/dist/types/tui.d.ts +49 -0
- package/package.json +9 -9
- package/src/apps/autoresearch-dashboard.ts +362 -4
- package/src/apps/cleanse-board.ts +219 -1
- package/src/apps/cleanse-picker.ts +9 -1
- package/src/apps/debug/log-viewer.ts +15 -15
- package/src/apps/debug/protocol-probe.ts +2 -20
- package/src/apps/git/avatar.ts +2 -22
- package/src/apps/git/git-tui.ts +406 -59
- package/src/apps/git/help.ts +130 -0
- package/src/apps/git/sidebar.ts +491 -0
- package/src/apps/live-visualizer.ts +65 -0
- package/src/apps/ps-top.ts +448 -17
- package/src/apps/session-picker.ts +1 -0
- package/src/apps/setup-model-picker.ts +5 -1
- package/src/apps/standalone-picker.ts +55 -1
- package/src/autocomplete.ts +44 -1
- package/src/chat/advisor-message.ts +72 -3
- package/src/chat/assistant-message.ts +346 -11
- package/src/chat/background-tan-message.ts +4 -4
- package/src/chat/bash-execution.ts +81 -0
- package/src/chat/cache-invalidation-marker.ts +8 -0
- package/src/chat/chat-transcript-builder.ts +37 -6
- package/src/chat/collab-prompt-message.ts +15 -3
- package/src/chat/compaction-summary-message.ts +74 -1
- package/src/chat/custom-message.ts +3 -1
- package/src/chat/eval-execution.ts +57 -0
- package/src/chat/execution-shared.ts +153 -1
- package/src/chat/hook-message.ts +1 -0
- package/src/chat/late-diagnostics-message.ts +85 -2
- package/src/chat/read-tool-group.ts +341 -1
- package/src/chat/served-model-marker.ts +8 -0
- package/src/chat/skill-message.ts +63 -0
- package/src/chat/stripped-tool-calls-placeholder.ts +18 -10
- package/src/chat/todo-reminder.ts +34 -4
- package/src/chat/tool-execution.ts +385 -10
- package/src/chat/transcript-actions.ts +38 -0
- package/src/chat/transcript-browser.ts +2 -9
- package/src/chat/transcript-entry.ts +10 -1
- package/src/chat/transcript-render-helpers.ts +16 -15
- package/src/chat/ttsr-notification.ts +38 -1
- package/src/chat/user-message.ts +167 -4
- package/src/chrome/collab-qrcode.ts +27 -2
- package/src/chrome/context-thresholds.ts +34 -0
- package/src/chrome/countdown-timer.ts +24 -1
- package/src/chrome/diff.ts +60 -1
- package/src/chrome/dynamic-border.ts +7 -0
- package/src/chrome/format.ts +12 -0
- package/src/chrome/message-divider.ts +33 -1
- package/src/chrome/message-frame.ts +53 -10
- package/src/chrome/message-notice.ts +71 -0
- package/src/chrome/overlay-box.ts +45 -2
- package/src/chrome/qrcode.ts +28 -0
- package/src/chrome/segment-track.ts +26 -0
- package/src/chrome/selector-helpers.ts +1 -17
- package/src/chrome/status-notice.ts +46 -0
- package/src/chrome/streaming-panel.ts +25 -1
- package/src/chrome/tool-activity.ts +20 -0
- package/src/chrome/transcript-container.ts +37 -0
- package/src/chrome/transcript-status.ts +40 -11
- package/src/components/box.ts +51 -0
- package/src/components/disclosure.ts +33 -0
- package/src/components/editor.ts +216 -27
- package/src/components/form.ts +93 -9
- package/src/components/image.ts +30 -0
- package/src/components/input.ts +77 -26
- package/src/components/key-value-list.ts +18 -0
- package/src/components/layout/geometry.ts +44 -0
- package/src/components/layout/row.ts +30 -0
- package/src/components/layout/split-pane.ts +65 -0
- package/src/components/layout/stack.ts +27 -0
- package/src/components/loader.ts +183 -1
- package/src/components/markdown.ts +21 -0
- package/src/components/metric.ts +39 -0
- package/src/components/progress-bar.ts +28 -0
- package/src/components/scroll-view.ts +23 -0
- package/src/components/section.ts +16 -0
- package/src/components/select-list.ts +174 -25
- package/src/components/settings-list.ts +264 -7
- package/src/components/spacer.ts +17 -0
- package/src/components/tab-bar.ts +42 -0
- package/src/components/table.ts +34 -0
- package/src/components/text.ts +23 -0
- package/src/components/tree-view.ts +65 -0
- package/src/components/truncated-text.ts +14 -0
- package/src/components/wizard-step.ts +18 -9
- package/src/debug-server.ts +14 -7
- package/src/hotkeys-markdown.ts +251 -69
- package/src/index.ts +6 -0
- package/src/latex-block.ts +11 -0
- package/src/native/apply.ts +275 -0
- package/src/native/backend.ts +682 -0
- package/src/native/blobs.ts +70 -0
- package/src/native/describe.ts +124 -0
- package/src/native/encode.ts +233 -0
- package/src/native/icons.ts +184 -0
- package/src/native/memo.ts +64 -0
- package/src/native/node.ts +112 -0
- package/src/native/overlay.ts +131 -0
- package/src/native/picker.ts +333 -0
- package/src/native/reconcile.ts +1063 -0
- package/src/native/settle.ts +23 -0
- package/src/native/spans.ts +308 -0
- package/src/native/state.ts +28 -0
- package/src/native/tone.ts +79 -0
- package/src/overlays/advisor-config.ts +420 -0
- package/src/overlays/agent-hub-renderer.ts +155 -18
- package/src/overlays/agent-hub.ts +1037 -44
- package/src/overlays/agent-transcript-viewer.ts +129 -0
- package/src/overlays/agents-hub.ts +688 -53
- package/src/overlays/annotation-overlay.ts +391 -52
- package/src/overlays/ask-dialog.ts +435 -24
- package/src/overlays/bordered-loader.ts +17 -0
- package/src/overlays/btw-history-panel.ts +182 -0
- package/src/overlays/btw-panel.ts +68 -2
- package/src/overlays/cleanse-panel.ts +105 -15
- package/src/overlays/codex-reset-fireworks.ts +65 -5
- package/src/overlays/composer-shape-preview.ts +26 -1
- package/src/overlays/copy-selector.ts +545 -23
- package/src/overlays/error-banner.ts +44 -0
- package/src/overlays/extensions/extension-list.ts +23 -17
- package/src/overlays/history-search.ts +175 -33
- package/src/overlays/hook-editor.ts +113 -6
- package/src/overlays/hook-input.ts +22 -1
- package/src/overlays/hook-selector.ts +311 -22
- package/src/overlays/hub-frame.ts +80 -0
- package/src/overlays/jobs-panel.ts +112 -0
- package/src/overlays/login-dialog.ts +183 -1
- package/src/overlays/logout-account-selector.ts +102 -4
- package/src/overlays/mcp-add-wizard.ts +232 -53
- package/src/overlays/model-browser.ts +681 -22
- package/src/overlays/model-hub.ts +1402 -78
- package/src/overlays/model-picker.ts +175 -1
- package/src/overlays/move-overlay.ts +45 -0
- package/src/overlays/oauth-selector.ts +227 -23
- package/src/overlays/omfg-panel.ts +86 -2
- package/src/overlays/pause-screen.ts +55 -4
- package/src/overlays/plan-review-overlay.ts +352 -0
- package/src/overlays/plan-save-overlay.ts +45 -0
- package/src/overlays/plugin-selector.ts +21 -1
- package/src/overlays/plugin-settings.ts +195 -5
- package/src/overlays/queue-mode-selector.ts +19 -1
- package/src/overlays/reset-usage-selector.ts +110 -40
- package/src/overlays/rewind-selector.ts +458 -44
- package/src/overlays/session-account-selector.ts +1 -1
- package/src/overlays/session-info-overlay.ts +137 -1
- package/src/overlays/session-selector.ts +807 -46
- package/src/overlays/settings-defs.ts +41 -0
- package/src/overlays/settings-selector.ts +583 -23
- package/src/overlays/show-images-selector.ts +18 -1
- package/src/overlays/snapcompact-shape-preview.ts +48 -3
- package/src/overlays/stats-notice.ts +31 -0
- package/src/overlays/theme-selector.ts +31 -1
- package/src/overlays/thinking-selector.ts +22 -1
- package/src/overlays/tree-selector.ts +570 -110
- package/src/overlays/usage-dashboard.ts +587 -12
- package/src/overlays/usage-row.ts +161 -25
- package/src/prompt/attachment-chips.ts +60 -0
- package/src/prompt/composer-attachments.ts +31 -9
- package/src/prompt/composer.ts +55 -2
- package/src/prompt/custom-editor.ts +302 -4
- package/src/prompt/editor-top-gap.ts +8 -0
- package/src/prompt/macos-spelling.ts +24 -9
- package/src/prompt/magic-keywords.ts +22 -1
- package/src/prompt/model-mention-autocomplete.ts +1 -0
- package/src/prompt/queued-messages.ts +101 -0
- package/src/prompt/skill-tokens.ts +5 -3
- package/src/prompt/welcome.ts +213 -6
- package/src/render/code-cell.ts +103 -2
- package/src/render/file-list.ts +29 -0
- package/src/render/hyperlink.ts +32 -0
- package/src/render/output-block.ts +110 -0
- package/src/render/output-pane.ts +37 -0
- package/src/render/tool-card.ts +137 -0
- package/src/render/tree-list.ts +39 -0
- package/src/render/width-aware-text.ts +20 -0
- package/src/setup/scenes/composer.ts +29 -0
- package/src/setup/scenes/glyph.ts +15 -0
- package/src/setup/scenes/model.ts +36 -8
- package/src/setup/scenes/outro.ts +28 -1
- package/src/setup/scenes/sign-in.ts +122 -13
- package/src/setup/scenes/splash.ts +28 -1
- package/src/setup/scenes/theme.ts +88 -3
- package/src/setup/scenes/types.ts +7 -1
- package/src/setup/startup-splash.ts +46 -9
- package/src/setup/wizard-overlay.ts +140 -23
- package/src/status-line/component.ts +398 -16
- package/src/status-line/context-usage.ts +279 -50
- package/src/status-line/footer.ts +94 -1
- package/src/status-line/metrics.ts +3 -6
- package/src/status-line/segments.ts +463 -97
- package/src/status-line/separators.ts +3 -16
- package/src/status-line/types.ts +43 -1
- package/src/terminal-capabilities.ts +7 -0
- package/src/terminal.ts +130 -2
- package/src/theme/defaults/dark-neon-noir.json +100 -0
- package/src/theme/defaults/index.ts +2 -0
- package/src/theme/defaults/light-prism.json +5 -5
- package/src/theme/defaults/onyx.json +62 -62
- package/src/theme/schema.ts +15 -0
- package/src/theme/shimmer.ts +33 -0
- package/src/theme/theme-class.ts +24 -19
- package/src/theme/theme.ts +111 -9
- package/src/tools/ask.ts +157 -1
- package/src/tools/ast-edit.ts +174 -1
- package/src/tools/ast-grep.ts +82 -2
- package/src/tools/autoresearch.ts +149 -1
- package/src/tools/bash.ts +180 -49
- package/src/tools/cfg-render.ts +66 -1
- package/src/tools/debug.ts +78 -1
- package/src/tools/default-renderer.ts +35 -1
- package/src/tools/edit.ts +247 -2
- package/src/tools/eval.ts +257 -1
- package/src/tools/fetch.ts +102 -3
- package/src/tools/find.ts +127 -1
- package/src/tools/github.ts +119 -10
- package/src/tools/glob.ts +116 -1
- package/src/tools/goal.ts +56 -1
- package/src/tools/grep.ts +83 -2
- package/src/tools/grouped-file-output.ts +205 -0
- package/src/tools/json-tree.ts +116 -1
- package/src/tools/lsp.ts +318 -1
- package/src/tools/mcp.ts +80 -1
- package/src/tools/memory.ts +93 -11
- package/src/tools/native-view.ts +292 -0
- package/src/tools/proc-render.ts +221 -1
- package/src/tools/read.ts +199 -4
- package/src/tools/renderer.ts +61 -0
- package/src/tools/report-tool-issue.ts +7 -1
- package/src/tools/resolve.ts +63 -1
- package/src/tools/task.ts +406 -4
- package/src/tools/think.ts +19 -1
- package/src/tools/todo.ts +89 -0
- package/src/tools/vibe.ts +230 -2
- package/src/tools/wait.ts +181 -1
- package/src/tools/web-search.ts +113 -1
- package/src/tools/write.ts +144 -5
- package/src/tools/xdev.ts +64 -1
- 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 {
|
|
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
|
-
|
|
431
|
-
|
|
432
|
-
|
|
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
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
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
|
}
|