@oh-my-pi/pi-tui 18.4.2 → 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 +56 -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/thinking-display.d.ts +2 -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 +42 -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/loop-watchdog.d.ts +9 -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 +9 -1
- 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 +16 -9
- package/dist/types/setup/scenes/splash.d.ts +8 -0
- package/dist/types/setup/scenes/types.d.ts +6 -27
- 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/stdin-buffer.d.ts +10 -7
- package/dist/types/terminal-capabilities.d.ts +1 -1
- package/dist/types/terminal.d.ts +36 -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-types.d.ts +90 -0
- package/dist/types/tools/web-search.d.ts +9 -168
- 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 +538 -51
- 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/thinking-display.ts +73 -11
- 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 +139 -17
- 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 +239 -36
- 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 +131 -56
- 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/loop-watchdog.ts +38 -10
- 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 +184 -7
- 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 +585 -33
- 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 +138 -24
- package/src/setup/scenes/splash.ts +28 -1
- package/src/setup/scenes/theme.ts +88 -3
- package/src/setup/scenes/types.ts +7 -28
- package/src/setup/startup-splash.ts +46 -9
- package/src/setup/wizard-overlay.ts +140 -23
- package/src/setup/wizard.ts +1 -1
- package/src/status-line/component.ts +414 -18
- 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/stdin-buffer.ts +27 -21
- package/src/terminal-capabilities.ts +7 -0
- package/src/terminal.ts +153 -11
- 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/mermaid-cache.ts +10 -1
- 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/theme/tui-adapters.ts +14 -1
- 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 +233 -62
- 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 +357 -31
- 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/streaming-output.ts +79 -16
- 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-types.ts +99 -0
- package/src/tools/web-search.ts +111 -170
- package/src/tools/write.ts +144 -5
- package/src/tools/xdev.ts +64 -1
- package/src/tui.ts +295 -4
- package/dist/types/setup/scenes/providers.d.ts +0 -3
- package/dist/types/setup/scenes/web-search.d.ts +0 -22
- package/src/setup/scenes/providers.ts +0 -110
- package/src/setup/scenes/web-search.ts +0 -165
|
@@ -11,6 +11,15 @@ import { parseModelString, splitUpstreamRouting, formatModelSelectorValue } from
|
|
|
11
11
|
* in the compact alt+p picker ({@link ./model-picker}).
|
|
12
12
|
*/
|
|
13
13
|
import { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
|
|
14
|
+
import type {
|
|
15
|
+
TspPickerAction,
|
|
16
|
+
TspPickerColumn,
|
|
17
|
+
TspPickerGroup,
|
|
18
|
+
TspPickerItem,
|
|
19
|
+
TspPickerProps,
|
|
20
|
+
TspPickerScope,
|
|
21
|
+
TspSpan,
|
|
22
|
+
} from "@oh-my-pi/pi-wire";
|
|
14
23
|
import type { KeysApi, Model } from "@oh-my-pi/pi-ai";
|
|
15
24
|
import { getOAuthProviders } from "@oh-my-pi/pi-ai/oauth";
|
|
16
25
|
import { getSupportedEfforts } from "@oh-my-pi/pi-catalog/model-thinking";
|
|
@@ -20,7 +29,14 @@ import type { Component, TUI } from "../tui";
|
|
|
20
29
|
import { extractPrintableText, matchesKey } from "../keys";
|
|
21
30
|
import { fuzzyFilter } from "../fuzzy";
|
|
22
31
|
import { formatKeyHint, formatKeyHints } from "../app-keybindings";
|
|
23
|
-
import { editorKey, editorKeys } from "../chrome/keybinding-hints";
|
|
32
|
+
import { boundKeys, editorKey, editorKeys } from "../chrome/keybinding-hints";
|
|
33
|
+
import type { KeyName } from "../key-hint-format";
|
|
34
|
+
import { col, compact, kbd, md, node, span, text } from "../native/describe";
|
|
35
|
+
import { CLOSE_ACTION, type PickerEvent, picker, pickerAction, pickerEvent } from "../native/picker";
|
|
36
|
+
import type { DescribeContext, NativeChild, NativeNode, NativeUiEvent } from "../native/node";
|
|
37
|
+
import { actionHint, hintsRow, type NativeHint } from "../native/overlay";
|
|
38
|
+
import { plainText } from "../native/spans";
|
|
39
|
+
import { isNativeRendering } from "../native/state";
|
|
24
40
|
import { Input } from "../components/input";
|
|
25
41
|
import { routeSgrMouseInput, type SgrMouseEvent } from "../mouse";
|
|
26
42
|
import { truncateToWidth, visibleWidth } from "../utils";
|
|
@@ -36,14 +52,18 @@ import { theme } from "../theme/theme";
|
|
|
36
52
|
import { matchesSelectCancel, matchesSelectDown, matchesSelectUp } from "../keybinding-matchers";
|
|
37
53
|
import {
|
|
38
54
|
buildBrowserItems,
|
|
55
|
+
MODEL_PICKER_COLUMNS,
|
|
39
56
|
ModelBrowser,
|
|
40
57
|
type ModelBrowserItem,
|
|
41
58
|
modelSearchText,
|
|
59
|
+
thinkingDotToken,
|
|
42
60
|
type RoleAssignments,
|
|
43
61
|
resolveRoleAssignments,
|
|
44
62
|
sortModelItems,
|
|
45
63
|
} from "./model-browser";
|
|
46
64
|
import {
|
|
65
|
+
describeHubFrame,
|
|
66
|
+
describeHubSidebar,
|
|
47
67
|
HubFrame,
|
|
48
68
|
moveStripSelection,
|
|
49
69
|
type SidebarEntry as HubSidebarEntry,
|
|
@@ -140,6 +160,8 @@ export interface ModelHubCallbacks {
|
|
|
140
160
|
export interface ModelHubOptions {
|
|
141
161
|
/** Preselect this provider's sidebar entry (e.g. when reopening after /login). */
|
|
142
162
|
initialProviderId?: string;
|
|
163
|
+
/** `provider/id` of the session's model, marked current in the native picker. */
|
|
164
|
+
currentSelector?: string;
|
|
143
165
|
}
|
|
144
166
|
|
|
145
167
|
interface SidebarEntry extends HubSidebarEntry<"recent" | "roles" | "all" | "separator" | "provider"> {
|
|
@@ -187,12 +209,72 @@ type StripState =
|
|
|
187
209
|
input: Input;
|
|
188
210
|
};
|
|
189
211
|
|
|
212
|
+
/** A Roles-view command; keys and the picker's action bar both run {@link ModelHubComponent}'s `#runRolesAction`. */
|
|
213
|
+
type RolesAction = "pick" | "clear" | "fallback" | "cycle" | "earlier" | "later" | "new" | "thinking";
|
|
214
|
+
|
|
215
|
+
/** Printable keys of the Roles view and the command each runs. */
|
|
216
|
+
const ROLES_ACTION_KEYS: Record<string, RolesAction> = {
|
|
217
|
+
x: "clear",
|
|
218
|
+
f: "fallback",
|
|
219
|
+
c: "cycle",
|
|
220
|
+
"[": "earlier",
|
|
221
|
+
"]": "later",
|
|
222
|
+
n: "new",
|
|
223
|
+
t: "thinking",
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
/** Picker fact columns of the Roles view. */
|
|
227
|
+
const ROLE_PICKER_COLUMNS: readonly TspPickerColumn[] = [
|
|
228
|
+
{ id: "model", head: "Model", format: "text", priority: 2 },
|
|
229
|
+
{ id: "thinking", head: "Thinking", format: "dim", priority: 1 },
|
|
230
|
+
];
|
|
231
|
+
|
|
232
|
+
/** Kind tab labels (sentence case; acronyms stay upper). */
|
|
233
|
+
const MODEL_KIND_LABELS: Record<"all" | ModelKind, string> = {
|
|
234
|
+
all: "All",
|
|
235
|
+
chat: "Chat",
|
|
236
|
+
tiny: "Tiny",
|
|
237
|
+
image: "Image",
|
|
238
|
+
tts: "TTS",
|
|
239
|
+
stt: "STT",
|
|
240
|
+
search: "Search",
|
|
241
|
+
judge: "Judge",
|
|
242
|
+
embedding: "Embedding",
|
|
243
|
+
rerank: "Rerank",
|
|
244
|
+
video: "Video",
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
/** A provider mark's initials: `amazon-bedrock` → `AB`, `anthropic` → `An`. */
|
|
248
|
+
function providerInitials(providerId: string): string {
|
|
249
|
+
const words = providerId.split(/[-_\s.]+/).filter(word => word.length > 0);
|
|
250
|
+
if (words.length >= 2) return `${words[0]!.charAt(0)}${words[1]!.charAt(0)}`.toUpperCase();
|
|
251
|
+
const word = words[0] ?? providerId;
|
|
252
|
+
return `${word.charAt(0).toUpperCase()}${word.charAt(1)}`;
|
|
253
|
+
}
|
|
254
|
+
|
|
190
255
|
const PROVIDER_REFRESH_DEBOUNCE_MS = 120;
|
|
191
256
|
const RECENT_LIMIT = 15;
|
|
192
257
|
const MODEL_KIND_TABS: ReadonlyArray<"all" | ModelKind> = ["all", ...MODEL_KINDS];
|
|
193
258
|
const ROLE_TABS = ["all", "chat", "kind"] as const;
|
|
194
259
|
type RoleTab = (typeof ROLE_TABS)[number];
|
|
195
260
|
|
|
261
|
+
/** Stable native list-item key of a Roles-view row. */
|
|
262
|
+
function rolesRowKey(row: RolesRow, index: number): string {
|
|
263
|
+
switch (row.kind) {
|
|
264
|
+
case "role":
|
|
265
|
+
return `role:${row.role}`;
|
|
266
|
+
case "chainKey":
|
|
267
|
+
return `chain:${row.role}`;
|
|
268
|
+
case "fallback":
|
|
269
|
+
return `fallback:${row.role}:${row.chainIndex}`;
|
|
270
|
+
case "separator":
|
|
271
|
+
return `sep:${index}`;
|
|
272
|
+
case "newRole":
|
|
273
|
+
case "newFallback":
|
|
274
|
+
return row.kind;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
196
278
|
/**
|
|
197
279
|
* Providers already auto-refreshed this process. Selecting a provider fetches
|
|
198
280
|
* its live model list at most once per application lifetime (surviving hub
|
|
@@ -292,6 +374,24 @@ export class ModelHubComponent implements Component {
|
|
|
292
374
|
);
|
|
293
375
|
#lockedLoginLine: number | null = null;
|
|
294
376
|
#rolesRowStart = 1;
|
|
377
|
+
/** Bumped on every visible-state change; the described node is rebuilt when it moves. */
|
|
378
|
+
#nativeVersion = 0;
|
|
379
|
+
#nativeCache: { version: number; picker: boolean; node: NativeNode } | undefined;
|
|
380
|
+
#currentSelector: string | undefined;
|
|
381
|
+
/** The opening online catalog refresh is still in flight (an empty scope then shows as loading). */
|
|
382
|
+
#catalogRefreshing = false;
|
|
383
|
+
#kindTabsMemo: { candidates: readonly ModelBrowserItem[]; tabs: TspPickerProps["tabs"] } | undefined;
|
|
384
|
+
#pickerRoleItems:
|
|
385
|
+
| { rows: readonly RolesRow[]; roles: RoleAssignments; cycle: string; items: readonly TspPickerItem[] }
|
|
386
|
+
| undefined;
|
|
387
|
+
#pickerRolePreview:
|
|
388
|
+
| {
|
|
389
|
+
row: RolesRow | undefined;
|
|
390
|
+
roles: RoleAssignments;
|
|
391
|
+
rows: readonly RolesRow[];
|
|
392
|
+
children: readonly NativeChild[];
|
|
393
|
+
}
|
|
394
|
+
| undefined;
|
|
295
395
|
|
|
296
396
|
constructor(
|
|
297
397
|
tui: TUI,
|
|
@@ -306,6 +406,7 @@ export class ModelHubComponent implements Component {
|
|
|
306
406
|
this.#registry = registry;
|
|
307
407
|
this.#scopedModels = scopedModels;
|
|
308
408
|
this.#callbacks = callbacks;
|
|
409
|
+
this.#currentSelector = options.currentSelector;
|
|
309
410
|
|
|
310
411
|
this.#browser = new ModelBrowser(settings, {
|
|
311
412
|
emptyText: () => this.#emptyStateMessage(),
|
|
@@ -332,13 +433,17 @@ export class ModelHubComponent implements Component {
|
|
|
332
433
|
// registry-independent, so the reload would only repeat the hydration
|
|
333
434
|
// above.
|
|
334
435
|
if (this.#scopedModels.length === 0) {
|
|
436
|
+
this.#catalogRefreshing = true;
|
|
335
437
|
this.#registry
|
|
336
438
|
.refresh("online")
|
|
337
439
|
.then(() => this.#syncFromRegistryState())
|
|
338
440
|
.catch(error => {
|
|
339
441
|
this.#configError = error instanceof Error ? error.message : String(error);
|
|
340
442
|
})
|
|
341
|
-
.finally(() =>
|
|
443
|
+
.finally(() => {
|
|
444
|
+
this.#catalogRefreshing = false;
|
|
445
|
+
this.#requestRender();
|
|
446
|
+
});
|
|
342
447
|
}
|
|
343
448
|
}
|
|
344
449
|
|
|
@@ -356,9 +461,16 @@ export class ModelHubComponent implements Component {
|
|
|
356
461
|
}
|
|
357
462
|
|
|
358
463
|
invalidate(): void {
|
|
464
|
+
this.#nativeVersion++;
|
|
359
465
|
this.#frame.invalidate();
|
|
360
466
|
}
|
|
361
467
|
|
|
468
|
+
/** Request a repaint after a state change, invalidating the described node. */
|
|
469
|
+
#requestRender(): void {
|
|
470
|
+
this.#nativeVersion++;
|
|
471
|
+
this.#tui.requestRender();
|
|
472
|
+
}
|
|
473
|
+
|
|
362
474
|
// ═══════════════════════════════════════════════════════════════════════
|
|
363
475
|
// Data pipeline
|
|
364
476
|
// ═══════════════════════════════════════════════════════════════════════
|
|
@@ -375,6 +487,7 @@ export class ModelHubComponent implements Component {
|
|
|
375
487
|
|
|
376
488
|
/** Rebuild items, roles, and the sidebar from the registry's in-memory state. */
|
|
377
489
|
#syncFromRegistryState(): void {
|
|
490
|
+
this.#nativeVersion++;
|
|
378
491
|
// A background rebuild (provider refresh, mutation) must not yank the
|
|
379
492
|
// sidebar viewport: remember the focused entry's screen row so it — or
|
|
380
493
|
// its nearest survivor — stays put after #buildSidebar reshuffles entries.
|
|
@@ -714,10 +827,23 @@ export class ModelHubComponent implements Component {
|
|
|
714
827
|
this.#rolesRows = rows;
|
|
715
828
|
}
|
|
716
829
|
|
|
830
|
+
/**
|
|
831
|
+
* Rows the Roles view lists on the All tab, separators aside: the picker's
|
|
832
|
+
* unfiltered `total`, counted in the same rows Tern counts as shown.
|
|
833
|
+
*/
|
|
834
|
+
#allRolesRowCount(): number {
|
|
835
|
+
const chains = this.#fallbackChains();
|
|
836
|
+
// "New role…" and "New fallback chain…", then each role and model chain with its fallback entries.
|
|
837
|
+
let count = 2;
|
|
838
|
+
for (const role of this.#visibleRoleIds()) count += 1 + (chains[role]?.length ?? 0);
|
|
839
|
+
for (const key in chains) if (key.includes("/")) count += 1 + chains[key].length;
|
|
840
|
+
return count;
|
|
841
|
+
}
|
|
842
|
+
|
|
717
843
|
/** Refresh roles + dependent state after a settings mutation (assign/unassign). */
|
|
718
844
|
#refreshAfterMutation(): void {
|
|
719
845
|
this.#syncFromRegistryState();
|
|
720
|
-
this.#
|
|
846
|
+
this.#requestRender();
|
|
721
847
|
}
|
|
722
848
|
|
|
723
849
|
/** Re-sync after an asynchronous callback finishes mutating settings. */
|
|
@@ -732,6 +858,7 @@ export class ModelHubComponent implements Component {
|
|
|
732
858
|
* never silently vanish.
|
|
733
859
|
*/
|
|
734
860
|
#onQueryChanged(query: string): void {
|
|
861
|
+
this.#nativeVersion++;
|
|
735
862
|
if (!query.trim()) {
|
|
736
863
|
this.#searchCounts = null;
|
|
737
864
|
this.#composeEntries();
|
|
@@ -782,13 +909,14 @@ export class ModelHubComponent implements Component {
|
|
|
782
909
|
// ═══════════════════════════════════════════════════════════════════════
|
|
783
910
|
|
|
784
911
|
#startRefreshSpinner(): void {
|
|
785
|
-
|
|
912
|
+
// Native surfaces clock the refresh spinner themselves.
|
|
913
|
+
if (this.#refreshSpinnerInterval || isNativeRendering()) return;
|
|
786
914
|
this.#refreshSpinnerInterval = setInterval(() => {
|
|
787
915
|
const frameCount = theme.spinnerFrames.length;
|
|
788
916
|
if (frameCount > 0) {
|
|
789
917
|
this.#refreshSpinnerFrame = (this.#refreshSpinnerFrame + 1) % frameCount;
|
|
790
918
|
}
|
|
791
|
-
this.#
|
|
919
|
+
this.#requestRender();
|
|
792
920
|
}, 80);
|
|
793
921
|
}
|
|
794
922
|
|
|
@@ -877,7 +1005,7 @@ export class ModelHubComponent implements Component {
|
|
|
877
1005
|
autoRefreshedProviders.add(providerId);
|
|
878
1006
|
void this.#refreshProviderInBackground(providerId, true);
|
|
879
1007
|
}
|
|
880
|
-
this.#
|
|
1008
|
+
this.#requestRender();
|
|
881
1009
|
}
|
|
882
1010
|
}
|
|
883
1011
|
|
|
@@ -929,6 +1057,8 @@ export class ModelHubComponent implements Component {
|
|
|
929
1057
|
// ═══════════════════════════════════════════════════════════════════════
|
|
930
1058
|
|
|
931
1059
|
#activateItem(item: ModelBrowserItem): void {
|
|
1060
|
+
// Also reached from the browser's own native list events, which bypass handleInput.
|
|
1061
|
+
this.#nativeVersion++;
|
|
932
1062
|
if (this.#assigning) {
|
|
933
1063
|
const target = this.#assigning;
|
|
934
1064
|
this.#assigning = null;
|
|
@@ -965,21 +1095,21 @@ export class ModelHubComponent implements Component {
|
|
|
965
1095
|
#finishAssignment(result: void | boolean | Promise<void | boolean>, onSuccess: () => void): void {
|
|
966
1096
|
if (!(result instanceof Promise)) {
|
|
967
1097
|
if (result !== false) onSuccess();
|
|
968
|
-
else this.#
|
|
1098
|
+
else this.#requestRender();
|
|
969
1099
|
return;
|
|
970
1100
|
}
|
|
971
1101
|
this.#assignmentPending = true;
|
|
972
|
-
this.#
|
|
1102
|
+
this.#requestRender();
|
|
973
1103
|
void result.then(
|
|
974
1104
|
applied => {
|
|
975
1105
|
this.#assignmentPending = false;
|
|
976
1106
|
if (this.#disposed) return;
|
|
977
1107
|
if (applied !== false) onSuccess();
|
|
978
|
-
else this.#
|
|
1108
|
+
else this.#requestRender();
|
|
979
1109
|
},
|
|
980
1110
|
() => {
|
|
981
1111
|
this.#assignmentPending = false;
|
|
982
|
-
if (!this.#disposed) this.#
|
|
1112
|
+
if (!this.#disposed) this.#requestRender();
|
|
983
1113
|
},
|
|
984
1114
|
);
|
|
985
1115
|
}
|
|
@@ -1562,6 +1692,7 @@ export class ModelHubComponent implements Component {
|
|
|
1562
1692
|
}
|
|
1563
1693
|
|
|
1564
1694
|
handleInput(data: string): void {
|
|
1695
|
+
this.#nativeVersion++;
|
|
1565
1696
|
if (this.#assignmentPending) {
|
|
1566
1697
|
if (matchesSelectCancel(data)) this.#callbacks.onCancel();
|
|
1567
1698
|
return;
|
|
@@ -1577,16 +1708,7 @@ export class ModelHubComponent implements Component {
|
|
|
1577
1708
|
}
|
|
1578
1709
|
|
|
1579
1710
|
if (matchesSelectCancel(data)) {
|
|
1580
|
-
|
|
1581
|
-
this.#cancelAssign();
|
|
1582
|
-
return;
|
|
1583
|
-
}
|
|
1584
|
-
const entry = this.#activeEntry();
|
|
1585
|
-
if (this.#isBrowserView(entry) && this.#browser.query.length > 0) {
|
|
1586
|
-
this.#browser.handleCancel();
|
|
1587
|
-
return;
|
|
1588
|
-
}
|
|
1589
|
-
this.#callbacks.onCancel();
|
|
1711
|
+
this.#cancel();
|
|
1590
1712
|
return;
|
|
1591
1713
|
}
|
|
1592
1714
|
|
|
@@ -1686,6 +1808,27 @@ export class ModelHubComponent implements Component {
|
|
|
1686
1808
|
}
|
|
1687
1809
|
}
|
|
1688
1810
|
|
|
1811
|
+
/** The cancel key's ladder: close a strip, leave assign mode, clear the query, then close the hub. */
|
|
1812
|
+
#cancel(): void {
|
|
1813
|
+
if (this.#assignmentPending) {
|
|
1814
|
+
this.#callbacks.onCancel();
|
|
1815
|
+
return;
|
|
1816
|
+
}
|
|
1817
|
+
if (this.#strip) {
|
|
1818
|
+
this.#closeStrip();
|
|
1819
|
+
return;
|
|
1820
|
+
}
|
|
1821
|
+
if (this.#assigning !== null) {
|
|
1822
|
+
this.#cancelAssign();
|
|
1823
|
+
return;
|
|
1824
|
+
}
|
|
1825
|
+
if (this.#isBrowserView(this.#activeEntry()) && this.#browser.query.length > 0) {
|
|
1826
|
+
this.#browser.handleCancel();
|
|
1827
|
+
return;
|
|
1828
|
+
}
|
|
1829
|
+
this.#callbacks.onCancel();
|
|
1830
|
+
}
|
|
1831
|
+
|
|
1689
1832
|
#isBrowserView(entry: SidebarEntry): boolean {
|
|
1690
1833
|
if (this.#assigning !== null) return true;
|
|
1691
1834
|
return entry.kind === "recent" || entry.kind === "all" || (entry.kind === "provider" && !entry.locked);
|
|
@@ -1804,70 +1947,70 @@ export class ModelHubComponent implements Component {
|
|
|
1804
1947
|
this.#roleIndex = this.#stepRoleIndex(this.#roleIndex, 1);
|
|
1805
1948
|
return;
|
|
1806
1949
|
}
|
|
1807
|
-
const row = this.#rolesRows[this.#roleIndex];
|
|
1808
|
-
const role = row?.kind === "role" ? row.role : undefined;
|
|
1809
1950
|
if (matchesKey(data, "enter") || matchesKey(data, "return") || data === "\n") {
|
|
1810
|
-
|
|
1951
|
+
this.#runRolesAction("pick");
|
|
1811
1952
|
return;
|
|
1812
1953
|
}
|
|
1813
1954
|
if (matchesKey(data, "backspace") || matchesKey(data, "delete")) {
|
|
1814
|
-
|
|
1815
|
-
else if (row?.kind === "fallback") this.#removeFallback(row);
|
|
1816
|
-
else if (row?.kind === "chainKey") this.#setFallbackChain(row.role, []);
|
|
1955
|
+
this.#runRolesAction("clear");
|
|
1817
1956
|
return;
|
|
1818
1957
|
}
|
|
1819
1958
|
// Reordering: [ / shift+↑ moves the row earlier, ] / shift+↓ later —
|
|
1820
1959
|
// cycle order on a role row, chain order on a fallback row.
|
|
1821
1960
|
if (matchesKey(data, "shift+up")) {
|
|
1822
|
-
|
|
1823
|
-
else if (row?.kind === "fallback") this.#moveFallback(row, -1);
|
|
1961
|
+
this.#runRolesAction("earlier");
|
|
1824
1962
|
return;
|
|
1825
1963
|
}
|
|
1826
1964
|
if (matchesKey(data, "shift+down")) {
|
|
1827
|
-
|
|
1828
|
-
else if (row?.kind === "fallback") this.#moveFallback(row, 1);
|
|
1965
|
+
this.#runRolesAction("later");
|
|
1829
1966
|
return;
|
|
1830
1967
|
}
|
|
1831
1968
|
const printable = extractPrintableText(data);
|
|
1832
|
-
if (printable
|
|
1833
|
-
|
|
1834
|
-
else if (row?.kind === "fallback") this.#removeFallback(row);
|
|
1835
|
-
else if (row?.kind === "chainKey") this.#setFallbackChain(row.role, []);
|
|
1836
|
-
return;
|
|
1837
|
-
}
|
|
1838
|
-
if (printable === "f") {
|
|
1839
|
-
if (row?.kind === "newFallback") this.#startAssignFallbackKey();
|
|
1840
|
-
else if (row && row.kind !== "newRole" && row.kind !== "separator") {
|
|
1841
|
-
this.#startAssignFallback(row.role, null);
|
|
1842
|
-
}
|
|
1843
|
-
return;
|
|
1969
|
+
if (printable !== undefined && Object.hasOwn(ROLES_ACTION_KEYS, printable)) {
|
|
1970
|
+
this.#runRolesAction(ROLES_ACTION_KEYS[printable]!);
|
|
1844
1971
|
}
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1972
|
+
}
|
|
1973
|
+
|
|
1974
|
+
/** One Roles-view command on the selected row (keys and action-bar buttons share it). */
|
|
1975
|
+
#runRolesAction(action: RolesAction): void {
|
|
1976
|
+
const row = this.#rolesRows[this.#roleIndex];
|
|
1977
|
+
const role = row?.kind === "role" ? row.role : undefined;
|
|
1978
|
+
switch (action) {
|
|
1979
|
+
case "pick":
|
|
1980
|
+
if (row) this.#activateRolesRow(row);
|
|
1981
|
+
return;
|
|
1982
|
+
case "clear":
|
|
1983
|
+
if (role) this.#unassignRole(role);
|
|
1984
|
+
else if (row?.kind === "fallback") this.#removeFallback(row);
|
|
1985
|
+
else if (row?.kind === "chainKey") this.#setFallbackChain(row.role, []);
|
|
1986
|
+
return;
|
|
1987
|
+
case "fallback":
|
|
1988
|
+
if (row?.kind === "newFallback") this.#startAssignFallbackKey();
|
|
1989
|
+
else if (row && row.kind !== "newRole" && row.kind !== "separator") {
|
|
1990
|
+
this.#startAssignFallback(row.role, null);
|
|
1991
|
+
}
|
|
1992
|
+
return;
|
|
1993
|
+
case "cycle":
|
|
1994
|
+
if (role) this.#toggleCycleMembership(role);
|
|
1995
|
+
return;
|
|
1996
|
+
case "earlier":
|
|
1997
|
+
case "later": {
|
|
1998
|
+
const delta = action === "earlier" ? -1 : 1;
|
|
1999
|
+
if (role) this.#moveCycleMembership(role, delta);
|
|
2000
|
+
else if (row?.kind === "fallback") this.#moveFallback(row, delta);
|
|
2001
|
+
return;
|
|
1869
2002
|
}
|
|
1870
|
-
|
|
2003
|
+
case "new":
|
|
2004
|
+
this.#openRoleNameStrip();
|
|
2005
|
+
return;
|
|
2006
|
+
case "thinking":
|
|
2007
|
+
if (role) {
|
|
2008
|
+
const target = this.#roleThinkingTarget(role);
|
|
2009
|
+
if (target) this.#openThinkingStrip(target.item, role, true, target.scope);
|
|
2010
|
+
} else if (row?.kind === "fallback") {
|
|
2011
|
+
this.#openFallbackThinkingStrip(row);
|
|
2012
|
+
}
|
|
2013
|
+
return;
|
|
1871
2014
|
}
|
|
1872
2015
|
}
|
|
1873
2016
|
|
|
@@ -1937,17 +2080,7 @@ export class ModelHubComponent implements Component {
|
|
|
1937
2080
|
|
|
1938
2081
|
if (overSidebar) {
|
|
1939
2082
|
const index = this.#sidebarEntryIndexAt(contentLine);
|
|
1940
|
-
|
|
1941
|
-
if (clicked && clicked.kind !== "separator") {
|
|
1942
|
-
const already = clicked.id === this.#activeEntryId;
|
|
1943
|
-
if (clicked.kind === "roles") this.#assigning = null;
|
|
1944
|
-
this.#setActiveEntry(clicked.id);
|
|
1945
|
-
// A click on Roles is a deliberate dive into the rows.
|
|
1946
|
-
if (clicked.kind === "roles") this.#focus = "list";
|
|
1947
|
-
if (already && clicked.kind === "provider" && clicked.locked) {
|
|
1948
|
-
this.#requestLogin(clicked);
|
|
1949
|
-
}
|
|
1950
|
-
}
|
|
2083
|
+
this.#clickSidebarEntry(index !== null ? this.#entries[index] : undefined);
|
|
1951
2084
|
return true;
|
|
1952
2085
|
}
|
|
1953
2086
|
|
|
@@ -1977,6 +2110,19 @@ export class ModelHubComponent implements Component {
|
|
|
1977
2110
|
return true;
|
|
1978
2111
|
}
|
|
1979
2112
|
|
|
2113
|
+
/** Pointer activation of a sidebar entry: pick the scope; a second click on a locked provider logs in. */
|
|
2114
|
+
#clickSidebarEntry(clicked: SidebarEntry | undefined): void {
|
|
2115
|
+
if (!clicked || clicked.kind === "separator") return;
|
|
2116
|
+
const already = clicked.id === this.#activeEntryId;
|
|
2117
|
+
if (clicked.kind === "roles") this.#assigning = null;
|
|
2118
|
+
this.#setActiveEntry(clicked.id);
|
|
2119
|
+
// A click on Roles is a deliberate dive into the rows.
|
|
2120
|
+
if (clicked.kind === "roles") this.#focus = "list";
|
|
2121
|
+
if (already && clicked.kind === "provider" && clicked.locked) {
|
|
2122
|
+
this.#requestLogin(clicked);
|
|
2123
|
+
}
|
|
2124
|
+
}
|
|
2125
|
+
|
|
1980
2126
|
/** Map a content-line index to a sidebar entry index (accounting for scroll). */
|
|
1981
2127
|
#sidebarEntryIndexAt(contentLine: number): number | null {
|
|
1982
2128
|
const index = this.#frame.sidebarScroll + contentLine;
|
|
@@ -2438,4 +2584,1182 @@ export class ModelHubComponent implements Component {
|
|
|
2438
2584
|
const height = Math.max(16, this.#tui.terminal?.rows || process.stdout.rows || 40);
|
|
2439
2585
|
return this.#frame.render(width, height, this.#entries, this.#renderFooter(width - 4));
|
|
2440
2586
|
}
|
|
2587
|
+
|
|
2588
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
2589
|
+
// Native (TSP) description
|
|
2590
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
2591
|
+
|
|
2592
|
+
/** A `picker` is its own sheet: the backend mounts it in `layer` without an `overlay` wrapper. */
|
|
2593
|
+
nativeSheet(cx: DescribeContext): boolean {
|
|
2594
|
+
return cx.supports("picker");
|
|
2595
|
+
}
|
|
2596
|
+
|
|
2597
|
+
describe(cx: DescribeContext): NativeNode {
|
|
2598
|
+
const usePicker = cx.supports("picker");
|
|
2599
|
+
const cached = this.#nativeCache;
|
|
2600
|
+
if (cached?.version === this.#nativeVersion && cached.picker === usePicker) return cached.node;
|
|
2601
|
+
if (usePicker) {
|
|
2602
|
+
const described = this.#describePicker();
|
|
2603
|
+
this.#nativeCache = { version: this.#nativeVersion, picker: true, node: described };
|
|
2604
|
+
return described;
|
|
2605
|
+
}
|
|
2606
|
+
const footer: NativeChild[] = [];
|
|
2607
|
+
const strip = this.#describeStrip();
|
|
2608
|
+
if (strip) footer.push(strip);
|
|
2609
|
+
footer.push(hintsRow(this.#footerHints()));
|
|
2610
|
+
const described = describeHubFrame(
|
|
2611
|
+
"omp.overlay.model-hub",
|
|
2612
|
+
"Models",
|
|
2613
|
+
describeHubSidebar(this.#entries, this.#activeEntryId, this.#nativeSidebarStyle, "scopes"),
|
|
2614
|
+
this.#describeBody(),
|
|
2615
|
+
node("col", { gap: "xs" }, footer, "footer"),
|
|
2616
|
+
);
|
|
2617
|
+
this.#nativeCache = { version: this.#nativeVersion, picker: false, node: described };
|
|
2618
|
+
return described;
|
|
2619
|
+
}
|
|
2620
|
+
|
|
2621
|
+
handleNativeEvent(event: NativeUiEvent): void {
|
|
2622
|
+
const picked = pickerEvent(event);
|
|
2623
|
+
if (picked) {
|
|
2624
|
+
this.#handlePickerEvent(picked);
|
|
2625
|
+
this.#requestRender();
|
|
2626
|
+
return;
|
|
2627
|
+
}
|
|
2628
|
+
// Same gate as the mouse router: nothing is interactive while an assignment applies.
|
|
2629
|
+
if (this.#assignmentPending) return;
|
|
2630
|
+
const target = event.key.slice(event.key.lastIndexOf("/") + 1);
|
|
2631
|
+
if (event.type === "action") {
|
|
2632
|
+
const entry = this.#activeEntry();
|
|
2633
|
+
if (target !== "login" || event.act !== "login") return;
|
|
2634
|
+
if (entry.kind !== "provider" || !entry.locked || this.#assigning !== null) return;
|
|
2635
|
+
this.#requestLogin(entry);
|
|
2636
|
+
} else if (event.type === "select" || event.type === "activate") {
|
|
2637
|
+
switch (target) {
|
|
2638
|
+
case "scopes":
|
|
2639
|
+
this.#clickSidebarEntry(this.#entries.find(entry => entry.id === event.item));
|
|
2640
|
+
break;
|
|
2641
|
+
case "roles": {
|
|
2642
|
+
if (this.#activeEntry().kind !== "roles" || this.#assigning !== null) return;
|
|
2643
|
+
const index = this.#rolesRows.findIndex((rowDef, i) => rolesRowKey(rowDef, i) === event.item);
|
|
2644
|
+
const rowDef = this.#rolesRows[index];
|
|
2645
|
+
if (!rowDef || rowDef.kind === "separator") return;
|
|
2646
|
+
this.#focus = "list";
|
|
2647
|
+
this.#roleIndex = index;
|
|
2648
|
+
this.#activateRolesRow(rowDef);
|
|
2649
|
+
break;
|
|
2650
|
+
}
|
|
2651
|
+
case "roleTabs": {
|
|
2652
|
+
const tab = ROLE_TABS.find(candidate => candidate === event.item);
|
|
2653
|
+
if (!tab) return;
|
|
2654
|
+
this.#roleTab = tab;
|
|
2655
|
+
this.#roleIndex = 0;
|
|
2656
|
+
this.#roleScrollStart = 0;
|
|
2657
|
+
this.#buildRolesRows();
|
|
2658
|
+
break;
|
|
2659
|
+
}
|
|
2660
|
+
case "kinds": {
|
|
2661
|
+
const kind = MODEL_KIND_TABS.find(candidate => candidate === event.item);
|
|
2662
|
+
if (!kind) return;
|
|
2663
|
+
this.#modelKindTab = kind;
|
|
2664
|
+
this.#applyModelKind();
|
|
2665
|
+
break;
|
|
2666
|
+
}
|
|
2667
|
+
case "strip": {
|
|
2668
|
+
// A chip click picks and applies it, like the footer mouse path.
|
|
2669
|
+
const strip = this.#strip;
|
|
2670
|
+
const index = Number(event.item);
|
|
2671
|
+
if (!strip || strip.kind === "roleName" || !Number.isInteger(index) || !strip.chips[index]) return;
|
|
2672
|
+
strip.index = index;
|
|
2673
|
+
this.#activateStripChip();
|
|
2674
|
+
break;
|
|
2675
|
+
}
|
|
2676
|
+
default:
|
|
2677
|
+
return;
|
|
2678
|
+
}
|
|
2679
|
+
} else {
|
|
2680
|
+
return;
|
|
2681
|
+
}
|
|
2682
|
+
this.#requestRender();
|
|
2683
|
+
}
|
|
2684
|
+
|
|
2685
|
+
/** Sidebar decoration for the native list: refreshing providers show an ellipsis, the status row spins. */
|
|
2686
|
+
#nativeSidebarStyle = (entry: SidebarEntry, index: number): Pick<SidebarStyle, "icon" | "annotation" | "muted"> => {
|
|
2687
|
+
const style = this.#sidebarStyle(entry, index);
|
|
2688
|
+
const refreshing = entry.providerId !== undefined && this.#refreshingProviders.has(entry.providerId);
|
|
2689
|
+
return { ...style, annotation: refreshing ? "…" : plainText(style.annotation) };
|
|
2690
|
+
};
|
|
2691
|
+
|
|
2692
|
+
#describeBody(): NativeNode {
|
|
2693
|
+
const children: NativeChild[] = [this.#describeStatus()];
|
|
2694
|
+
const entry = this.#activeEntry();
|
|
2695
|
+
if (entry.kind === "roles" && this.#assigning === null) {
|
|
2696
|
+
children.push(...this.#describeRolesView());
|
|
2697
|
+
} else if (entry.kind === "provider" && entry.locked && this.#assigning === null) {
|
|
2698
|
+
children.push(this.#describeLockedView(entry));
|
|
2699
|
+
} else {
|
|
2700
|
+
children.push(
|
|
2701
|
+
node(
|
|
2702
|
+
"row",
|
|
2703
|
+
{ gap: "sm", align: "center" },
|
|
2704
|
+
[
|
|
2705
|
+
text([span("Kind:", "dim")]),
|
|
2706
|
+
node(
|
|
2707
|
+
"tabs",
|
|
2708
|
+
{ items: MODEL_KIND_TABS.map(kind => ({ id: kind, label: kind })), active: this.#modelKindTab },
|
|
2709
|
+
undefined,
|
|
2710
|
+
"kinds",
|
|
2711
|
+
),
|
|
2712
|
+
],
|
|
2713
|
+
"kindTabs",
|
|
2714
|
+
),
|
|
2715
|
+
this.#browser,
|
|
2716
|
+
);
|
|
2717
|
+
}
|
|
2718
|
+
return node("col", { gap: "sm", grow: 1 }, children, "body");
|
|
2719
|
+
}
|
|
2720
|
+
|
|
2721
|
+
#describeStatus(): NativeNode {
|
|
2722
|
+
if (this.#assignmentPending) {
|
|
2723
|
+
return node("spinner", { label: [span("Applying model…", "accent")] }, undefined, "status");
|
|
2724
|
+
}
|
|
2725
|
+
let spans: TspSpan[];
|
|
2726
|
+
const assigning = this.#assigning;
|
|
2727
|
+
if (assigning !== null) {
|
|
2728
|
+
if (assigning.kind === "fallbackKey") {
|
|
2729
|
+
spans = [span("New fallback chain — pick the model it protects", "accent")];
|
|
2730
|
+
} else {
|
|
2731
|
+
const info = this.#settings.getRoleInfo(assigning.role);
|
|
2732
|
+
const label = info.tag ?? info.name ?? assigning.role;
|
|
2733
|
+
const verb =
|
|
2734
|
+
assigning.kind === "role"
|
|
2735
|
+
? "Assigning"
|
|
2736
|
+
: assigning.index === null
|
|
2737
|
+
? "Adding fallback for"
|
|
2738
|
+
: "Replacing fallback of";
|
|
2739
|
+
spans = [span(`${verb} `, "accent"), span(label, "accent strong")];
|
|
2740
|
+
}
|
|
2741
|
+
return node("text", { spans, truncate: "end" }, undefined, "status");
|
|
2742
|
+
}
|
|
2743
|
+
const entry = this.#activeEntry();
|
|
2744
|
+
if (this.#configError && entry.kind !== "provider") {
|
|
2745
|
+
return node("text", { spans: [span(this.#configError, "error")], wrap: "word" }, undefined, "status");
|
|
2746
|
+
}
|
|
2747
|
+
const scopedSuffix = this.#scopedModels.length > 0 ? " · --models scope" : "";
|
|
2748
|
+
switch (entry.kind) {
|
|
2749
|
+
case "recent":
|
|
2750
|
+
spans = [span(`Recently used models${scopedSuffix}`, "muted")];
|
|
2751
|
+
break;
|
|
2752
|
+
case "roles":
|
|
2753
|
+
spans = [
|
|
2754
|
+
span("Model roles — ", "muted"),
|
|
2755
|
+
span("f", "key"),
|
|
2756
|
+
span(" adds a retry fallback, cleared roles fall back to auto-selection", "muted"),
|
|
2757
|
+
];
|
|
2758
|
+
break;
|
|
2759
|
+
case "provider":
|
|
2760
|
+
if (entry.locked) {
|
|
2761
|
+
spans = [span(`${entry.label} · not configured`, "muted")];
|
|
2762
|
+
} else if (entry.providerId && this.#refreshingProviders.has(entry.providerId)) {
|
|
2763
|
+
return node(
|
|
2764
|
+
"spinner",
|
|
2765
|
+
{ label: [span(`${entry.label} · refreshing model list…`, "muted")] },
|
|
2766
|
+
undefined,
|
|
2767
|
+
"status",
|
|
2768
|
+
);
|
|
2769
|
+
} else {
|
|
2770
|
+
spans = [span(`${entry.label} · ${entry.annotation ?? "0"} models${scopedSuffix}`, "muted")];
|
|
2771
|
+
}
|
|
2772
|
+
break;
|
|
2773
|
+
default:
|
|
2774
|
+
spans = [span(`All available models${scopedSuffix}`, "muted")];
|
|
2775
|
+
break;
|
|
2776
|
+
}
|
|
2777
|
+
return node("text", { spans, truncate: "end" }, undefined, "status");
|
|
2778
|
+
}
|
|
2779
|
+
|
|
2780
|
+
#describeRolesView(): NativeNode[] {
|
|
2781
|
+
const tabs = node(
|
|
2782
|
+
"row",
|
|
2783
|
+
{ gap: "sm", align: "center" },
|
|
2784
|
+
[
|
|
2785
|
+
text([span("Roles:", "dim")]),
|
|
2786
|
+
node(
|
|
2787
|
+
"tabs",
|
|
2788
|
+
{
|
|
2789
|
+
items: ROLE_TABS.map(tab => ({ id: tab, label: tab === "kind" ? "kinds" : tab })),
|
|
2790
|
+
active: this.#roleTab,
|
|
2791
|
+
},
|
|
2792
|
+
undefined,
|
|
2793
|
+
"roleTabs",
|
|
2794
|
+
),
|
|
2795
|
+
],
|
|
2796
|
+
"roleTabsRow",
|
|
2797
|
+
);
|
|
2798
|
+
const cycleOrder = this.#cycleOrder();
|
|
2799
|
+
const items = this.#rolesRows.map((rowDef, index) => this.#describeRolesRow(rowDef, index, cycleOrder));
|
|
2800
|
+
const selectedRow = this.#rolesRows[this.#roleIndex];
|
|
2801
|
+
const list = node(
|
|
2802
|
+
"list",
|
|
2803
|
+
{
|
|
2804
|
+
selected: selectedRow ? rolesRowKey(selectedRow, this.#roleIndex) : null,
|
|
2805
|
+
virtual: true,
|
|
2806
|
+
grow: 1,
|
|
2807
|
+
tone: this.#focus === "list" ? "accent" : undefined,
|
|
2808
|
+
aria: "Roles",
|
|
2809
|
+
},
|
|
2810
|
+
items,
|
|
2811
|
+
"roles",
|
|
2812
|
+
);
|
|
2813
|
+
return [tabs, list, this.#describeCycle(cycleOrder)];
|
|
2814
|
+
}
|
|
2815
|
+
|
|
2816
|
+
#describeRolesRow(rowDef: RolesRow, index: number, cycleOrder: readonly string[]): NativeNode {
|
|
2817
|
+
const key = rolesRowKey(rowDef, index);
|
|
2818
|
+
switch (rowDef.kind) {
|
|
2819
|
+
case "separator":
|
|
2820
|
+
return node("rule", undefined, undefined, key);
|
|
2821
|
+
case "newRole":
|
|
2822
|
+
return node("item", { label: [span("+ New role…", "dim")] }, undefined, key);
|
|
2823
|
+
case "newFallback":
|
|
2824
|
+
return node("item", { label: [span("+ New fallback…", "dim")] }, undefined, key);
|
|
2825
|
+
case "chainKey": {
|
|
2826
|
+
const slash = rowDef.role.lastIndexOf("/");
|
|
2827
|
+
return node(
|
|
2828
|
+
"item",
|
|
2829
|
+
{
|
|
2830
|
+
label: [
|
|
2831
|
+
span(`${theme.status.shadowed} `, "dim"),
|
|
2832
|
+
span(rowDef.role.slice(0, slash + 1), "dim"),
|
|
2833
|
+
span(rowDef.role.slice(slash + 1)),
|
|
2834
|
+
],
|
|
2835
|
+
},
|
|
2836
|
+
undefined,
|
|
2837
|
+
key,
|
|
2838
|
+
);
|
|
2839
|
+
}
|
|
2840
|
+
case "fallback":
|
|
2841
|
+
return node("item", { label: [span(" ↳ ", "dim"), span(rowDef.selector, "muted")] }, undefined, key);
|
|
2842
|
+
case "role":
|
|
2843
|
+
break;
|
|
2844
|
+
}
|
|
2845
|
+
const role = rowDef.role;
|
|
2846
|
+
const info = this.#settings.getRoleInfo(role);
|
|
2847
|
+
const assignment = this.#roles[role];
|
|
2848
|
+
const tag = info.tag ?? info.name ?? role;
|
|
2849
|
+
let label: TspSpan[];
|
|
2850
|
+
let detail: TspSpan[];
|
|
2851
|
+
const value: TspSpan[] = [];
|
|
2852
|
+
if (assignment && !assignment.autoSelected) {
|
|
2853
|
+
const color = info.color ?? "muted";
|
|
2854
|
+
label = [span(`${theme.status.enabled} `, color), span(tag, color)];
|
|
2855
|
+
detail = [span(`${assignment.model.provider}/`, "dim"), span(assignment.model.id)];
|
|
2856
|
+
if (assignment.thinkingLevel !== ThinkingLevel.Inherit) {
|
|
2857
|
+
const glyph = thinkingLevelGlyph(assignment.thinkingLevel, theme);
|
|
2858
|
+
const levelLabel = getConfiguredThinkingLevelMetadata(assignment.thinkingLevel).label;
|
|
2859
|
+
value.push(span(glyph ? `${glyph} ${levelLabel}` : levelLabel, "dim"));
|
|
2860
|
+
}
|
|
2861
|
+
} else {
|
|
2862
|
+
label = [span(`${theme.status.shadowed} `, "dim"), span(tag, "dim")];
|
|
2863
|
+
detail = assignment
|
|
2864
|
+
? [span(`auto → ${assignment.model.provider}/${assignment.model.id}`, "dim")]
|
|
2865
|
+
: [span("—", "dim")];
|
|
2866
|
+
}
|
|
2867
|
+
// Quick-cycle membership badge (`⟳ 2` = second stop of the ctrl+p cycle).
|
|
2868
|
+
const cycleIndex = cycleOrder.indexOf(role);
|
|
2869
|
+
if (cycleIndex >= 0) {
|
|
2870
|
+
if (value.length > 0) value.push(span(" "));
|
|
2871
|
+
value.push(span(`${theme.icon.loop} ${cycleIndex + 1}`, "accent"));
|
|
2872
|
+
}
|
|
2873
|
+
return node("item", { label, detail, value: value.length > 0 ? value : undefined }, undefined, key);
|
|
2874
|
+
}
|
|
2875
|
+
|
|
2876
|
+
/** Live preview of the quick-switch cycle; the selected row's role is emphasized. */
|
|
2877
|
+
#describeCycle(cycleOrder: readonly string[]): NativeNode {
|
|
2878
|
+
const [cycleKey] = boundKeys("app.model.cycleForward", ["ctrl+p"]);
|
|
2879
|
+
const keycap: NativeChild[] = cycleKey ? [kbd(cycleKey)] : [];
|
|
2880
|
+
if (cycleOrder.length === 0) {
|
|
2881
|
+
return node(
|
|
2882
|
+
"row",
|
|
2883
|
+
{ gap: "xs", align: "center", wrap: true },
|
|
2884
|
+
[
|
|
2885
|
+
...keycap,
|
|
2886
|
+
text([span("cycle is empty — press ", "dim"), span("c", "key"), span(" on a role to add it", "dim")]),
|
|
2887
|
+
],
|
|
2888
|
+
"cycle",
|
|
2889
|
+
);
|
|
2890
|
+
}
|
|
2891
|
+
const selectedRow = this.#rolesRows[this.#roleIndex];
|
|
2892
|
+
const selectedRole =
|
|
2893
|
+
selectedRow && (selectedRow.kind === "role" || selectedRow.kind === "fallback") ? selectedRow.role : "";
|
|
2894
|
+
const track: TspSpan[] = [];
|
|
2895
|
+
for (const role of cycleOrder) {
|
|
2896
|
+
if (track.length > 0) track.push(span(" › ", "dim"));
|
|
2897
|
+
track.push(span(role, role === selectedRole ? "accent strong" : "muted"));
|
|
2898
|
+
}
|
|
2899
|
+
return node(
|
|
2900
|
+
"row",
|
|
2901
|
+
{ gap: "xs", align: "center", wrap: true },
|
|
2902
|
+
[...keycap, text([span("cycle:", "dim")]), text(track, { wrap: "word" })],
|
|
2903
|
+
"cycle",
|
|
2904
|
+
);
|
|
2905
|
+
}
|
|
2906
|
+
|
|
2907
|
+
#describeLockedView(entry: SidebarEntry): NativeNode {
|
|
2908
|
+
const children: NativeChild[] = [
|
|
2909
|
+
text([span(`${entry.label} has no credentials configured`, "warning")], { wrap: "word" }),
|
|
2910
|
+
];
|
|
2911
|
+
const envVars = entry.providerId ? (providerEntry(entry.providerId)?.envVars ?? []) : [];
|
|
2912
|
+
children.push(
|
|
2913
|
+
text(
|
|
2914
|
+
[
|
|
2915
|
+
span(
|
|
2916
|
+
envVars.length > 0
|
|
2917
|
+
? `Set ${envVars.join(" or ")} in your environment, or add a key in config.`
|
|
2918
|
+
: "Add an API key for this provider in config.",
|
|
2919
|
+
"muted",
|
|
2920
|
+
),
|
|
2921
|
+
],
|
|
2922
|
+
{ wrap: "word" },
|
|
2923
|
+
),
|
|
2924
|
+
);
|
|
2925
|
+
if (entry.oauth) {
|
|
2926
|
+
children.push(
|
|
2927
|
+
node(
|
|
2928
|
+
"row",
|
|
2929
|
+
{ gap: "sm", align: "center", actions: { click: "login" } },
|
|
2930
|
+
[text([span("Log in with OAuth", "accent")]), node("kbd", { keys: ["enter"] })],
|
|
2931
|
+
"login",
|
|
2932
|
+
),
|
|
2933
|
+
);
|
|
2934
|
+
}
|
|
2935
|
+
const catalogCount = entry.catalogCount ?? 0;
|
|
2936
|
+
if (catalogCount > 0) {
|
|
2937
|
+
const models = this.#scopedModels.length > 0 ? [] : this.#registry.getAll("all");
|
|
2938
|
+
const items: NativeNode[] = [];
|
|
2939
|
+
for (const model of models) {
|
|
2940
|
+
if (model.provider !== entry.providerId) continue;
|
|
2941
|
+
items.push(node("item", { label: model.id, disabled: true }, undefined, model.id));
|
|
2942
|
+
}
|
|
2943
|
+
children.push(
|
|
2944
|
+
node(
|
|
2945
|
+
"section",
|
|
2946
|
+
{ head: [span(`${catalogCount} models in catalog`, "dim")] },
|
|
2947
|
+
[node("list", { selected: null, virtual: true }, items, "catalog")],
|
|
2948
|
+
"catalog",
|
|
2949
|
+
),
|
|
2950
|
+
);
|
|
2951
|
+
}
|
|
2952
|
+
return node("col", { gap: "sm", grow: 1 }, children, "locked");
|
|
2953
|
+
}
|
|
2954
|
+
|
|
2955
|
+
// ─── Picker (data-first `picker` kind, NATIVE_REDESIGN.md §4.7) ─────────
|
|
2956
|
+
|
|
2957
|
+
#describePicker(): NativeNode {
|
|
2958
|
+
const entry = this.#activeEntry();
|
|
2959
|
+
const rolesView = entry.kind === "roles" && this.#assigning === null;
|
|
2960
|
+
const lockedView = entry.kind === "provider" && entry.locked === true && this.#assigning === null;
|
|
2961
|
+
const strip = this.#strip;
|
|
2962
|
+
const props: TspPickerProps = {
|
|
2963
|
+
title: "Models",
|
|
2964
|
+
subtitle: this.#pickerSubtitle(entry, rolesView),
|
|
2965
|
+
icon: "cpu",
|
|
2966
|
+
noun: rolesView ? "roles" : "models",
|
|
2967
|
+
size: "lg",
|
|
2968
|
+
layout: "rows",
|
|
2969
|
+
preview: "side",
|
|
2970
|
+
query: this.#browser.query,
|
|
2971
|
+
cursor: this.#browser.cursor,
|
|
2972
|
+
placeholder: "Search models…",
|
|
2973
|
+
scopes: this.#pickerScopes(),
|
|
2974
|
+
scope: entry.id,
|
|
2975
|
+
actions: this.#pickerActions(entry, rolesView, lockedView),
|
|
2976
|
+
strip: strip ? this.#pickerStrip(strip) : null,
|
|
2977
|
+
focus: strip ? "strip" : this.#focus === "scope" ? "scopes" : "list",
|
|
2978
|
+
};
|
|
2979
|
+
if (rolesView) {
|
|
2980
|
+
const row = this.#rolesRows[this.#roleIndex];
|
|
2981
|
+
return picker(
|
|
2982
|
+
{
|
|
2983
|
+
...props,
|
|
2984
|
+
tabs: ROLE_TABS.map(tab => ({
|
|
2985
|
+
id: tab,
|
|
2986
|
+
label: tab === "all" ? "All" : tab === "chat" ? "Chat" : "Kinds",
|
|
2987
|
+
count: this.#visibleRoleIds().filter(
|
|
2988
|
+
role => tab === "all" || this.#settings.getRoleInfo(role).section === tab,
|
|
2989
|
+
).length,
|
|
2990
|
+
})),
|
|
2991
|
+
tab: this.#roleTab,
|
|
2992
|
+
columns: ROLE_PICKER_COLUMNS,
|
|
2993
|
+
items: this.#rolePickerItems(),
|
|
2994
|
+
order: this.#rolePickerOrder(),
|
|
2995
|
+
selected: row && row.kind !== "separator" ? rolesRowKey(row, this.#roleIndex) : null,
|
|
2996
|
+
total: this.#allRolesRowCount(),
|
|
2997
|
+
},
|
|
2998
|
+
this.#rolePreview(row),
|
|
2999
|
+
);
|
|
3000
|
+
}
|
|
3001
|
+
const catalogue = this.#browser.pickerItems(this.#availableItems);
|
|
3002
|
+
const current = this.#currentSelector ? [this.#currentSelector] : undefined;
|
|
3003
|
+
if (lockedView) {
|
|
3004
|
+
return picker(
|
|
3005
|
+
{
|
|
3006
|
+
...props,
|
|
3007
|
+
columns: MODEL_PICKER_COLUMNS,
|
|
3008
|
+
...catalogue,
|
|
3009
|
+
order: [],
|
|
3010
|
+
selected: null,
|
|
3011
|
+
total: entry.catalogCount ?? 0,
|
|
3012
|
+
empty: this.#lockedMessage(entry),
|
|
3013
|
+
...(current ? { current } : {}),
|
|
3014
|
+
},
|
|
3015
|
+
this.#lockedPreview(entry),
|
|
3016
|
+
);
|
|
3017
|
+
}
|
|
3018
|
+
const query = this.#browser.query.trim();
|
|
3019
|
+
const view = this.#browser.pickerOrder({
|
|
3020
|
+
providers: entry.kind === "all" && query.length === 0,
|
|
3021
|
+
rest: entry.kind === "provider" ? entry.label : query ? "Matches" : "All models",
|
|
3022
|
+
});
|
|
3023
|
+
const refreshing =
|
|
3024
|
+
this.#catalogRefreshing ||
|
|
3025
|
+
(entry.kind === "provider" && this.#refreshingProviders.has(entry.providerId ?? ""));
|
|
3026
|
+
const state =
|
|
3027
|
+
view.count > 0 || query
|
|
3028
|
+
? "ready"
|
|
3029
|
+
: refreshing
|
|
3030
|
+
? "loading"
|
|
3031
|
+
: this.#configError && !this.#assigning
|
|
3032
|
+
? "error"
|
|
3033
|
+
: "ready";
|
|
3034
|
+
return picker(
|
|
3035
|
+
{
|
|
3036
|
+
...props,
|
|
3037
|
+
tabs: this.#pickerKindTabs(),
|
|
3038
|
+
tab: this.#modelKindTab,
|
|
3039
|
+
columns: MODEL_PICKER_COLUMNS,
|
|
3040
|
+
...catalogue,
|
|
3041
|
+
order: view.order,
|
|
3042
|
+
...(view.hits ? { hits: view.hits } : {}),
|
|
3043
|
+
selected: this.#browser.pickerSelected,
|
|
3044
|
+
...(current ? { current } : {}),
|
|
3045
|
+
total: this.#browser.baseCount,
|
|
3046
|
+
state,
|
|
3047
|
+
...(state === "error" && this.#configError ? { message: this.#configError } : {}),
|
|
3048
|
+
empty:
|
|
3049
|
+
this.#emptyStateMessage()?.trim() ??
|
|
3050
|
+
(entry.kind === "provider" ? `No models from ${entry.label} yet` : "No models available in this scope"),
|
|
3051
|
+
},
|
|
3052
|
+
this.#browser.pickerPreview("full", this.#currentSelector),
|
|
3053
|
+
);
|
|
3054
|
+
}
|
|
3055
|
+
|
|
3056
|
+
/** What the hub is doing when it is more than browsing: applying, assigning, refreshing, a config error. */
|
|
3057
|
+
#pickerSubtitle(entry: SidebarEntry, rolesView: boolean): TspPickerProps["subtitle"] {
|
|
3058
|
+
if (this.#assignmentPending) return "Applying model…";
|
|
3059
|
+
const assigning = this.#assigning;
|
|
3060
|
+
if (assigning !== null) {
|
|
3061
|
+
if (assigning.kind === "fallbackKey") return "New fallback chain — pick the model it protects";
|
|
3062
|
+
const info = this.#settings.getRoleInfo(assigning.role);
|
|
3063
|
+
const label = info.tag ?? info.name ?? assigning.role;
|
|
3064
|
+
const verb =
|
|
3065
|
+
assigning.kind === "role"
|
|
3066
|
+
? "Assigning"
|
|
3067
|
+
: assigning.index === null
|
|
3068
|
+
? "Adding fallback for"
|
|
3069
|
+
: "Replacing fallback of";
|
|
3070
|
+
return [span(`${verb} `), span(label, "strong")];
|
|
3071
|
+
}
|
|
3072
|
+
if (this.#configError && entry.kind !== "provider") return [span(this.#configError, "error")];
|
|
3073
|
+
if (entry.kind === "provider" && entry.providerId && this.#refreshingProviders.has(entry.providerId)) {
|
|
3074
|
+
return `${entry.label} · refreshing model list…`;
|
|
3075
|
+
}
|
|
3076
|
+
if (this.#scopedModels.length > 0) return "--models scope";
|
|
3077
|
+
if (rolesView) return "Cleared roles fall back to auto-selection";
|
|
3078
|
+
return undefined;
|
|
3079
|
+
}
|
|
3080
|
+
|
|
3081
|
+
/** The sidebar as picker scopes: Roles, All models, then signed-in and signed-out providers. */
|
|
3082
|
+
#pickerScopes(): TspPickerScope[] {
|
|
3083
|
+
const counts = this.#searchCounts;
|
|
3084
|
+
const oauthIds = new Set(getOAuthProviders().map(provider => provider.id));
|
|
3085
|
+
const scopes: TspPickerScope[] = [];
|
|
3086
|
+
for (const entry of this.#entries) {
|
|
3087
|
+
switch (entry.kind) {
|
|
3088
|
+
case "separator":
|
|
3089
|
+
break;
|
|
3090
|
+
case "roles":
|
|
3091
|
+
scopes.push({ id: entry.id, label: "Roles", icon: "sparkles", count: this.#visibleRoleIds().length });
|
|
3092
|
+
break;
|
|
3093
|
+
case "all":
|
|
3094
|
+
scopes.push({
|
|
3095
|
+
id: entry.id,
|
|
3096
|
+
label: "All models",
|
|
3097
|
+
icon: "list",
|
|
3098
|
+
count: counts ? this.#searchTotal : this.#availableItems.length,
|
|
3099
|
+
});
|
|
3100
|
+
break;
|
|
3101
|
+
case "recent":
|
|
3102
|
+
scopes.push({
|
|
3103
|
+
id: entry.id,
|
|
3104
|
+
label: "Recent",
|
|
3105
|
+
icon: "clock",
|
|
3106
|
+
count: counts ? this.#recentSearchCount : this.#recentItems.length,
|
|
3107
|
+
});
|
|
3108
|
+
break;
|
|
3109
|
+
case "provider": {
|
|
3110
|
+
const providerId = entry.providerId ?? entry.label;
|
|
3111
|
+
const mark = { text: providerInitials(providerId), seed: providerId };
|
|
3112
|
+
if (entry.locked) {
|
|
3113
|
+
const envVars = providerEntry(providerId)?.envVars ?? [];
|
|
3114
|
+
scopes.push({
|
|
3115
|
+
id: entry.id,
|
|
3116
|
+
label: entry.label,
|
|
3117
|
+
mark,
|
|
3118
|
+
group: "Not signed in",
|
|
3119
|
+
disabled: oauthIds.has(providerId)
|
|
3120
|
+
? "Sign in with /login"
|
|
3121
|
+
: envVars.length > 0
|
|
3122
|
+
? `Set ${envVars.join(" or ")} to sign in`
|
|
3123
|
+
: "Add an API key in config to sign in",
|
|
3124
|
+
dot: "muted",
|
|
3125
|
+
});
|
|
3126
|
+
break;
|
|
3127
|
+
}
|
|
3128
|
+
const dot = this.#providerDot(providerId);
|
|
3129
|
+
scopes.push({
|
|
3130
|
+
id: entry.id,
|
|
3131
|
+
label: entry.label,
|
|
3132
|
+
mark,
|
|
3133
|
+
group: "Providers",
|
|
3134
|
+
count: counts ? (counts.get(providerId) ?? 0) : Number(entry.annotation ?? 0),
|
|
3135
|
+
...(dot ? { dot } : {}),
|
|
3136
|
+
});
|
|
3137
|
+
break;
|
|
3138
|
+
}
|
|
3139
|
+
}
|
|
3140
|
+
}
|
|
3141
|
+
return scopes;
|
|
3142
|
+
}
|
|
3143
|
+
|
|
3144
|
+
/** Discovery state as a scope dot: refreshing, ok, cached/empty, unavailable, signed out. */
|
|
3145
|
+
#providerDot(providerId: string): TspPickerScope["dot"] {
|
|
3146
|
+
if (this.#refreshingProviders.has(providerId)) return "pending";
|
|
3147
|
+
switch (this.#registry.getProviderDiscoveryState(providerId)?.status) {
|
|
3148
|
+
case "ok":
|
|
3149
|
+
return "success";
|
|
3150
|
+
case "cached":
|
|
3151
|
+
case "empty":
|
|
3152
|
+
return "warning";
|
|
3153
|
+
case "unavailable":
|
|
3154
|
+
return "error";
|
|
3155
|
+
case "unauthenticated":
|
|
3156
|
+
return "muted";
|
|
3157
|
+
default:
|
|
3158
|
+
return undefined;
|
|
3159
|
+
}
|
|
3160
|
+
}
|
|
3161
|
+
|
|
3162
|
+
/** Kind tabs with their counts in the active scope; the same array while the scope's models are unchanged. */
|
|
3163
|
+
#pickerKindTabs(): TspPickerProps["tabs"] {
|
|
3164
|
+
const candidates = this.#candidateItems;
|
|
3165
|
+
if (this.#kindTabsMemo?.candidates === candidates) return this.#kindTabsMemo.tabs;
|
|
3166
|
+
const counts = new Map<string, number>();
|
|
3167
|
+
for (const item of candidates) {
|
|
3168
|
+
const kind = modelKind(item.model);
|
|
3169
|
+
counts.set(kind, (counts.get(kind) ?? 0) + 1);
|
|
3170
|
+
}
|
|
3171
|
+
const tabs = MODEL_KIND_TABS.map(kind => ({
|
|
3172
|
+
id: kind,
|
|
3173
|
+
label: MODEL_KIND_LABELS[kind],
|
|
3174
|
+
count: kind === "all" ? candidates.length : (counts.get(kind) ?? 0),
|
|
3175
|
+
}));
|
|
3176
|
+
this.#kindTabsMemo = { candidates, tabs };
|
|
3177
|
+
return tabs;
|
|
3178
|
+
}
|
|
3179
|
+
|
|
3180
|
+
/** The action bar for the current view; every button runs the path of the key it shows. */
|
|
3181
|
+
#pickerActions(entry: SidebarEntry, rolesView: boolean, lockedView: boolean): TspPickerAction[] {
|
|
3182
|
+
const strip = this.#strip;
|
|
3183
|
+
const cancel = (label: string): TspPickerAction => ({ ...CLOSE_ACTION, label });
|
|
3184
|
+
if (this.#assignmentPending) return [CLOSE_ACTION];
|
|
3185
|
+
if (strip) {
|
|
3186
|
+
const apply =
|
|
3187
|
+
strip.kind === "roleName"
|
|
3188
|
+
? pickerAction("roleName", "Create role", "enter", { primary: true })
|
|
3189
|
+
: pickerAction(
|
|
3190
|
+
"stripApply",
|
|
3191
|
+
strip.kind === "thinking" ? "Apply" : strip.kind === "scope" ? "Save to scope" : "Assign / clear",
|
|
3192
|
+
"enter",
|
|
3193
|
+
{ primary: true },
|
|
3194
|
+
);
|
|
3195
|
+
return [apply, cancel(strip.kind === "thinking" ? "Keep" : "Cancel")];
|
|
3196
|
+
}
|
|
3197
|
+
const refresh =
|
|
3198
|
+
entry.kind === "provider" && !entry.locked ? pickerAction("refresh", "Refresh provider", "f5") : undefined;
|
|
3199
|
+
if (this.#assigning !== null) {
|
|
3200
|
+
const label =
|
|
3201
|
+
this.#assigning.kind === "fallback"
|
|
3202
|
+
? "Pick fallback"
|
|
3203
|
+
: this.#assigning.kind === "fallbackKey"
|
|
3204
|
+
? "Pick protected model"
|
|
3205
|
+
: "Assign";
|
|
3206
|
+
return compact([pickerAction("assign", label, "enter", { primary: true }), refresh, cancel("Cancel")]);
|
|
3207
|
+
}
|
|
3208
|
+
if (rolesView) {
|
|
3209
|
+
const row = this.#rolesRows[this.#roleIndex];
|
|
3210
|
+
const roleAction = (action: RolesAction, label: string, key: string, primary = false) =>
|
|
3211
|
+
pickerAction(`roles:${action}`, label, key, primary ? { primary: true } : undefined);
|
|
3212
|
+
const actions: (TspPickerAction | undefined)[] = [];
|
|
3213
|
+
switch (row?.kind) {
|
|
3214
|
+
case "role": {
|
|
3215
|
+
const assigned = this.#roles[row.role];
|
|
3216
|
+
actions.push(
|
|
3217
|
+
roleAction("pick", "Pick model", "enter", true),
|
|
3218
|
+
roleAction("fallback", "Add fallback", "f"),
|
|
3219
|
+
assigned && !assigned.autoSelected ? roleAction("clear", "Clear", "x") : undefined,
|
|
3220
|
+
this.#roleThinkingTarget(row.role) ? roleAction("thinking", "Thinking", "t") : undefined,
|
|
3221
|
+
roleAction("cycle", this.#cycleOrder().includes(row.role) ? "Leave cycle" : "Add to cycle", "c"),
|
|
3222
|
+
roleAction("new", "New role", "n"),
|
|
3223
|
+
);
|
|
3224
|
+
break;
|
|
3225
|
+
}
|
|
3226
|
+
case "fallback":
|
|
3227
|
+
actions.push(
|
|
3228
|
+
roleAction("pick", "Replace", "enter", true),
|
|
3229
|
+
roleAction("fallback", "Add another", "f"),
|
|
3230
|
+
roleAction("clear", "Remove", "x"),
|
|
3231
|
+
this.#resolveFallbackEntry(row.role, row.chainIndex)
|
|
3232
|
+
? roleAction("thinking", "Thinking", "t")
|
|
3233
|
+
: undefined,
|
|
3234
|
+
roleAction("earlier", "Earlier", "["),
|
|
3235
|
+
roleAction("later", "Later", "]"),
|
|
3236
|
+
);
|
|
3237
|
+
break;
|
|
3238
|
+
case "chainKey":
|
|
3239
|
+
actions.push(roleAction("pick", "Add fallback", "enter", true), roleAction("clear", "Clear chain", "x"));
|
|
3240
|
+
break;
|
|
3241
|
+
case "newFallback":
|
|
3242
|
+
actions.push(roleAction("pick", "New fallback chain", "enter", true));
|
|
3243
|
+
break;
|
|
3244
|
+
case "newRole":
|
|
3245
|
+
actions.push(roleAction("pick", "New role", "enter", true));
|
|
3246
|
+
break;
|
|
3247
|
+
}
|
|
3248
|
+
return compact([...actions, CLOSE_ACTION]);
|
|
3249
|
+
}
|
|
3250
|
+
if (lockedView) {
|
|
3251
|
+
return entry.oauth
|
|
3252
|
+
? [pickerAction("login", "Log in", "enter", { primary: true }), CLOSE_ACTION]
|
|
3253
|
+
: [CLOSE_ACTION];
|
|
3254
|
+
}
|
|
3255
|
+
return compact([
|
|
3256
|
+
pickerAction(
|
|
3257
|
+
"assign",
|
|
3258
|
+
"Assign role",
|
|
3259
|
+
"enter",
|
|
3260
|
+
this.#browser.pickerSelected ? { primary: true } : { primary: true, disabled: "No model selected" },
|
|
3261
|
+
),
|
|
3262
|
+
refresh,
|
|
3263
|
+
this.#browser.query.length > 0 ? cancel("Clear search") : CLOSE_ACTION,
|
|
3264
|
+
]);
|
|
3265
|
+
}
|
|
3266
|
+
|
|
3267
|
+
/** The open strip as the picker's chip strip (role assignment, save scope, thinking level, new role name). */
|
|
3268
|
+
#pickerStrip(strip: StripState): NonNullable<TspPickerProps["strip"]> {
|
|
3269
|
+
if (strip.kind === "roleName") {
|
|
3270
|
+
return {
|
|
3271
|
+
label: [span("New role name ", "muted"), span(strip.input.getValue(), "mono"), span("▏", "accent")],
|
|
3272
|
+
items: [],
|
|
3273
|
+
};
|
|
3274
|
+
}
|
|
3275
|
+
let label: TspSpan[];
|
|
3276
|
+
if (strip.kind === "role") {
|
|
3277
|
+
// The second step of "New fallback chain…" reuses the role strip with key chips only.
|
|
3278
|
+
const keyStrip = strip.chips.every(chip => chip.label.startsWith("for "));
|
|
3279
|
+
label = keyStrip
|
|
3280
|
+
? [span("New fallback chain", "muted")]
|
|
3281
|
+
: [span("Assign ", "muted"), span(strip.item.id, "mono"), span(" to", "muted")];
|
|
3282
|
+
} else {
|
|
3283
|
+
const info = this.#settings.getRoleInfo(strip.role ?? "");
|
|
3284
|
+
const roleLabel = (info.tag ?? strip.role ?? "").toLowerCase();
|
|
3285
|
+
label = [
|
|
3286
|
+
span(strip.kind === "thinking" ? "Thinking for " : "Save ", "muted"),
|
|
3287
|
+
span(roleLabel, "strong"),
|
|
3288
|
+
span(" · ", "muted"),
|
|
3289
|
+
span(strip.item.id, "mono"),
|
|
3290
|
+
...(strip.kind === "scope" ? [span(" to", "muted")] : []),
|
|
3291
|
+
];
|
|
3292
|
+
}
|
|
3293
|
+
return {
|
|
3294
|
+
label,
|
|
3295
|
+
items: strip.chips.map((chip, index) => {
|
|
3296
|
+
const assigned = chip.role ? this.#roles[chip.role] : undefined;
|
|
3297
|
+
const level =
|
|
3298
|
+
chip.action === "thinking"
|
|
3299
|
+
? chip.thinkingLevel
|
|
3300
|
+
: chip.action === "unassign" && assigned && !assigned.autoSelected
|
|
3301
|
+
? assigned.thinkingLevel
|
|
3302
|
+
: undefined;
|
|
3303
|
+
const dot = level !== undefined ? thinkingDotToken(level) : undefined;
|
|
3304
|
+
const on =
|
|
3305
|
+
chip.action === "unassign" ||
|
|
3306
|
+
(strip.kind === "thinking" &&
|
|
3307
|
+
chip.thinkingLevel !== undefined &&
|
|
3308
|
+
chip.thinkingLevel === strip.initialThinkingLevel);
|
|
3309
|
+
return { id: String(index), label: chip.label, ...(on ? { on: true } : {}), ...(dot ? { dot } : {}) };
|
|
3310
|
+
}),
|
|
3311
|
+
selected: String(strip.index),
|
|
3312
|
+
};
|
|
3313
|
+
}
|
|
3314
|
+
|
|
3315
|
+
/** Roles-view rows as picker items; rebuilt only when the rows, roles or cycle change. */
|
|
3316
|
+
#rolePickerItems(): readonly TspPickerItem[] {
|
|
3317
|
+
const cycleOrder = this.#cycleOrder();
|
|
3318
|
+
const cycle = cycleOrder.join("\0");
|
|
3319
|
+
const memo = this.#pickerRoleItems;
|
|
3320
|
+
if (memo?.rows === this.#rolesRows && memo.roles === this.#roles && memo.cycle === cycle) return memo.items;
|
|
3321
|
+
const items: TspPickerItem[] = [];
|
|
3322
|
+
this.#rolesRows.forEach((row, index) => {
|
|
3323
|
+
const id = rolesRowKey(row, index);
|
|
3324
|
+
switch (row.kind) {
|
|
3325
|
+
case "separator":
|
|
3326
|
+
return;
|
|
3327
|
+
case "newRole":
|
|
3328
|
+
items.push({ id, label: "New role…", icon: "plus", tone: "muted" });
|
|
3329
|
+
return;
|
|
3330
|
+
case "newFallback":
|
|
3331
|
+
items.push({ id, label: "New fallback chain…", icon: "plus", tone: "muted" });
|
|
3332
|
+
return;
|
|
3333
|
+
case "chainKey":
|
|
3334
|
+
items.push({ id, label: row.role, mono: true, icon: "git-branch", detail: "fallback chain" });
|
|
3335
|
+
return;
|
|
3336
|
+
case "fallback":
|
|
3337
|
+
items.push({ id, label: row.selector, mono: true, depth: 1, detail: `fallback ${row.chainIndex + 1}` });
|
|
3338
|
+
return;
|
|
3339
|
+
case "role":
|
|
3340
|
+
break;
|
|
3341
|
+
}
|
|
3342
|
+
const info = this.#settings.getRoleInfo(row.role);
|
|
3343
|
+
const tag = info.tag ?? info.name ?? row.role;
|
|
3344
|
+
const assignment = this.#roles[row.role];
|
|
3345
|
+
const facts: Record<string, string> = {};
|
|
3346
|
+
if (assignment) {
|
|
3347
|
+
const selector = `${assignment.model.provider}/${assignment.model.id}`;
|
|
3348
|
+
facts.model = assignment.autoSelected ? `auto → ${selector}` : selector;
|
|
3349
|
+
if (assignment.thinkingLevel !== ThinkingLevel.Inherit) {
|
|
3350
|
+
facts.thinking = getConfiguredThinkingLevelMetadata(assignment.thinkingLevel).label;
|
|
3351
|
+
}
|
|
3352
|
+
} else {
|
|
3353
|
+
facts.model = "—";
|
|
3354
|
+
}
|
|
3355
|
+
const cycleIndex = cycleOrder.indexOf(row.role);
|
|
3356
|
+
items.push({
|
|
3357
|
+
id,
|
|
3358
|
+
label: tag,
|
|
3359
|
+
...(info.name && info.name !== tag ? { detail: info.name } : {}),
|
|
3360
|
+
facts,
|
|
3361
|
+
...(assignment && !assignment.autoSelected ? {} : { tone: "muted" as const }),
|
|
3362
|
+
...(cycleIndex >= 0
|
|
3363
|
+
? {
|
|
3364
|
+
badges: [
|
|
3365
|
+
{
|
|
3366
|
+
text: `cycle ${cycleIndex + 1}`,
|
|
3367
|
+
tone: "accent" as const,
|
|
3368
|
+
title: `Stop ${cycleIndex + 1} of the quick-switch cycle`,
|
|
3369
|
+
},
|
|
3370
|
+
],
|
|
3371
|
+
}
|
|
3372
|
+
: {}),
|
|
3373
|
+
});
|
|
3374
|
+
});
|
|
3375
|
+
this.#pickerRoleItems = { rows: this.#rolesRows, roles: this.#roles, cycle, items };
|
|
3376
|
+
return items;
|
|
3377
|
+
}
|
|
3378
|
+
|
|
3379
|
+
/** Roles-view order: the separators become group heads (chat roles, kind roles, model fallback chains). */
|
|
3380
|
+
#rolePickerOrder(): (string | TspPickerGroup)[] {
|
|
3381
|
+
const order: (string | TspPickerGroup)[] = [];
|
|
3382
|
+
let head: { group: string; label: string; count: number } | undefined;
|
|
3383
|
+
const open = (group: string, label: string) => {
|
|
3384
|
+
head = { group, label, count: 0 };
|
|
3385
|
+
order.push(head);
|
|
3386
|
+
};
|
|
3387
|
+
open(this.#roleTab === "kind" ? "kind" : "chat", this.#roleTab === "kind" ? "Kind roles" : "Chat roles");
|
|
3388
|
+
this.#rolesRows.forEach((row, index) => {
|
|
3389
|
+
if (row.kind === "separator") {
|
|
3390
|
+
const previous = this.#rolesRows[index - 1];
|
|
3391
|
+
if (previous?.kind === "newRole") open("chains", "Model fallback chains");
|
|
3392
|
+
else open("kind", "Kind roles");
|
|
3393
|
+
return;
|
|
3394
|
+
}
|
|
3395
|
+
order.push(rolesRowKey(row, index));
|
|
3396
|
+
// Heads count roles and chains, not their fallback entries or the "new" rows.
|
|
3397
|
+
if (head && (row.kind === "role" || row.kind === "chainKey")) head.count++;
|
|
3398
|
+
});
|
|
3399
|
+
return order;
|
|
3400
|
+
}
|
|
3401
|
+
|
|
3402
|
+
/** The selected Roles-view row's preview: its model's facts and its fallback chain. */
|
|
3403
|
+
#rolePreview(row: RolesRow | undefined): readonly NativeChild[] {
|
|
3404
|
+
const memo = this.#pickerRolePreview;
|
|
3405
|
+
if (memo !== undefined && memo.row === row && memo.roles === this.#roles && memo.rows === this.#rolesRows) {
|
|
3406
|
+
return memo.children;
|
|
3407
|
+
}
|
|
3408
|
+
const children: NativeChild[] = [];
|
|
3409
|
+
const chainList = (key: string): NativeChild | undefined => {
|
|
3410
|
+
const chain = this.#fallbackChains()[key] ?? [];
|
|
3411
|
+
if (chain.length === 0) return undefined;
|
|
3412
|
+
return node("section", { head: "Fallback chain" }, [
|
|
3413
|
+
md(chain.map((selector, index) => `${index + 1}. \`${selector}\``).join("\n")),
|
|
3414
|
+
]);
|
|
3415
|
+
};
|
|
3416
|
+
const modelItem = (model: Model): ModelBrowserItem => ({
|
|
3417
|
+
provider: model.provider,
|
|
3418
|
+
id: model.id,
|
|
3419
|
+
model,
|
|
3420
|
+
selector: `${model.provider}/${model.id}`,
|
|
3421
|
+
});
|
|
3422
|
+
switch (row?.kind) {
|
|
3423
|
+
case "role": {
|
|
3424
|
+
const info = this.#settings.getRoleInfo(row.role);
|
|
3425
|
+
const assignment = this.#roles[row.role];
|
|
3426
|
+
if (assignment) {
|
|
3427
|
+
children.push(...this.#browser.modelPreview(modelItem(assignment.model), "full", this.#currentSelector));
|
|
3428
|
+
children.push(
|
|
3429
|
+
node("kv", {
|
|
3430
|
+
items: [
|
|
3431
|
+
{ k: [span("Role", "muted")], v: info.name },
|
|
3432
|
+
{
|
|
3433
|
+
k: [span("Thinking", "muted")],
|
|
3434
|
+
v: getConfiguredThinkingLevelMetadata(assignment.thinkingLevel).label,
|
|
3435
|
+
},
|
|
3436
|
+
{ k: [span("Source", "muted")], v: assignment.autoSelected ? "auto-selected" : "configured" },
|
|
3437
|
+
],
|
|
3438
|
+
}),
|
|
3439
|
+
);
|
|
3440
|
+
} else {
|
|
3441
|
+
children.push(
|
|
3442
|
+
text(info.name, { role: "omp.picker.title" }),
|
|
3443
|
+
text([span("Not assigned; no available model fits this role.", "muted")], { wrap: "word" }),
|
|
3444
|
+
);
|
|
3445
|
+
}
|
|
3446
|
+
const chain = chainList(row.role);
|
|
3447
|
+
if (chain) children.push(chain);
|
|
3448
|
+
break;
|
|
3449
|
+
}
|
|
3450
|
+
case "fallback": {
|
|
3451
|
+
const resolved = this.#resolveFallbackEntry(row.role, row.chainIndex);
|
|
3452
|
+
if (resolved) children.push(...this.#browser.modelPreview(resolved.item, "full", this.#currentSelector));
|
|
3453
|
+
else children.push(text([span(row.selector, "mono")], { role: "omp.picker.title" }));
|
|
3454
|
+
const chain = chainList(row.role);
|
|
3455
|
+
if (chain) children.push(chain);
|
|
3456
|
+
break;
|
|
3457
|
+
}
|
|
3458
|
+
case "chainKey": {
|
|
3459
|
+
children.push(text([span(row.role, "mono")], { role: "omp.picker.title" }));
|
|
3460
|
+
const chain = chainList(row.role);
|
|
3461
|
+
if (chain) children.push(chain);
|
|
3462
|
+
break;
|
|
3463
|
+
}
|
|
3464
|
+
case "newRole":
|
|
3465
|
+
children.push(
|
|
3466
|
+
text("New role", { role: "omp.picker.title" }),
|
|
3467
|
+
text([span("Name a custom role, then pick the model it runs on.", "muted")], { wrap: "word" }),
|
|
3468
|
+
);
|
|
3469
|
+
break;
|
|
3470
|
+
case "newFallback":
|
|
3471
|
+
children.push(
|
|
3472
|
+
text("New fallback chain", { role: "omp.picker.title" }),
|
|
3473
|
+
text([span("Pick the model (or provider) a new retry fallback chain protects.", "muted")], {
|
|
3474
|
+
wrap: "word",
|
|
3475
|
+
}),
|
|
3476
|
+
);
|
|
3477
|
+
break;
|
|
3478
|
+
}
|
|
3479
|
+
this.#pickerRolePreview = { row, roles: this.#roles, rows: this.#rolesRows, children };
|
|
3480
|
+
return children;
|
|
3481
|
+
}
|
|
3482
|
+
|
|
3483
|
+
#lockedMessage(entry: SidebarEntry): string {
|
|
3484
|
+
const envVars = entry.providerId ? (providerEntry(entry.providerId)?.envVars ?? []) : [];
|
|
3485
|
+
const how =
|
|
3486
|
+
envVars.length > 0
|
|
3487
|
+
? `Set ${envVars.join(" or ")} in your environment, or add a key in config.`
|
|
3488
|
+
: "Add an API key for this provider in config.";
|
|
3489
|
+
return `${entry.label} has no credentials configured. ${how}`;
|
|
3490
|
+
}
|
|
3491
|
+
|
|
3492
|
+
/** A signed-out provider's preview: how to sign in and what its catalog holds. */
|
|
3493
|
+
#lockedPreview(entry: SidebarEntry): readonly NativeChild[] {
|
|
3494
|
+
const children: NativeChild[] = [
|
|
3495
|
+
text(entry.label, { role: "omp.picker.title" }),
|
|
3496
|
+
text([span(this.#lockedMessage(entry), "muted")], { wrap: "word" }),
|
|
3497
|
+
];
|
|
3498
|
+
const catalogCount = entry.catalogCount ?? 0;
|
|
3499
|
+
if (catalogCount > 0 && this.#scopedModels.length === 0) {
|
|
3500
|
+
const ids: string[] = [];
|
|
3501
|
+
for (const model of this.#registry.getAll("all")) {
|
|
3502
|
+
if (model.provider === entry.providerId) ids.push(`- \`${model.id}\``);
|
|
3503
|
+
}
|
|
3504
|
+
children.push(node("section", { head: `${catalogCount} models in catalog` }, [md(ids.join("\n"))]));
|
|
3505
|
+
}
|
|
3506
|
+
return children;
|
|
3507
|
+
}
|
|
3508
|
+
|
|
3509
|
+
/** Picker pointer events, each on the path of the key it stands for. */
|
|
3510
|
+
#handlePickerEvent(event: PickerEvent): void {
|
|
3511
|
+
if (event.kind === "action" && event.act === CLOSE_ACTION.id) {
|
|
3512
|
+
this.#cancel();
|
|
3513
|
+
return;
|
|
3514
|
+
}
|
|
3515
|
+
// Same gate as the keys: only cancel works while an assignment applies.
|
|
3516
|
+
if (this.#assignmentPending) return;
|
|
3517
|
+
const entry = this.#activeEntry();
|
|
3518
|
+
const rolesView = entry.kind === "roles" && this.#assigning === null;
|
|
3519
|
+
if (event.kind !== "action") {
|
|
3520
|
+
if (this.#strip) return;
|
|
3521
|
+
if (rolesView) {
|
|
3522
|
+
const index = this.#rolesRows.findIndex((row, i) => rolesRowKey(row, i) === event.item);
|
|
3523
|
+
const row = this.#rolesRows[index];
|
|
3524
|
+
if (!row || row.kind === "separator") return;
|
|
3525
|
+
this.#focus = "list";
|
|
3526
|
+
this.#roleIndex = index;
|
|
3527
|
+
if (event.kind === "activate") this.#activateRolesRow(row);
|
|
3528
|
+
return;
|
|
3529
|
+
}
|
|
3530
|
+
if (this.#isBrowserView(entry) && this.#browser.routePickerItem(event.item, event.kind === "activate")) {
|
|
3531
|
+
this.#focus = "list";
|
|
3532
|
+
}
|
|
3533
|
+
return;
|
|
3534
|
+
}
|
|
3535
|
+
const { act, value } = event;
|
|
3536
|
+
if (act.startsWith("roles:")) {
|
|
3537
|
+
const action = act.slice("roles:".length) as RolesAction;
|
|
3538
|
+
if (!rolesView || this.#strip) return;
|
|
3539
|
+
this.#focus = "list";
|
|
3540
|
+
this.#runRolesAction(action);
|
|
3541
|
+
return;
|
|
3542
|
+
}
|
|
3543
|
+
switch (act) {
|
|
3544
|
+
case "scope":
|
|
3545
|
+
this.#clickSidebarEntry(this.#entries.find(candidate => candidate.id === value));
|
|
3546
|
+
return;
|
|
3547
|
+
case "tab":
|
|
3548
|
+
if (rolesView) {
|
|
3549
|
+
const tab = ROLE_TABS.find(candidate => candidate === value);
|
|
3550
|
+
if (!tab) return;
|
|
3551
|
+
this.#roleTab = tab;
|
|
3552
|
+
this.#roleIndex = 0;
|
|
3553
|
+
this.#roleScrollStart = 0;
|
|
3554
|
+
this.#buildRolesRows();
|
|
3555
|
+
} else {
|
|
3556
|
+
const kind = MODEL_KIND_TABS.find(candidate => candidate === value);
|
|
3557
|
+
if (!kind) return;
|
|
3558
|
+
this.#modelKindTab = kind;
|
|
3559
|
+
this.#applyModelKind();
|
|
3560
|
+
}
|
|
3561
|
+
return;
|
|
3562
|
+
case "strip": {
|
|
3563
|
+
const strip = this.#strip;
|
|
3564
|
+
const index = Number(value);
|
|
3565
|
+
if (!strip || strip.kind === "roleName" || !Number.isInteger(index) || !strip.chips[index]) return;
|
|
3566
|
+
strip.index = index;
|
|
3567
|
+
this.#activateStripChip();
|
|
3568
|
+
return;
|
|
3569
|
+
}
|
|
3570
|
+
case "stripApply":
|
|
3571
|
+
this.#activateStripChip();
|
|
3572
|
+
return;
|
|
3573
|
+
case "roleName":
|
|
3574
|
+
this.#submitRoleName();
|
|
3575
|
+
return;
|
|
3576
|
+
case "cancel":
|
|
3577
|
+
if (this.#strip) this.#closeStrip();
|
|
3578
|
+
return;
|
|
3579
|
+
case "clear":
|
|
3580
|
+
if (this.#browser.query.length > 0) this.#browser.handleCancel();
|
|
3581
|
+
return;
|
|
3582
|
+
case "assign": {
|
|
3583
|
+
if (this.#strip || !this.#isBrowserView(entry)) return;
|
|
3584
|
+
const selected = this.#browser.pickerSelected;
|
|
3585
|
+
if (!selected) return;
|
|
3586
|
+
this.#focus = "list";
|
|
3587
|
+
this.#browser.routePickerItem(selected, true);
|
|
3588
|
+
return;
|
|
3589
|
+
}
|
|
3590
|
+
case "refresh":
|
|
3591
|
+
if (entry.kind === "provider" && !entry.locked) {
|
|
3592
|
+
this.#scheduleProviderRefresh(entry.providerId ?? "", { force: true });
|
|
3593
|
+
}
|
|
3594
|
+
return;
|
|
3595
|
+
case "login":
|
|
3596
|
+
if (entry.kind === "provider" && entry.locked && this.#assigning === null) this.#requestLogin(entry);
|
|
3597
|
+
return;
|
|
3598
|
+
}
|
|
3599
|
+
}
|
|
3600
|
+
|
|
3601
|
+
/** Footer chip labels, built from each chip's action instead of its ANSI rendering. */
|
|
3602
|
+
#chipSpans(chip: StripChip): TspSpan[] {
|
|
3603
|
+
switch (chip.action) {
|
|
3604
|
+
case "assign":
|
|
3605
|
+
case "unassign": {
|
|
3606
|
+
const color = this.#settings.getRoleInfo(chip.role ?? "").color ?? "muted";
|
|
3607
|
+
return chip.action === "unassign"
|
|
3608
|
+
? [span(`${theme.status.enabled} ${chip.label}`, color), span(` ${theme.status.success}`, "dim")]
|
|
3609
|
+
: [span(chip.label, color)];
|
|
3610
|
+
}
|
|
3611
|
+
case "scope":
|
|
3612
|
+
return [span(chip.label, chip.scope === "project" ? "accent" : "muted")];
|
|
3613
|
+
case "thinking": {
|
|
3614
|
+
const glyph = chip.thinkingLevel === undefined ? "" : thinkingLevelGlyph(chip.thinkingLevel, theme);
|
|
3615
|
+
return glyph ? [span(glyph, "accent"), span(` ${chip.label}`)] : [span(chip.label)];
|
|
3616
|
+
}
|
|
3617
|
+
default:
|
|
3618
|
+
return [span(plainText(chip.styled), "muted")];
|
|
3619
|
+
}
|
|
3620
|
+
}
|
|
3621
|
+
|
|
3622
|
+
#describeStrip(): NativeNode | undefined {
|
|
3623
|
+
const strip = this.#strip;
|
|
3624
|
+
if (!strip) return undefined;
|
|
3625
|
+
if (strip.kind === "roleName") {
|
|
3626
|
+
return node(
|
|
3627
|
+
"row",
|
|
3628
|
+
{ gap: "sm", align: "center" },
|
|
3629
|
+
[
|
|
3630
|
+
text([span("New role name:", "accent")]),
|
|
3631
|
+
col([strip.input], { grow: 1 }),
|
|
3632
|
+
text([span("(letters, digits, - and _)", "dim")]),
|
|
3633
|
+
],
|
|
3634
|
+
"roleName",
|
|
3635
|
+
);
|
|
3636
|
+
}
|
|
3637
|
+
let prefix: TspSpan[];
|
|
3638
|
+
if (strip.kind === "role") {
|
|
3639
|
+
prefix = [span(strip.item.id, "accent"), span(" →", "dim")];
|
|
3640
|
+
} else {
|
|
3641
|
+
const info = this.#settings.getRoleInfo(strip.role ?? "");
|
|
3642
|
+
prefix = [
|
|
3643
|
+
span((info.tag ?? strip.role ?? "").toLowerCase(), info.color ?? "muted"),
|
|
3644
|
+
span(` · ${strip.item.id} →`, "dim"),
|
|
3645
|
+
];
|
|
3646
|
+
}
|
|
3647
|
+
return node(
|
|
3648
|
+
"row",
|
|
3649
|
+
{ gap: "sm", align: "center" },
|
|
3650
|
+
[
|
|
3651
|
+
text(prefix),
|
|
3652
|
+
node(
|
|
3653
|
+
"tabs",
|
|
3654
|
+
{
|
|
3655
|
+
items: strip.chips.map((chip, index) => ({ id: String(index), label: this.#chipSpans(chip) })),
|
|
3656
|
+
active: String(strip.index),
|
|
3657
|
+
actions: { click: "activate" },
|
|
3658
|
+
},
|
|
3659
|
+
undefined,
|
|
3660
|
+
"strip",
|
|
3661
|
+
),
|
|
3662
|
+
],
|
|
3663
|
+
"chips",
|
|
3664
|
+
);
|
|
3665
|
+
}
|
|
3666
|
+
|
|
3667
|
+
#footerHints(): (NativeHint | undefined)[] {
|
|
3668
|
+
const keys = (label: string, ...ids: KeyName[]): NativeHint => ({ keys: ids, label });
|
|
3669
|
+
const cancel = (label: string) => actionHint("tui.select.cancel", label);
|
|
3670
|
+
const upDown = (label: string) => actionHint(["tui.select.up", "tui.select.down"], label);
|
|
3671
|
+
const search = keys("type to search");
|
|
3672
|
+
const kind = keys("kind", "alt+left", "alt+right");
|
|
3673
|
+
const reorder = keys("reorder", "[", "]");
|
|
3674
|
+
const strip = this.#strip;
|
|
3675
|
+
if (strip) {
|
|
3676
|
+
switch (strip.kind) {
|
|
3677
|
+
case "roleName":
|
|
3678
|
+
return [keys("create + pick model", "enter"), cancel("cancel")];
|
|
3679
|
+
case "role":
|
|
3680
|
+
return [keys("choose", "left", "right"), keys("assign/clear", "enter"), cancel("cancel")];
|
|
3681
|
+
case "scope":
|
|
3682
|
+
return [keys("save scope", "left", "right"), keys("choose", "enter"), cancel("cancel")];
|
|
3683
|
+
case "thinking":
|
|
3684
|
+
return [keys("thinking level", "left", "right"), keys("apply", "enter"), cancel("keep")];
|
|
3685
|
+
}
|
|
3686
|
+
}
|
|
3687
|
+
if (this.#assigning !== null) {
|
|
3688
|
+
if (this.#focus === "scope") {
|
|
3689
|
+
return [keys("models", "enter", "right"), upDown("providers"), search, kind, cancel("cancel")];
|
|
3690
|
+
}
|
|
3691
|
+
const pick =
|
|
3692
|
+
this.#assigning.kind === "fallback"
|
|
3693
|
+
? "pick fallback"
|
|
3694
|
+
: this.#assigning.kind === "fallbackKey"
|
|
3695
|
+
? "pick the protected model"
|
|
3696
|
+
: "assign";
|
|
3697
|
+
return [keys(pick, "enter"), upDown("models"), keys("providers", "left"), search, kind, cancel("cancel")];
|
|
3698
|
+
}
|
|
3699
|
+
const entry = this.#activeEntry();
|
|
3700
|
+
if (entry.kind === "roles") {
|
|
3701
|
+
if (this.#focus !== "list") {
|
|
3702
|
+
return [
|
|
3703
|
+
upDown("providers"),
|
|
3704
|
+
keys("roles", "enter", "right"),
|
|
3705
|
+
keys("tabs", "alt+left", "alt+right"),
|
|
3706
|
+
cancel("close"),
|
|
3707
|
+
];
|
|
3708
|
+
}
|
|
3709
|
+
const row = this.#rolesRows[this.#roleIndex];
|
|
3710
|
+
if (row?.kind === "fallback") {
|
|
3711
|
+
// Advertise `t` only where a strip would open, as the ANSI footer does.
|
|
3712
|
+
const editable = this.#resolveFallbackEntry(row.role, row.chainIndex) !== undefined;
|
|
3713
|
+
return [
|
|
3714
|
+
upDown("rows"),
|
|
3715
|
+
keys("replace", "enter"),
|
|
3716
|
+
keys("add another", "f"),
|
|
3717
|
+
keys("remove", "x"),
|
|
3718
|
+
editable ? keys("thinking", "t") : undefined,
|
|
3719
|
+
reorder,
|
|
3720
|
+
keys("providers", "left"),
|
|
3721
|
+
];
|
|
3722
|
+
}
|
|
3723
|
+
if (row?.kind === "chainKey") {
|
|
3724
|
+
return [
|
|
3725
|
+
upDown("rows"),
|
|
3726
|
+
keys("add fallback", "enter", "f"),
|
|
3727
|
+
keys("clear chain", "x"),
|
|
3728
|
+
keys("providers", "left"),
|
|
3729
|
+
];
|
|
3730
|
+
}
|
|
3731
|
+
if (row?.kind === "newFallback") {
|
|
3732
|
+
return [upDown("rows"), keys("new model/provider fallback chain", "enter"), keys("providers", "left")];
|
|
3733
|
+
}
|
|
3734
|
+
const editable = row?.kind === "role" && this.#roleThinkingTarget(row.role) !== undefined;
|
|
3735
|
+
return [
|
|
3736
|
+
upDown("rows"),
|
|
3737
|
+
keys("pick", "enter"),
|
|
3738
|
+
keys("fallback", "f"),
|
|
3739
|
+
keys("clear", "x"),
|
|
3740
|
+
editable ? keys("thinking", "t") : undefined,
|
|
3741
|
+
keys("cycle", "c"),
|
|
3742
|
+
reorder,
|
|
3743
|
+
keys("new", "n"),
|
|
3744
|
+
];
|
|
3745
|
+
}
|
|
3746
|
+
if (entry.kind === "provider" && entry.locked) {
|
|
3747
|
+
return entry.oauth
|
|
3748
|
+
? [keys("log in", "enter"), upDown("providers"), cancel("close")]
|
|
3749
|
+
: [upDown("providers"), cancel("close")];
|
|
3750
|
+
}
|
|
3751
|
+
const refresh = entry.kind === "provider" ? keys("refresh", "f5") : undefined;
|
|
3752
|
+
if (this.#focus === "scope") {
|
|
3753
|
+
return [keys("models", "enter", "right"), upDown("providers"), search, kind, refresh, cancel("close")];
|
|
3754
|
+
}
|
|
3755
|
+
return [
|
|
3756
|
+
keys("assign roles", "enter"),
|
|
3757
|
+
upDown("models"),
|
|
3758
|
+
keys("providers", "left"),
|
|
3759
|
+
search,
|
|
3760
|
+
kind,
|
|
3761
|
+
refresh,
|
|
3762
|
+
cancel("close"),
|
|
3763
|
+
];
|
|
3764
|
+
}
|
|
2441
3765
|
}
|