@pellux/goodvibes-tui 2.0.21 → 2.1.1
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 +65 -1
- package/README.md +30 -29
- package/docs/README.md +8 -5
- package/docs/foundation-artifacts/operator-contract.json +1 -1
- package/package.json +28 -19
- package/src/cli/management.ts +5 -2
- package/src/cli/tui-startup.ts +34 -39
- package/src/core/agent-view-render.ts +157 -0
- package/src/core/approval-alert.ts +49 -11
- package/src/core/bookmark-navigation.ts +7 -1
- package/src/core/budget-breach-notifier.ts +27 -8
- package/src/core/compaction-receipt.ts +1 -1
- package/src/core/context-usage.ts +1 -1
- package/src/core/conversation-compaction-render.ts +3 -3
- package/src/core/conversation-display-only.ts +86 -0
- package/src/core/conversation-event-navigation.ts +27 -0
- package/src/core/conversation-line-cache.ts +216 -447
- package/src/core/conversation-render-context.ts +20 -27
- package/src/core/conversation-rendering.ts +157 -575
- package/src/core/conversation-splash-state.ts +6 -1
- package/src/core/conversation-types.ts +35 -0
- package/src/core/conversation-usage.ts +12 -0
- package/src/core/conversation.ts +130 -131
- package/src/core/long-task-notifier.ts +100 -31
- package/src/core/notices.ts +75 -0
- package/src/core/pairing-tailscale-gateway.ts +1 -1
- package/src/core/process-output-log.ts +143 -0
- package/src/core/rewind-receipt.ts +1 -1
- package/src/core/session-focus.ts +190 -0
- package/src/core/session-resume-core.ts +23 -81
- package/src/core/stream-event-wiring.ts +31 -5
- package/src/core/system-message-noise.ts +2 -2
- package/src/core/system-message-router.ts +16 -18
- package/src/core/turn-event-wiring.ts +272 -33
- package/src/core/work-tree-focus.ts +290 -0
- package/src/core/work-tree-fold-store.ts +143 -0
- package/src/core/work-tree-model.ts +799 -0
- package/src/core/work-tree-render.ts +384 -0
- package/src/core/work-tree-sources.ts +184 -0
- package/src/core/work-tree-wiring.ts +176 -0
- package/src/core/workstream-notification.ts +36 -6
- package/src/core/wrfc-notice-titles.ts +37 -0
- package/src/input/agents-modal-text.ts +119 -0
- package/src/input/agents-modal.ts +699 -0
- package/src/input/bookmark-modal.ts +33 -9
- package/src/input/changes-git.ts +251 -0
- package/src/input/changes-modal.ts +629 -0
- package/src/input/changes-model.ts +125 -0
- package/src/input/command-palette-types.ts +41 -0
- package/src/input/command-palette.ts +272 -0
- package/src/input/command-reference.ts +2 -2
- package/src/input/command-registry.ts +77 -35
- package/src/input/commands/acp-runtime.ts +19 -9
- package/src/input/commands/channel-runtime.ts +2 -2
- package/src/input/commands/ci-runtime.ts +1 -1
- package/src/input/commands/codebase-runtime.ts +1 -1
- package/src/input/commands/config.ts +7 -1
- package/src/input/commands/context-window.ts +25 -19
- package/src/input/commands/control-room-runtime.ts +18 -18
- package/src/input/commands/conversation-runtime.ts +0 -1
- package/src/input/commands/cost-runtime.ts +29 -20
- package/src/input/commands/diff-runtime.ts +35 -181
- package/src/input/commands/eval.ts +1 -1
- package/src/input/commands/experience-runtime.ts +3 -7
- package/src/input/commands/fresh-conversation.ts +86 -0
- package/src/input/commands/git-runtime.ts +5 -22
- package/src/input/commands/graph-runtime.ts +1 -1
- package/src/input/commands/guidance-runtime.ts +1 -1
- package/src/input/commands/health-runtime.ts +8 -9
- package/src/input/commands/hooks-runtime.ts +2 -2
- package/src/input/commands/hosted-runtime.ts +3 -3
- package/src/input/commands/incident-runtime.ts +3 -3
- package/src/input/commands/intelligence-runtime.ts +6 -9
- package/src/input/commands/knowledge.ts +2 -2
- package/src/input/commands/legacy-panel-command.ts +57 -0
- package/src/input/commands/local-auth-runtime.ts +8 -9
- package/src/input/commands/local-provider-runtime.ts +7 -4
- package/src/input/commands/local-runtime.ts +12 -15
- package/src/input/commands/local-setup-review.ts +1 -1
- package/src/input/commands/local-setup.ts +13 -19
- package/src/input/commands/memory.ts +1 -1
- package/src/input/commands/notify-runtime.ts +9 -0
- package/src/input/commands/operator-runtime.ts +10 -12
- package/src/input/commands/permissions-provenance.ts +3 -3
- package/src/input/commands/planning-runtime.ts +7 -7
- package/src/input/commands/platform-sandbox-runtime.ts +2 -2
- package/src/input/commands/platform-services-runtime.ts +1 -1
- package/src/input/commands/policy-dispatch.ts +2 -2
- package/src/input/commands/policy.ts +3 -3
- package/src/input/commands/provider.ts +1 -1
- package/src/input/commands/remote-runtime.ts +1 -1
- package/src/input/commands/review-runtime.ts +39 -94
- package/src/input/commands/rewind-runtime.ts +81 -108
- package/src/input/commands/runtime-services.ts +15 -17
- package/src/input/commands/services-runtime.ts +2 -2
- package/src/input/commands/session-content.ts +21 -5
- package/src/input/commands/session-workflow.ts +13 -38
- package/src/input/commands/settings-sync-runtime.ts +2 -2
- package/src/input/commands/shell-core.ts +19 -4
- package/src/input/commands/skills-runtime.ts +2 -2
- package/src/input/commands/status-runtime.ts +13 -0
- package/src/input/commands/subscription-runtime.ts +1 -1
- package/src/input/commands/tasks-runtime.ts +2 -8
- package/src/input/commands/work-plan-runtime.ts +4 -4
- package/src/input/commands/workstream-attempts.ts +24 -47
- package/src/input/commands/workstream-runtime.ts +2 -2
- package/src/input/commands/worktree-runtime.ts +3 -3
- package/src/input/commands.ts +3 -1
- package/src/input/composer-editor.ts +44 -0
- package/src/input/concealed-input.ts +1 -1
- package/src/input/config-modal-types.ts +20 -8
- package/src/input/config-modal.ts +106 -105
- package/src/input/confirm-dialog-types.ts +28 -0
- package/src/input/confirm-dialog.ts +70 -0
- package/src/input/delete-key-policy.ts +3 -3
- package/src/input/feed-context-factory.ts +15 -16
- package/src/input/handler-command-route.ts +7 -42
- package/src/input/handler-content-actions.ts +16 -15
- package/src/input/handler-feed-routes.ts +26 -211
- package/src/input/handler-feed.ts +68 -95
- package/src/input/handler-interactions.ts +5 -3
- package/src/input/handler-modal-routes.ts +215 -216
- package/src/input/handler-modal-stack.ts +81 -11
- package/src/input/handler-modal-token-routes.ts +4 -0
- package/src/input/handler-onboarding.ts +0 -1
- package/src/input/handler-picker-routes.ts +65 -107
- package/src/input/handler-session-view-route.ts +149 -0
- package/src/input/handler-shortcuts.ts +16 -154
- package/src/input/handler-types.ts +4 -2
- package/src/input/handler-ui-state.ts +45 -15
- package/src/input/handler-work-tree-route.ts +123 -0
- package/src/input/handler.ts +25 -70
- package/src/input/keybindings.ts +16 -71
- package/src/input/masked-entry-modal.ts +117 -0
- package/src/input/mcp-workspace.ts +48 -17
- package/src/input/model-picker-filter.ts +8 -0
- package/src/input/model-picker-open.ts +99 -0
- package/src/input/model-picker-types.ts +1 -1
- package/src/input/model-picker.ts +92 -9
- package/src/input/notifications-modal.ts +123 -0
- package/src/input/overlay-filter.ts +26 -0
- package/src/input/profile-picker-modal.ts +37 -8
- package/src/input/search.ts +3 -3
- package/src/input/selection-modal-queue.ts +2 -0
- package/src/input/selection-modal.ts +28 -16
- package/src/input/session-picker-modal.ts +33 -8
- package/src/input/settings-modal-activation.ts +7 -2
- package/src/input/settings-modal-data.ts +11 -6
- package/src/input/settings-modal-types.ts +1 -1
- package/src/input/settings-modal.ts +20 -20
- package/src/input/submission-intent.ts +1 -1
- package/src/input/submission-router.ts +4 -9
- package/src/input/surface-modal-host.ts +110 -0
- package/src/input/theme-settings-actions.ts +82 -0
- package/src/input/usage-modal.ts +183 -0
- package/src/input/views.ts +97 -0
- package/src/main.ts +175 -165
- package/src/permissions/hunk-selection.ts +1 -1
- package/src/permissions/prompt-card.ts +511 -0
- package/src/permissions/prompt.ts +59 -622
- package/src/renderer/agents-modal.ts +310 -0
- package/src/renderer/autocomplete-overlay.ts +42 -220
- package/src/renderer/back-to-bottom.ts +80 -0
- package/src/renderer/block-actions-overlay.ts +30 -48
- package/src/renderer/bookmark-modal.ts +48 -79
- package/src/renderer/changes-modal.ts +325 -0
- package/src/renderer/code-block.ts +184 -106
- package/src/renderer/command-palette.ts +93 -0
- package/src/renderer/composer-fragments.ts +4 -4
- package/src/renderer/composer.ts +146 -0
- package/src/renderer/compositor.ts +26 -160
- package/src/renderer/config-modal.ts +185 -81
- package/src/renderer/confirm-dialog.ts +50 -0
- package/src/renderer/context-inspector.ts +116 -154
- package/src/renderer/context-status-hint.ts +4 -4
- package/src/renderer/conversation-layout.ts +20 -0
- package/src/renderer/conversation-overlays.ts +94 -84
- package/src/renderer/conversation-surface.ts +19 -38
- package/src/renderer/diff-tint.ts +98 -0
- package/src/renderer/file-picker-overlay.ts +63 -90
- package/src/renderer/footer-targets.ts +38 -0
- package/src/renderer/git-status.ts +3 -4
- package/src/renderer/header-line.ts +140 -117
- package/src/renderer/help-overlay.ts +133 -219
- package/src/renderer/history-search-overlay.ts +15 -12
- package/src/renderer/lane-graph/bead.ts +800 -0
- package/src/renderer/lane-graph/glyphs.ts +134 -0
- package/src/renderer/lane-graph/layout.ts +248 -0
- package/src/renderer/lane-graph/paint.ts +537 -0
- package/src/renderer/lane-graph/semantic-memo.ts +77 -0
- package/src/renderer/layout-engine.ts +2 -57
- package/src/renderer/layout.ts +7 -13
- package/src/renderer/markdown-inline.ts +2 -1
- package/src/renderer/markdown-table.ts +17 -12
- package/src/renderer/markdown.ts +92 -34
- package/src/renderer/masked-entry-modal.ts +78 -0
- package/src/renderer/mcp-workspace.ts +166 -203
- package/src/renderer/model-workspace.ts +381 -436
- package/src/renderer/notifications-modal.ts +116 -0
- package/src/renderer/onboarding/onboarding-wizard.ts +178 -588
- package/src/renderer/process-view.ts +192 -0
- package/src/renderer/profile-picker-modal.ts +58 -110
- package/src/renderer/search-overlay.ts +6 -3
- package/src/renderer/selection-modal-overlay.ts +87 -327
- package/src/renderer/semantic-diff.ts +2 -2
- package/src/renderer/session-chips.ts +102 -0
- package/src/renderer/session-picker-modal.ts +177 -297
- package/src/renderer/settings-modal-connections.ts +14 -23
- package/src/renderer/settings-modal-context.ts +367 -0
- package/src/renderer/settings-modal-helpers.ts +6 -11
- package/src/renderer/settings-modal.ts +322 -726
- package/src/renderer/shell-surface.ts +277 -175
- package/src/renderer/shortcuts-overlay.ts +217 -0
- package/src/renderer/status-line.ts +475 -0
- package/src/renderer/surface-compose.ts +174 -0
- package/src/renderer/surface-kit-extra.ts +303 -0
- package/src/renderer/surface-kit-list.ts +366 -0
- package/src/renderer/surface-kit-parts.ts +360 -0
- package/src/renderer/surface-kit.ts +568 -0
- package/src/renderer/syntax-highlighter.ts +280 -197
- package/src/renderer/syntax-theme.ts +51 -0
- package/src/renderer/system-message.ts +0 -1
- package/src/renderer/term-caps.ts +42 -1
- package/src/renderer/terminal-bg-probe.ts +116 -19
- package/src/renderer/terminal-palette-probe.ts +322 -0
- package/src/renderer/terminal-palette-reader.ts +183 -0
- package/src/renderer/terminal-palette.ts +58 -0
- package/src/renderer/theme-mode-config.ts +16 -1
- package/src/renderer/theme.ts +234 -207
- package/src/renderer/thinking.ts +0 -1
- package/src/renderer/throbber.ts +188 -0
- package/src/renderer/toast-center.ts +119 -0
- package/src/renderer/ui-factory.ts +39 -602
- package/src/renderer/ui-primitives.ts +8 -25
- package/src/renderer/usage-modal.ts +353 -0
- package/src/renderer/user-message.ts +58 -0
- package/src/renderer/voice-capture-chip.ts +53 -0
- package/src/runtime/bootstrap-command-context.ts +0 -5
- package/src/runtime/bootstrap-command-parts.ts +38 -89
- package/src/runtime/bootstrap-core.ts +13 -15
- package/src/runtime/bootstrap-hook-bridge.ts +12 -21
- package/src/runtime/bootstrap-notifier-sync.ts +17 -1
- package/src/runtime/bootstrap-shell.ts +16 -50
- package/src/runtime/bootstrap.ts +14 -14
- package/src/runtime/client/fleet-union.ts +5 -5
- package/src/runtime/client/hosted-exit.ts +2 -2
- package/src/runtime/client/hosted-sessions.ts +1 -1
- package/src/runtime/client/spine-adoption.ts +1 -1
- package/src/runtime/context-accounting-source.ts +5 -4
- package/src/runtime/index.ts +1 -0
- package/src/runtime/notification-dispatch.ts +102 -29
- package/src/runtime/recovery-offer-wiring.ts +4 -1
- package/src/runtime/recovery-prompt.ts +31 -15
- package/src/runtime/runtime-services-types.ts +10 -6
- package/src/runtime/services.ts +17 -12
- package/src/runtime/session-continuity-hints.ts +2 -5
- package/src/runtime/session-inbound-inputs.ts +1 -1
- package/src/runtime/session-resume-liveness-confirm.ts +14 -2
- package/src/runtime/startup-typeahead-gate.ts +103 -0
- package/src/runtime/store/domains/domain-read-matrix.ts +0 -1
- package/src/runtime/store/domains/index.ts +0 -8
- package/src/runtime/store/selectors/index.ts +0 -28
- package/src/runtime/store/state.ts +0 -8
- package/src/runtime/ui-services.ts +4 -6
- package/src/runtime/usage-tracker.ts +348 -0
- package/src/runtime/wrfc-persistence.ts +4 -4
- package/src/shell/blocking-input.ts +92 -2
- package/src/shell/recovery-input-helpers.ts +1 -1
- package/src/shell/session-views.ts +539 -0
- package/src/shell/status-report.ts +86 -0
- package/src/shell/transcript-scroll.ts +68 -0
- package/src/shell/ui-openers.ts +84 -172
- package/src/shell/view-openers.ts +205 -0
- package/src/tools/wrfc-agent-guard.ts +22 -2
- package/src/utils/format-duration.ts +4 -8
- package/src/utils/splash-lines.ts +9 -1
- package/src/utils/terminal-width.ts +56 -0
- package/src/verification/live-verifier.ts +0 -10
- package/src/version.ts +1 -1
- package/src/{panels → views}/agent-inspector-shared.ts +2 -2
- package/src/{panels → views}/builtin-modals.ts +22 -21
- package/src/views/builtin-views.ts +135 -0
- package/src/{panels → views}/confirm-state.ts +9 -9
- package/src/{panels → views}/diff-review-model.ts +10 -3
- package/src/{panels → views}/eval-registry.ts +1 -1
- package/src/{panels → views}/fleet-acts.ts +49 -85
- package/src/{panels/fleet-panel-format.ts → views/fleet-format.ts} +40 -40
- package/src/{panels → views}/fleet-gateway.ts +5 -5
- package/src/{panels → views}/fleet-observed-render.ts +16 -16
- package/src/{panels → views}/fleet-read-model.ts +9 -9
- package/src/{panels/fleet-tabs.ts → views/fleet-session-tabs.ts} +13 -13
- package/src/{panels → views}/fleet-spawn.ts +33 -49
- package/src/{panels → views}/fleet-steer.ts +12 -12
- package/src/{panels → views}/fleet-stop.ts +4 -4
- package/src/{panels → views}/fleet-transcript.ts +26 -27
- package/src/{panels/fleet-panel-worktree-detail.ts → views/fleet-worktree-detail.ts} +7 -7
- package/src/{panels → views}/hosted-session-feed.ts +6 -6
- package/src/views/modal-surface-registry.ts +41 -0
- package/src/{panels → views}/modals/devices-modal.ts +2 -2
- package/src/{panels → views}/modals/hooks-modal.ts +13 -13
- package/src/{panels → views}/modals/keybindings-modal.ts +19 -27
- package/src/{panels → views}/modals/knowledge-modal.ts +6 -5
- package/src/{panels → views}/modals/local-auth-modal.ts +22 -21
- package/src/{panels → views}/modals/marketplace-modal.ts +11 -11
- package/src/{panels → views}/modals/memory-modal.ts +8 -8
- package/src/{panels → views}/modals/modal-surface-helpers.ts +15 -5
- package/src/{panels → views}/modals/pairing-modal.ts +9 -9
- package/src/{panels → views}/modals/planning-modal.ts +15 -20
- package/src/{panels → views}/modals/plugins-modal.ts +6 -6
- package/src/{panels → views}/modals/policy-modal.ts +13 -5
- package/src/{panels → views}/modals/provider-health-modal.ts +4 -4
- package/src/{panels → views}/modals/remote-modal.ts +3 -3
- package/src/{panels → views}/modals/sandbox-modal.ts +4 -4
- package/src/{panels → views}/modals/security-modal.ts +22 -16
- package/src/{panels → views}/modals/services-modal.ts +3 -3
- package/src/{panels → views}/modals/settings-sync-modal.ts +4 -4
- package/src/{panels → views}/modals/skills-modal.ts +9 -9
- package/src/{panels → views}/modals/subscription-modal.ts +3 -3
- package/src/{panels → views}/modals/work-plan-modal.ts +10 -13
- package/src/views/notifications-feed.ts +314 -0
- package/src/{panels → views}/polish-core.ts +38 -36
- package/src/{panels → views}/polish-tables.ts +35 -26
- package/src/{panels → views}/polish.ts +101 -95
- package/src/{panels → views}/project-planning-answer-actions.ts +3 -3
- package/src/{panels → views}/provider-health-routes.ts +14 -14
- package/src/{panels → views}/search-focus.ts +8 -8
- package/src/{panels → views}/session-maintenance.ts +11 -11
- package/src/views/skills-discovery.ts +159 -0
- package/src/views/view-deps.ts +80 -0
- package/src/views/view-panel-adapter.ts +53 -0
- package/src/{panels → views}/workstream-graph-render.ts +1 -1
- package/src/core/conversation-fold.ts +0 -101
- package/src/core/conversation-turn-structure.ts +0 -464
- package/src/core/conversation-user-receipts.ts +0 -42
- package/src/input/commands/operator-panel-runtime.ts +0 -186
- package/src/input/panel-integration-actions.ts +0 -33
- package/src/input/panel-mouse-geometry.ts +0 -97
- package/src/input/panel-paste-flood-guard.ts +0 -86
- package/src/panels/base-panel.ts +0 -301
- package/src/panels/builtin/agent.ts +0 -27
- package/src/panels/builtin/development.ts +0 -92
- package/src/panels/builtin/knowledge.ts +0 -12
- package/src/panels/builtin/operations.ts +0 -177
- package/src/panels/builtin/session.ts +0 -81
- package/src/panels/builtin/shared.ts +0 -357
- package/src/panels/builtin-panels.ts +0 -28
- package/src/panels/cost-tracker-panel.ts +0 -746
- package/src/panels/diff-panel.ts +0 -797
- package/src/panels/diff-review-panel.ts +0 -395
- package/src/panels/expandable-list-panel.ts +0 -189
- package/src/panels/fleet-deep-link.ts +0 -31
- package/src/panels/fleet-panel.ts +0 -799
- package/src/panels/git-panel.ts +0 -800
- package/src/panels/hosted-session-panel.ts +0 -221
- package/src/panels/index.ts +0 -21
- package/src/panels/local-auth-panel.ts +0 -478
- package/src/panels/notifications-feed.ts +0 -127
- package/src/panels/notifications-panel.ts +0 -160
- package/src/panels/panel-confirm-overlay.ts +0 -72
- package/src/panels/panel-manager.ts +0 -791
- package/src/panels/plugins-panel.ts +0 -371
- package/src/panels/scrollable-list-panel.ts +0 -498
- package/src/panels/skills-panel.ts +0 -454
- package/src/panels/token-budget-panel.ts +0 -688
- package/src/panels/types.ts +0 -191
- package/src/panels/wrfc-panel-format.ts +0 -133
- package/src/renderer/diff-view.ts +0 -134
- package/src/renderer/fleet-tab-strip.ts +0 -56
- package/src/renderer/footer-tips.ts +0 -53
- package/src/renderer/fullscreen-primitives.ts +0 -146
- package/src/renderer/fullscreen-workspace.ts +0 -194
- package/src/renderer/hint-grammar.ts +0 -52
- package/src/renderer/modal-factory.ts +0 -480
- package/src/renderer/overlay-box.ts +0 -154
- package/src/renderer/overlay-viewport.ts +0 -104
- package/src/renderer/panel-composite.ts +0 -145
- package/src/renderer/panel-workspace-bar.ts +0 -54
- package/src/renderer/process-indicator.ts +0 -163
- package/src/renderer/tab-strip.ts +0 -222
- package/src/renderer/text-layout.ts +0 -31
- package/src/renderer/tool-call.ts +0 -276
- package/src/renderer/tool-result-expanded-lines.ts +0 -48
- package/src/renderer/tool-result-summary.ts +0 -219
- package/src/runtime/diagnostics/panels/index.ts +0 -21
- package/src/runtime/diagnostics/panels/ops.ts +0 -156
- package/src/runtime/perf/panel-contracts.ts +0 -32
- package/src/runtime/store/domains/panels.ts +0 -117
- package/src/verification/verification-ledger.ts +0 -707
- /package/src/{panels → views}/memory-consolidation-gateway.ts +0 -0
- /package/src/{panels → views}/modals/modal-theme.ts +0 -0
- /package/src/{panels → views}/provider-health-domains.ts +0 -0
|
@@ -1,117 +1,51 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* renderModelWorkspace, the model picker drawn with the surface kit.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* ✦ Models esc
|
|
5
|
+
*
|
|
6
|
+
* Main Chat Helper Tool LLM TTS Embeddings
|
|
7
|
+
*
|
|
8
|
+
* ▏opus 6 of 5,582
|
|
9
|
+
*
|
|
10
|
+
* ✦ pinned ┌ element inset ──────────┐
|
|
11
|
+
* ● Claude Opus 5.5 anthropic 1.0M Claude Opus 5.5
|
|
12
|
+
* ✦ recent anthropic:claude-opus-5-5
|
|
13
|
+
* route-llm abacusai 8.2K Context ▰▰▰▰▰▰▰▰▰▰ 1.0M
|
|
14
|
+
* ✦ lm studio Quality ▰▰▰▰▰▰▱▱▱▱ 0.61
|
|
15
|
+
* qwen3-coder lmstudio 128K Cost $15 / $75 per 1M
|
|
16
|
+
*
|
|
17
|
+
* ⏎ use for Main Chat tab next target ctrl+f pin ctrl+r refresh
|
|
18
|
+
*
|
|
19
|
+
* The five model targets are tabs. The list is grouped Pinned, Recent, then by
|
|
20
|
+
* the group-by mode (provider by default, which includes LAN servers). The
|
|
21
|
+
* detail inset shows the selected model with meters for context, benchmark
|
|
22
|
+
* quality and price where that data exists, its capabilities, which targets it
|
|
23
|
+
* serves, and the picker's active filters. Provider, effort, context-cap and
|
|
24
|
+
* embedding-provider steps use the same frame.
|
|
6
25
|
*/
|
|
7
26
|
|
|
8
27
|
import type { ModelDefinition } from '@pellux/goodvibes-sdk/platform/providers';
|
|
9
|
-
import type { ModelPickerModal } from '../input/model-picker.ts';
|
|
10
|
-
import type { ModelPickerTargetInfo } from '../input/model-picker.ts';
|
|
11
|
-
import type { Line } from '@pellux/goodvibes-sdk/platform/types';
|
|
12
|
-
import { createEmptyLine, createStyledCell } from '@pellux/goodvibes-sdk/platform/types';
|
|
13
|
-
import { fitDisplay, getDisplayWidth, wrapText } from '../utils/terminal-width.ts';
|
|
28
|
+
import type { ModelPickerModal, ModelPickerTargetInfo } from '../input/model-picker.ts';
|
|
14
29
|
import { abbreviateCount } from '../utils/format-number.ts';
|
|
15
|
-
import {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
const renderCache = new WeakMap<ModelPickerModal, { key: string; lines: Line[] }>();
|
|
35
|
-
const objectIds = new WeakMap<object, number>();
|
|
36
|
-
let nextObjectId = 1;
|
|
37
|
-
|
|
38
|
-
function clamp(value: number, min: number, max: number): number {
|
|
39
|
-
return Math.max(min, Math.min(max, value));
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function fillRange(line: Line, startX: number, endX: number, bg: string): void {
|
|
43
|
-
for (let x = Math.max(0, startX); x <= Math.min(line.length - 1, endX); x += 1) {
|
|
44
|
-
const cell = line[x] ?? createStyledCell(' ');
|
|
45
|
-
line[x] = createStyledCell(cell.char, {
|
|
46
|
-
fg: cell.fg,
|
|
47
|
-
bg,
|
|
48
|
-
bold: cell.bold,
|
|
49
|
-
dim: cell.dim,
|
|
50
|
-
underline: cell.underline,
|
|
51
|
-
italic: cell.italic,
|
|
52
|
-
strikethrough: cell.strikethrough,
|
|
53
|
-
link: cell.link,
|
|
54
|
-
});
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
function makeLine(width: number, bg = ''): Line {
|
|
59
|
-
const line = createEmptyLine(width);
|
|
60
|
-
if (bg) fillRange(line, 0, width - 1, bg);
|
|
61
|
-
return line;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
function writeText(line: Line, startX: number, maxWidth: number, text: string, style: Partial<Omit<Line[number], 'char'>> = {}): void {
|
|
65
|
-
let x = startX;
|
|
66
|
-
let used = 0;
|
|
67
|
-
for (const ch of text) {
|
|
68
|
-
const width = getDisplayWidth(ch);
|
|
69
|
-
if (width <= 0) continue;
|
|
70
|
-
if (used + width > maxWidth || x >= line.length) break;
|
|
71
|
-
line[x] = createStyledCell(ch, style);
|
|
72
|
-
if (width > 1 && x + 1 < line.length) {
|
|
73
|
-
line[x + 1] = createStyledCell(' ', style);
|
|
74
|
-
}
|
|
75
|
-
x += width;
|
|
76
|
-
used += width;
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
function borderLine(width: number, left: string, fill: string, right: string): Line {
|
|
81
|
-
const line = makeLine(width);
|
|
82
|
-
if (width <= 0) return line;
|
|
83
|
-
line[0] = createStyledCell(left, { fg: PALETTE.border });
|
|
84
|
-
for (let x = 1; x < width - 1; x += 1) {
|
|
85
|
-
line[x] = createStyledCell(fill, { fg: PALETTE.border });
|
|
86
|
-
}
|
|
87
|
-
if (width > 1) line[width - 1] = createStyledCell(right, { fg: PALETTE.border });
|
|
88
|
-
return line;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
function contentLine(width: number, bg: string): Line {
|
|
92
|
-
const line = makeLine(width, bg);
|
|
93
|
-
if (width > 0) line[0] = createStyledCell(GLYPHS.frame.vertical, { fg: PALETTE.border });
|
|
94
|
-
if (width > 1) line[width - 1] = createStyledCell(GLYPHS.frame.vertical, { fg: PALETTE.border });
|
|
95
|
-
return line;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
function drawVertical(line: Line, x: number, bg = ''): void {
|
|
99
|
-
if (x <= 0 || x >= line.length - 1) return;
|
|
100
|
-
line[x] = createStyledCell(GLYPHS.frame.vertical, { fg: PALETTE.border, bg });
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
function padDisplay(text: string, width: number): string {
|
|
104
|
-
return fitDisplay(text, width, '');
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
function stableWindow(total: number, selected: number, visible: number): { start: number; end: number } {
|
|
108
|
-
if (total <= 0 || visible <= 0) return { start: 0, end: 0 };
|
|
109
|
-
const clamped = clamp(selected, 0, total - 1);
|
|
110
|
-
const half = Math.floor(visible / 2);
|
|
111
|
-
const maxStart = Math.max(0, total - visible);
|
|
112
|
-
const start = clamp(clamped - half, 0, maxStart);
|
|
113
|
-
return { start, end: Math.min(total, start + visible) };
|
|
114
|
-
}
|
|
30
|
+
import { activeTokens } from './theme.ts';
|
|
31
|
+
import {
|
|
32
|
+
beginModal,
|
|
33
|
+
finishModal,
|
|
34
|
+
modalInnerWidth,
|
|
35
|
+
scrollCountText,
|
|
36
|
+
standardModalWidth,
|
|
37
|
+
searchRow,
|
|
38
|
+
wrapLines,
|
|
39
|
+
type KitHint,
|
|
40
|
+
type ModalFrame,
|
|
41
|
+
type SurfaceLayer,
|
|
42
|
+
} from './surface-kit.ts';
|
|
43
|
+
import { drawList, type KitListResult, type KitRow } from './surface-kit-list.ts';
|
|
44
|
+
import { meter, inset, insetPut, insetWrap, tabs, type KitInset } from './surface-kit-parts.ts';
|
|
45
|
+
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
// Formatting
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
115
49
|
|
|
116
50
|
function formatContext(value: number | undefined): string {
|
|
117
51
|
if (!value) return '-';
|
|
@@ -122,22 +56,6 @@ function modelKey(model: ModelDefinition): string {
|
|
|
122
56
|
return model.registryKey ?? `${model.provider}:${model.id}`;
|
|
123
57
|
}
|
|
124
58
|
|
|
125
|
-
function targetSummary(info: ModelPickerTargetInfo): string {
|
|
126
|
-
if (!info.enabled) return 'disabled';
|
|
127
|
-
// 'embeddings' has no model concept, configuredNote carries the honest
|
|
128
|
-
// provider id + dimensions + configured-state summary instead of a
|
|
129
|
-
// computed provider:model route (which would print a phantom "model:").
|
|
130
|
-
if (info.configuredNote) return info.configuredNote;
|
|
131
|
-
const route = formatRoute(info.provider, info.model);
|
|
132
|
-
if (info.inherited) return `inherits ${route}`;
|
|
133
|
-
return route;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
function selectedModel(picker: ModelPickerModal): ModelDefinition | null {
|
|
137
|
-
if (picker.mode !== 'model') return null;
|
|
138
|
-
return picker.getSelected();
|
|
139
|
-
}
|
|
140
|
-
|
|
141
59
|
function formatRoute(provider: string, model: string): string {
|
|
142
60
|
if (!provider && !model) return '(not set)';
|
|
143
61
|
if (!provider) return model;
|
|
@@ -145,377 +63,406 @@ function formatRoute(provider: string, model: string): string {
|
|
|
145
63
|
return model.startsWith(`${provider}:`) ? model : `${provider}:${model}`;
|
|
146
64
|
}
|
|
147
65
|
|
|
66
|
+
function targetSummary(info: ModelPickerTargetInfo): string {
|
|
67
|
+
if (!info.enabled) return 'disabled';
|
|
68
|
+
// 'embeddings' has no model concept; configuredNote carries the honest
|
|
69
|
+
// provider id + dimensions + configured-state summary instead.
|
|
70
|
+
if (info.configuredNote) return info.configuredNote;
|
|
71
|
+
const route = formatRoute(info.provider, info.model);
|
|
72
|
+
return info.inherited ? `inherits ${route}` : route;
|
|
73
|
+
}
|
|
74
|
+
|
|
148
75
|
function targetLabelFor(target: string): string {
|
|
149
|
-
if (target === 'helper') return 'Helper
|
|
76
|
+
if (target === 'helper') return 'Helper';
|
|
150
77
|
if (target === 'tool') return 'Tool LLM';
|
|
151
|
-
if (target === 'tts') return 'TTS
|
|
78
|
+
if (target === 'tts') return 'TTS';
|
|
152
79
|
if (target === 'embeddings') return 'Embeddings';
|
|
153
80
|
return 'Main Chat';
|
|
154
81
|
}
|
|
155
82
|
|
|
156
|
-
function
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
lines.push(`Model selection: choose the model to store for ${targetLabel}. Use filters to narrow large catalogs.`);
|
|
170
|
-
if (selected) {
|
|
171
|
-
const caps = selected.capabilities ?? {};
|
|
172
|
-
const capText = [
|
|
173
|
-
caps.reasoning ? 'reasoning' : '',
|
|
174
|
-
caps.multimodal ? 'vision' : '',
|
|
175
|
-
caps.toolCalling ? 'tools' : '',
|
|
176
|
-
caps.codeEditing ? 'code' : '',
|
|
177
|
-
].filter(Boolean).join(', ') || 'standard';
|
|
178
|
-
lines.push(`Selected: ${modelKey(selected)} | ${selected.displayName} | context ${formatContext(selected.contextWindow)} | ${capText}`);
|
|
179
|
-
}
|
|
180
|
-
} else if (picker.mode === 'effort') {
|
|
181
|
-
lines.push(`Reasoning effort applies to the main chat model. Select the default effort for this model.`);
|
|
182
|
-
} else if (picker.mode === 'embeddingProvider') {
|
|
183
|
-
lines.push('Embedding provider selection: choose the provider memory search and the code index use to embed content.');
|
|
184
|
-
const selectedEmbedding = picker.embeddingProviders[picker.selectedIndex];
|
|
185
|
-
if (selectedEmbedding) {
|
|
186
|
-
const status = selectedEmbedding.configured ? 'configured' : 'unconfigured';
|
|
187
|
-
lines.push(`Selected: ${selectedEmbedding.id} | ${selectedEmbedding.dimensions} dimensions | ${status}${selectedEmbedding.detail ? ` | ${selectedEmbedding.detail}` : ''}`);
|
|
188
|
-
}
|
|
189
|
-
} else {
|
|
190
|
-
lines.push(`Context cap overrides the detected local-model context window for this selection.`);
|
|
191
|
-
}
|
|
192
|
-
// A visible cursor glyph on the query itself when search is focused,
|
|
193
|
-
// the only prior affordance was a footer-hint text swap ('/ search' vs.
|
|
194
|
-
// 'Typing filters search...'), buried at the end of a long hint line and
|
|
195
|
-
// easy to miss (confirmed via live tmux repro). This puts the "you are
|
|
196
|
-
// typing here" signal where the eye already is, on the Search: value.
|
|
197
|
-
const searchDisplay = picker.query.length > 0
|
|
198
|
-
? `${picker.query}${picker.searchFocused ? GLYPHS.surface.cursor : ''}`
|
|
199
|
-
: (picker.searchFocused ? GLYPHS.surface.cursor : '(none)');
|
|
200
|
-
const filterText = `Search: ${searchDisplay} | Price: ${picker.categoryFilter} | Capability: ${picker.capabilityFilter} | Group: ${picker.groupBy} | Available only: ${picker.availableOnly ? 'yes' : 'no'}`;
|
|
201
|
-
lines.push(filterText);
|
|
202
|
-
return lines.flatMap((line) => wrapText(line, Math.max(1, width)));
|
|
83
|
+
function isTargetModel(info: ModelPickerTargetInfo | null, model: ModelDefinition): boolean {
|
|
84
|
+
if (!info || !info.enabled) return false;
|
|
85
|
+
return info.model === model.registryKey || (info.model === model.id && (!info.provider || info.provider === model.provider));
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function capabilityNames(model: ModelDefinition): string[] {
|
|
89
|
+
const caps = model.capabilities ?? {};
|
|
90
|
+
return [
|
|
91
|
+
caps.toolCalling ? 'tools' : '',
|
|
92
|
+
caps.multimodal ? 'vision' : '',
|
|
93
|
+
caps.reasoning ? 'reasoning' : '',
|
|
94
|
+
caps.codeEditing ? 'code' : '',
|
|
95
|
+
].filter(Boolean);
|
|
203
96
|
}
|
|
204
97
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
98
|
+
// ---------------------------------------------------------------------------
|
|
99
|
+
// List rows per mode
|
|
100
|
+
// ---------------------------------------------------------------------------
|
|
101
|
+
|
|
102
|
+
/** Model rows; without a detail inset the selected row names its full key instead of the provider. */
|
|
103
|
+
/** The muted row shown while the catalog is still loading behind an open picker. */
|
|
104
|
+
const LOADING_ROW: KitRow = { label: 'loading catalog…', muted: true };
|
|
105
|
+
|
|
106
|
+
function modelRows(picker: ModelPickerModal, compact = false): KitRow[] {
|
|
107
|
+
const t = activeTokens();
|
|
108
|
+
const models = picker.getFilteredModels();
|
|
109
|
+
const target = picker.getSelectedTargetInfo();
|
|
110
|
+
const recent = new Set(picker.recentIds);
|
|
111
|
+
const rows: KitRow[] = [];
|
|
112
|
+
let section = '';
|
|
113
|
+
models.forEach((model, index) => {
|
|
114
|
+
const group = picker.isPinned(model) ? 'Pinned' : recent.has(model.id) ? 'Recent' : picker.getModelGroupKey(model);
|
|
115
|
+
if (group !== section) {
|
|
116
|
+
rows.push({ header: group });
|
|
117
|
+
section = group;
|
|
118
|
+
}
|
|
119
|
+
const current = isTargetModel(target, model);
|
|
120
|
+
const selected = index === picker.selectedIndex;
|
|
121
|
+
rows.push({
|
|
122
|
+
label: model.displayName || model.id,
|
|
123
|
+
desc: compact && selected ? modelKey(model) : model.provider,
|
|
124
|
+
right: formatContext(model.contextWindow),
|
|
125
|
+
mark: current ? '●' : undefined,
|
|
126
|
+
markFg: t.brand,
|
|
127
|
+
current,
|
|
128
|
+
selected,
|
|
129
|
+
});
|
|
217
130
|
});
|
|
131
|
+
if (rows.length === 0 && !picker.catalogLoading) rows.push({ label: picker.query ? `No models match "${picker.query}".` : 'No models are available for this target.', muted: true });
|
|
132
|
+
return rows;
|
|
218
133
|
}
|
|
219
134
|
|
|
220
|
-
function
|
|
135
|
+
function providerRows(picker: ModelPickerModal): KitRow[] {
|
|
136
|
+
const t = activeTokens();
|
|
137
|
+
const counts = new Map<string, number>();
|
|
138
|
+
for (const model of picker.models) counts.set(model.provider, (counts.get(model.provider) ?? 0) + 1);
|
|
221
139
|
const providers = picker.getFilteredProviders();
|
|
222
|
-
const
|
|
223
|
-
|
|
224
|
-
for (const
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
const
|
|
231
|
-
const
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
140
|
+
const rows: KitRow[] = [];
|
|
141
|
+
let index = 0;
|
|
142
|
+
for (const item of picker.getItems()) {
|
|
143
|
+
if (item.isGroupHeader) {
|
|
144
|
+
rows.push({ header: item.label });
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
const provider = item.id;
|
|
148
|
+
const configured = picker.configuredProviders.has(provider);
|
|
149
|
+
const via = picker.configuredViaMap.get(provider) ?? (configured ? 'configured' : 'not configured');
|
|
150
|
+
const i = providers.indexOf(provider);
|
|
151
|
+
rows.push({
|
|
152
|
+
label: provider,
|
|
153
|
+
desc: via,
|
|
154
|
+
right: `${counts.get(provider) ?? 0} models`,
|
|
155
|
+
mark: configured ? '●' : '○',
|
|
156
|
+
markFg: configured ? t.success : t.warning,
|
|
157
|
+
selected: (i >= 0 ? i : index) === picker.selectedIndex,
|
|
158
|
+
});
|
|
159
|
+
index++;
|
|
240
160
|
}
|
|
241
|
-
if (
|
|
242
|
-
|
|
161
|
+
if (rows.length === 0 && !picker.catalogLoading) rows.push({ label: picker.query ? `No providers match "${picker.query}".` : 'No providers are registered.', muted: true });
|
|
162
|
+
return rows;
|
|
243
163
|
}
|
|
244
164
|
|
|
245
|
-
function
|
|
246
|
-
const
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
const line = lines[visibleRow]!;
|
|
258
|
-
if (!model) continue;
|
|
259
|
-
const selected = absolute === picker.selectedIndex;
|
|
260
|
-
const bg = selected ? PALETTE.selectedBg : PALETTE.bodyBg;
|
|
261
|
-
fillRange(line, startX, startX + width - 1, bg);
|
|
262
|
-
const marker = selected ? (picker.focusPane === 'items' ? GLYPHS.navigation.selected : '•') : ' ';
|
|
263
|
-
const caps = model.capabilities ?? {};
|
|
264
|
-
const capText = [
|
|
265
|
-
caps.reasoning ? 'R' : '-',
|
|
266
|
-
caps.multimodal ? 'V' : '-',
|
|
267
|
-
caps.toolCalling ? 'T' : '-',
|
|
268
|
-
caps.codeEditing ? 'C' : '-',
|
|
269
|
-
].join('');
|
|
270
|
-
let x = startX + 1;
|
|
271
|
-
writeText(line, x, 2, padDisplay(marker, 2), { fg: PALETTE.text, bg, bold: selected }); x += 2;
|
|
272
|
-
writeText(line, x, keyW, padDisplay(modelKey(model), keyW), { fg: selected ? PALETTE.text : PALETTE.muted, bg, bold: selected }); x += keyW + 1;
|
|
273
|
-
writeText(line, x, nameW, padDisplay(model.displayName, nameW), { fg: selected ? PALETTE.subtitle : PALETTE.text, bg }); x += nameW + 1;
|
|
274
|
-
writeText(line, x, providerW, padDisplay(model.provider, providerW), { fg: PALETTE.muted, bg }); x += providerW + 1;
|
|
275
|
-
writeText(line, x, ctxW, padDisplay(formatContext(model.contextWindow), ctxW), { fg: PALETTE.dim, bg }); x += ctxW + 1;
|
|
276
|
-
writeText(line, x, tierW, padDisplay(model.tier ?? 'paid', tierW), { fg: model.tier === 'free' ? PALETTE.good : PALETTE.dim, bg }); x += tierW + 1;
|
|
277
|
-
writeText(line, x, capsW, padDisplay(capText, capsW), { fg: PALETTE.info, bg });
|
|
278
|
-
}
|
|
279
|
-
if (start > 0 && lines[0]) writeText(lines[0], startX + 1, width - 2, `${GLYPHS.navigation.moreAbove} ${start} more model(s) above`, { fg: PALETTE.dim, bg: PALETTE.bodyBg, dim: true });
|
|
280
|
-
if (end < models.length && lines[rows - 1]) writeText(lines[rows - 1]!, startX + 1, width - 2, `${GLYPHS.navigation.moreBelow} ${models.length - end} more model(s) below`, { fg: PALETTE.dim, bg: PALETTE.bodyBg, dim: true });
|
|
165
|
+
function embeddingRows(picker: ModelPickerModal): KitRow[] {
|
|
166
|
+
const t = activeTokens();
|
|
167
|
+
if (picker.embeddingProviders.length === 0) return picker.catalogLoading ? [] : [{ label: 'No embedding providers are registered.', muted: true }];
|
|
168
|
+
return picker.embeddingProviders.map((provider, index) => ({
|
|
169
|
+
label: provider.label,
|
|
170
|
+
desc: provider.detail,
|
|
171
|
+
right: `${provider.dimensions}d · ${provider.configured ? 'configured' : 'unconfigured'}`,
|
|
172
|
+
rightFg: provider.configured ? undefined : t.warning,
|
|
173
|
+
mark: provider.configured ? '●' : '○',
|
|
174
|
+
markFg: provider.configured ? t.success : t.warning,
|
|
175
|
+
selected: index === picker.selectedIndex,
|
|
176
|
+
}));
|
|
281
177
|
}
|
|
282
178
|
|
|
283
179
|
/**
|
|
284
|
-
* The effort step
|
|
285
|
-
*
|
|
286
|
-
*
|
|
287
|
-
*
|
|
288
|
-
* the setting (named levels, a thinking-token budget, an on/off toggle, or
|
|
289
|
-
* nothing at all). A best-guess spec prints its caveat under the list rather
|
|
290
|
-
* than presenting unverified levels as if the provider had confirmed them.
|
|
180
|
+
* The effort step: exactly the levels the chosen model resolved to, introduced
|
|
181
|
+
* by a headline naming what the model does with the setting. A best-guess spec
|
|
182
|
+
* prints its caveat under the list rather than presenting unverified levels as
|
|
183
|
+
* confirmed; a model with nothing to configure gets a statement, not a list.
|
|
291
184
|
*/
|
|
292
|
-
function
|
|
185
|
+
function effortRows(picker: ModelPickerModal): KitRow[] {
|
|
186
|
+
const t = activeTokens();
|
|
293
187
|
const presentation = picker.effortPresentation;
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
if (presentation && rows > 0) {
|
|
297
|
-
fillRange(lines[row]!, startX, startX + width - 1, PALETTE.bodyBg);
|
|
298
|
-
writeText(lines[row]!, startX + 1, width - 2, presentation.headline, { fg: PALETTE.subtitle, bg: PALETTE.bodyBg });
|
|
299
|
-
row += 1;
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
// A model with nothing to configure gets a statement, not an empty list.
|
|
188
|
+
const rows: KitRow[] = [];
|
|
189
|
+
if (presentation) rows.push({ label: presentation.headline, labelFg: t.accent });
|
|
303
190
|
if (presentation && !presentation.configurable) {
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
writeText(lines[row]!, startX + 1, width - 2, 'Esc returns to the model list.', { fg: PALETTE.dim, bg: PALETTE.bodyBg, dim: true });
|
|
307
|
-
}
|
|
308
|
-
return;
|
|
191
|
+
rows.push({ label: 'Esc returns to the model list.', labelFg: t.textFaint });
|
|
192
|
+
return rows;
|
|
309
193
|
}
|
|
194
|
+
picker.effortLevels.forEach((effort, index) => {
|
|
195
|
+
rows.push({ label: effort, desc: presentation?.choices[index]?.description || undefined, selected: index === picker.selectedIndex });
|
|
196
|
+
});
|
|
197
|
+
if (presentation?.caveat && picker.effortLevels.length > 0) rows.push({ label: presentation.caveat, labelFg: t.textFaint });
|
|
198
|
+
return rows;
|
|
199
|
+
}
|
|
310
200
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
}
|
|
201
|
+
// ---------------------------------------------------------------------------
|
|
202
|
+
// Detail inset
|
|
203
|
+
// ---------------------------------------------------------------------------
|
|
204
|
+
|
|
205
|
+
/** Active filters in a few words, '' when none are set. */
|
|
206
|
+
function filterSummary(picker: ModelPickerModal): string {
|
|
207
|
+
const parts: string[] = [];
|
|
208
|
+
if (picker.categoryFilter !== 'all') parts.push(`${picker.categoryFilter} only`);
|
|
209
|
+
if (picker.capabilityFilter !== 'none') parts.push(`needs ${picker.capabilityFilter}`);
|
|
210
|
+
if (picker.availableOnly) parts.push('available only');
|
|
211
|
+
if (picker.benchmarkSort !== 'none') parts.push(`sorted by ${picker.benchmarkSort}`);
|
|
212
|
+
if (picker.groupBy !== 'provider') parts.push(`grouped by ${picker.groupBy}`);
|
|
213
|
+
return parts.join(' · ');
|
|
214
|
+
}
|
|
322
215
|
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
}
|
|
216
|
+
function kv(canvas: ModalFrame['canvas'], p: KitInset, y: number, key: string, value: string, fg?: string): number {
|
|
217
|
+
const t = activeTokens();
|
|
218
|
+
if (y > p.bottom) return y;
|
|
219
|
+
insetPut(canvas, p, p.l, y, key, { fg: t.textMuted });
|
|
220
|
+
return insetWrap(canvas, p, p.l + 12, y, value, { fg: fg ?? t.text });
|
|
327
221
|
}
|
|
328
222
|
|
|
329
|
-
function
|
|
330
|
-
|
|
331
|
-
const
|
|
332
|
-
const
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
const
|
|
341
|
-
|
|
342
|
-
|
|
223
|
+
function drawModelDetail(f: ModalFrame, picker: ModelPickerModal, p: KitInset): void {
|
|
224
|
+
const t = activeTokens();
|
|
225
|
+
const c = f.canvas;
|
|
226
|
+
const model = picker.getSelected();
|
|
227
|
+
let y = p.top;
|
|
228
|
+
if (!model) {
|
|
229
|
+
y = insetWrap(c, p, p.l, y, 'Nothing is selected.', { fg: t.textMuted });
|
|
230
|
+
} else {
|
|
231
|
+
y = insetWrap(c, p, p.l, y, model.displayName || model.id, { fg: t.text, bold: true });
|
|
232
|
+
y = insetWrap(c, p, p.l, y, modelKey(model), { fg: t.textFaint });
|
|
233
|
+
y++;
|
|
234
|
+
const serves = picker.targetInfos.filter((info) => isTargetModel(info, model)).map((info) => targetLabelFor(info.target));
|
|
235
|
+
const configured = picker.configuredProviders.has(model.provider);
|
|
236
|
+
y = kv(c, p, y, 'Provider', model.provider);
|
|
237
|
+
if (model.tier) y = kv(c, p, y, 'Tier', model.tier);
|
|
238
|
+
if (serves.length > 0) y = kv(c, p, y, 'Serves', serves.join(' · '));
|
|
239
|
+
const target = picker.getSelectedTargetInfo();
|
|
240
|
+
if (target && !isTargetModel(target, model)) y = kv(c, p, y, 'Now', `${targetLabelFor(target.target)}: ${targetSummary(target)}`);
|
|
241
|
+
y = kv(c, p, y, 'Status', configured ? 'configured' : 'not configured', configured ? t.success : t.warning);
|
|
242
|
+
if (picker.isPinned(model)) y = kv(c, p, y, 'Pinned', 'yes');
|
|
243
|
+
y++;
|
|
244
|
+
// Meters only where the data exists.
|
|
245
|
+
if (model.contextWindow && y <= p.bottom) {
|
|
246
|
+
insetPut(c, p, p.l, y, 'Context', { fg: t.textMuted });
|
|
247
|
+
const end = meter(c, p.l + 12, y, Math.min(1, model.contextWindow / 1_000_000), t.brand, p.bg);
|
|
248
|
+
insetPut(c, p, end + 2, y++, formatContext(model.contextWindow), { fg: t.textFaint });
|
|
249
|
+
}
|
|
250
|
+
const score = picker.benchmarkScore(model);
|
|
251
|
+
if (score !== null && y <= p.bottom) {
|
|
252
|
+
insetPut(c, p, p.l, y, 'Quality', { fg: t.textMuted });
|
|
253
|
+
const end = meter(c, p.l + 12, y, score, t.success, p.bg);
|
|
254
|
+
insetPut(c, p, end + 2, y++, score.toFixed(2), { fg: t.textFaint });
|
|
255
|
+
}
|
|
256
|
+
if (model.pricing && y <= p.bottom) {
|
|
257
|
+
y = kv(c, p, y, 'Cost', `$${model.pricing.input} in · $${model.pricing.output} out per 1M tokens`);
|
|
258
|
+
}
|
|
259
|
+
const caps = capabilityNames(model);
|
|
260
|
+
if (caps.length > 0 && y <= p.bottom) {
|
|
261
|
+
let cx = p.l;
|
|
262
|
+
for (const cap of caps) {
|
|
263
|
+
if (cx + cap.length + 2 > p.r + 1) break;
|
|
264
|
+
cx = c.put(cx, y, ` ${cap} `, { fg: t.text, bg: t.border }) + 1;
|
|
265
|
+
}
|
|
266
|
+
y += 2;
|
|
267
|
+
}
|
|
268
|
+
if (model.description && y <= p.bottom) y = insetWrap(c, p, p.l, y, model.description, { fg: t.textMuted }) + 1;
|
|
269
|
+
if (picker.isLocalModel(model) && y <= p.bottom) y = insetWrap(c, p, p.l, y, 'Local model: space sets a context cap.', { fg: t.textFaint }) + 1;
|
|
343
270
|
}
|
|
271
|
+
drawTargetFooter(f, picker, p, y);
|
|
344
272
|
}
|
|
345
273
|
|
|
346
|
-
function
|
|
347
|
-
const
|
|
348
|
-
const
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
const
|
|
353
|
-
if (
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
const
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
274
|
+
function drawTargetFooter(f: ModalFrame, picker: ModelPickerModal, p: KitInset, fromY: number): void {
|
|
275
|
+
const t = activeTokens();
|
|
276
|
+
const target = picker.getSelectedTargetInfo();
|
|
277
|
+
const lines: Array<{ text: string; fg: string }> = [];
|
|
278
|
+
const width = p.r - p.l + 1;
|
|
279
|
+
if (target && picker.mode !== 'model') {
|
|
280
|
+
for (const l of wrapLines(`${target.label}: ${targetSummary(target)}`, width)) lines.push({ text: l, fg: t.textMuted });
|
|
281
|
+
if (target.description) for (const l of wrapLines(target.description, width)) lines.push({ text: l, fg: t.textFaint });
|
|
282
|
+
}
|
|
283
|
+
if (picker.mode === 'model') {
|
|
284
|
+
const chords = FILTER_HINTS.map(([key, action]) => `${key} ${action}`).join(' · ');
|
|
285
|
+
for (const l of wrapLines(chords, width)) lines.push({ text: l, fg: t.textFaint });
|
|
286
|
+
}
|
|
287
|
+
// Pinned to the bottom of the inset when there is room, else right after the content.
|
|
288
|
+
let y = Math.max(fromY, p.bottom - lines.length + 1);
|
|
289
|
+
for (const line of lines) {
|
|
290
|
+
if (y > p.bottom) break;
|
|
291
|
+
insetPut(f.canvas, p, p.l, y++, line.text, { fg: line.fg });
|
|
363
292
|
}
|
|
364
|
-
if (start > 0 && lines[0]) writeText(lines[0], startX + 1, width - 2, `${GLYPHS.navigation.moreAbove} ${start} more provider(s) above`, { fg: PALETTE.dim, bg: PALETTE.bodyBg, dim: true });
|
|
365
|
-
if (end < providers.length && lines[rows - 1]) writeText(lines[rows - 1]!, startX + 1, width - 2, `${GLYPHS.navigation.moreBelow} ${providers.length - end} more provider(s) below`, { fg: PALETTE.dim, bg: PALETTE.bodyBg, dim: true });
|
|
366
293
|
}
|
|
367
294
|
|
|
368
|
-
function
|
|
369
|
-
|
|
370
|
-
const
|
|
295
|
+
function drawStepDetail(f: ModalFrame, picker: ModelPickerModal, p: KitInset): void {
|
|
296
|
+
const t = activeTokens();
|
|
297
|
+
const c = f.canvas;
|
|
298
|
+
let y = p.top;
|
|
371
299
|
if (picker.mode === 'provider') {
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
300
|
+
const provider = picker.getFilteredProviders()[picker.selectedIndex] ?? '';
|
|
301
|
+
const target = picker.getSelectedTargetInfo();
|
|
302
|
+
y = insetWrap(c, p, p.l, y, provider || 'No provider selected', { fg: t.text, bold: true });
|
|
303
|
+
y++;
|
|
304
|
+
y = insetWrap(c, p, p.l, y, `Choose a provider, then choose a model for ${target?.label ?? targetLabelFor(picker.target)}.`, { fg: t.textMuted });
|
|
305
|
+
if (provider) {
|
|
306
|
+
const via = picker.configuredViaMap.get(provider);
|
|
307
|
+
y = kv(c, p, y + 1, 'Status', picker.configuredProviders.has(provider) ? `configured${via ? ` via ${via}` : ''}` : 'not configured', picker.configuredProviders.has(provider) ? t.success : t.warning);
|
|
308
|
+
y = kv(c, p, y, 'Models', String(picker.models.filter((m) => m.provider === provider).length));
|
|
309
|
+
}
|
|
310
|
+
} else if (picker.mode === 'embeddingProvider') {
|
|
311
|
+
const selected = picker.embeddingProviders[picker.selectedIndex];
|
|
312
|
+
y = insetWrap(c, p, p.l, y, 'Embedding provider', { fg: t.text, bold: true });
|
|
313
|
+
y = insetWrap(c, p, p.l, y + 1, 'The provider memory search and the code index use to embed content.', { fg: t.textMuted });
|
|
314
|
+
if (selected) {
|
|
315
|
+
y = kv(c, p, y + 1, 'Provider', selected.id);
|
|
316
|
+
y = kv(c, p, y, 'Dimensions', String(selected.dimensions));
|
|
317
|
+
y = kv(c, p, y, 'Status', selected.configured ? 'configured' : 'unconfigured', selected.configured ? t.success : t.warning);
|
|
318
|
+
if (selected.detail) y = insetWrap(c, p, p.l, y + 1, selected.detail, { fg: t.textMuted });
|
|
319
|
+
}
|
|
320
|
+
} else if (picker.mode === 'effort') {
|
|
321
|
+
const model = picker.pendingModel;
|
|
322
|
+
y = insetWrap(c, p, p.l, y, model ? (model.displayName || model.id) : 'Reasoning effort', { fg: t.text, bold: true });
|
|
323
|
+
if (model) y = insetWrap(c, p, p.l, y, modelKey(model), { fg: t.textFaint });
|
|
324
|
+
y = insetWrap(c, p, p.l, y + 1, 'Reasoning effort applies to the main chat model. Select the default effort for this model.', { fg: t.textMuted });
|
|
382
325
|
}
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
326
|
+
drawTargetFooter(f, picker, p, y + 1);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/** The context-cap step: a small form in the list area. */
|
|
330
|
+
function drawContextCap(f: ModalFrame, picker: ModelPickerModal, top: number, x0: number, x1: number): void {
|
|
331
|
+
const t = activeTokens();
|
|
332
|
+
const model = picker.contextCapPendingModel;
|
|
333
|
+
const width = x1 - x0 + 1;
|
|
334
|
+
let y = top;
|
|
335
|
+
const line = (text: string, fg: string, bold = false): void => {
|
|
336
|
+
for (const l of wrapLines(text, width)) if (y <= f.bottom) f.canvas.put(x0, y++, l, { fg, bold });
|
|
337
|
+
};
|
|
338
|
+
line(`Model: ${model ? modelKey(model) : '(none)'}`, t.text);
|
|
339
|
+
line(`Detected context: ${formatContext(model?.contextWindow)}`, t.textMuted);
|
|
340
|
+
y++;
|
|
341
|
+
const input = picker.contextCapQuery.length > 0 ? picker.contextCapQuery : '';
|
|
342
|
+
if (y <= f.bottom) {
|
|
343
|
+
const end = f.canvas.put(x0, y, 'Override ', { fg: t.textMuted });
|
|
344
|
+
const typed = f.canvas.put(end, y, input, { fg: t.text, bold: true });
|
|
345
|
+
f.canvas.put(typed, y, '▏', { fg: t.brand });
|
|
346
|
+
if (!input) f.canvas.put(typed + 1, y, 'uses the detected context', { fg: t.textFaint });
|
|
347
|
+
y += 2;
|
|
386
348
|
}
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
const tierW = 8;
|
|
390
|
-
const capsW = 14;
|
|
391
|
-
const nameW = clamp(Math.floor(width * 0.28), 16, 36);
|
|
392
|
-
const keyW = Math.max(10, width - providerW - ctxW - tierW - capsW - nameW - 10);
|
|
393
|
-
let x = startX + 3;
|
|
394
|
-
writeText(line, x, keyW, padDisplay('Model key', keyW), style); x += keyW + 1;
|
|
395
|
-
writeText(line, x, nameW, padDisplay('Display name', nameW), style); x += nameW + 1;
|
|
396
|
-
writeText(line, x, providerW, padDisplay('Provider', providerW), style); x += providerW + 1;
|
|
397
|
-
writeText(line, x, ctxW, padDisplay('Context', ctxW), style); x += ctxW + 1;
|
|
398
|
-
writeText(line, x, tierW, padDisplay('Tier', tierW), style); x += tierW + 1;
|
|
399
|
-
writeText(line, x, capsW, padDisplay('Caps', capsW), style);
|
|
349
|
+
if (picker.contextCapError) line(picker.contextCapError, t.error);
|
|
350
|
+
line('Type digits to set a cap. Enter confirms; Esc returns to the model list.', t.textFaint);
|
|
400
351
|
}
|
|
401
352
|
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
const
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
drawVertical(header, targetW, PALETTE.footerBg);
|
|
421
|
-
const modeLabel = picker.mode === 'provider' ? 'Provider list' : picker.mode === 'model' ? 'Model list' : picker.mode === 'effort' ? 'Reasoning effort' : picker.mode === 'embeddingProvider' ? 'Embedding providers' : 'Context cap';
|
|
422
|
-
writeText(header, contentX + 1, contentW - 2, `${modeLabel} • ${picker.getItemCount()} item(s)`, { fg: PALETTE.subtitle, bold: true, bg: PALETTE.footerBg });
|
|
423
|
-
lines.push(header);
|
|
424
|
-
|
|
425
|
-
const sep = borderLine(safeWidth, GLYPHS.frame.teeLeft, GLYPHS.frame.horizontal, GLYPHS.frame.teeRight);
|
|
426
|
-
if (targetW > 0 && targetW < safeWidth - 1) sep[targetW] = createStyledCell(GLYPHS.frame.cross, { fg: PALETTE.border });
|
|
427
|
-
lines.push(sep);
|
|
428
|
-
|
|
429
|
-
const footerRows = 2;
|
|
430
|
-
const bodyRows = safeHeight - 3 - footerRows;
|
|
431
|
-
const maxDetailRows = Math.max(3, bodyRows - 2);
|
|
432
|
-
const minDetailRows = Math.min(6, maxDetailRows);
|
|
433
|
-
const details = detailLines(picker, contentW - 2);
|
|
434
|
-
// Never drop detail lines (the modal rule: UI-authored descriptive text is
|
|
435
|
-
// always shown in full). Size the detail band to the ACTUAL content,
|
|
436
|
-
// growing past the old fixed proportional default up to maxDetailRows,
|
|
437
|
-
// the ceiling that keeps at least a header row and one list row available,
|
|
438
|
-
// rather than silently slicing off whatever didn't fit a 32% guess.
|
|
439
|
-
const detailRows = clamp(Math.max(details.length, Math.round(bodyRows * 0.32)), minDetailRows, maxDetailRows);
|
|
440
|
-
const listRows = Math.max(1, bodyRows - detailRows - 1);
|
|
441
|
-
|
|
442
|
-
for (let row = 0; row < bodyRows; row += 1) {
|
|
443
|
-
const inDetail = row < detailRows;
|
|
444
|
-
const line = contentLine(safeWidth, inDetail ? PALETTE.detailBg : PALETTE.bodyBg);
|
|
445
|
-
fillRange(line, 1, targetW - 1, PALETTE.targetBg);
|
|
446
|
-
drawVertical(line, targetW, inDetail ? PALETTE.detailBg : PALETTE.bodyBg);
|
|
447
|
-
renderTargets(picker, line, 1, targetW - 1, row);
|
|
448
|
-
if (inDetail) {
|
|
449
|
-
const text = details[row] ?? '';
|
|
450
|
-
const fg = row === 0 ? PALETTE.title : row <= 2 ? PALETTE.text : PALETTE.muted;
|
|
451
|
-
writeText(line, contentX + 1, contentW - 2, text, { fg, bg: PALETTE.detailBg, bold: row === 0 });
|
|
452
|
-
}
|
|
453
|
-
lines.push(line);
|
|
353
|
+
// ---------------------------------------------------------------------------
|
|
354
|
+
// Hints
|
|
355
|
+
// ---------------------------------------------------------------------------
|
|
356
|
+
|
|
357
|
+
/** The filter chords of the model list (shown in the detail inset, or in the hints without one). */
|
|
358
|
+
const FILTER_HINTS: readonly KitHint[] = [['ctrl+t', 'price'], ['ctrl+k', 'capability'], ['ctrl+a', 'available'], ['ctrl+b', 'benchmark sort'], ['ctrl+g', 'group']];
|
|
359
|
+
|
|
360
|
+
function pickerHints(picker: ModelPickerModal, withFilters: boolean): KitHint[] {
|
|
361
|
+
const target = picker.getSelectedTargetInfo()?.label ?? targetLabelFor(picker.target);
|
|
362
|
+
switch (picker.mode) {
|
|
363
|
+
case 'effort': return [['↑↓', 'move'], ['⏎', 'use this effort'], ['esc', 'back to models']];
|
|
364
|
+
case 'contextCap': return [['0-9', 'cap'], ['⏎', 'confirm'], ['esc', 'back to models']];
|
|
365
|
+
case 'embeddingProvider': return [['↑↓', 'move'], ['⏎', 'use'], ['tab', 'next target']];
|
|
366
|
+
case 'provider': return [['↑↓', 'move'], ['⏎', 'choose provider'], ['tab', 'next target']];
|
|
367
|
+
default: return [
|
|
368
|
+
['⏎', `use for ${target}`], ['tab', 'next target'], ['ctrl+f', 'pin'], ['ctrl+r', 'refresh catalog'],
|
|
369
|
+
...(withFilters ? FILTER_HINTS : []),
|
|
370
|
+
];
|
|
454
371
|
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// ---------------------------------------------------------------------------
|
|
375
|
+
// Renderer
|
|
376
|
+
// ---------------------------------------------------------------------------
|
|
455
377
|
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
378
|
+
const renderCache = new WeakMap<ModelPickerModal, { key: string; tokens: object; layer: SurfaceLayer }>();
|
|
379
|
+
|
|
380
|
+
export function renderModelWorkspace(picker: ModelPickerModal, screenWidth: number, screenHeight: number): SurfaceLayer {
|
|
381
|
+
const cacheKey = getRenderCacheKey(picker, screenWidth, screenHeight);
|
|
382
|
+
const cached = renderCache.get(picker);
|
|
383
|
+
if (cached?.key === cacheKey && cached.tokens === activeTokens()) return cached.layer;
|
|
384
|
+
|
|
385
|
+
const pending = picker.mode === 'effort' ? picker.pendingModel : picker.mode === 'contextCap' ? picker.contextCapPendingModel : null;
|
|
386
|
+
const crumbs = picker.mode === 'effort' ? [pending?.displayName ?? 'model', 'Reasoning effort']
|
|
387
|
+
: picker.mode === 'contextCap' ? [pending?.displayName ?? 'model', 'Context cap']
|
|
388
|
+
: picker.mode === 'provider' ? ['Providers']
|
|
389
|
+
: undefined;
|
|
390
|
+
const inner = modalInnerWidth(standardModalWidth(screenWidth));
|
|
391
|
+
const sideBySide = inner >= 60;
|
|
392
|
+
const f = beginModal(screenWidth, screenHeight, { title: 'Models', crumbs, hints: pickerHints(picker, !sideBySide) });
|
|
393
|
+
|
|
394
|
+
// Targets as tabs; the active tab carries the gradient.
|
|
395
|
+
const labels = picker.targetInfos.length > 0
|
|
396
|
+
? picker.targetInfos.map((info) => `${targetLabelFor(info.target)}${info.enabled ? '' : ' (off)'}`)
|
|
397
|
+
: [targetLabelFor(picker.target)];
|
|
398
|
+
tabs(f.canvas, f.l, f.top, labels, picker.targetInfos.length > 0 ? picker.targetIndex : 0, f.r);
|
|
399
|
+
|
|
400
|
+
const searchable = picker.mode === 'model' || picker.mode === 'provider';
|
|
401
|
+
let body = f.top + 2;
|
|
402
|
+
if (searchable) {
|
|
403
|
+
const total = picker.mode === 'model' ? picker.models.length : picker.providers.length;
|
|
404
|
+
const shown = picker.getItemCount();
|
|
405
|
+
const counted = picker.query.length > 0 || shown !== total ? `${shown.toLocaleString('en-US')} of ${total.toLocaleString('en-US')}` : `${total.toLocaleString('en-US')} ${picker.mode === 'model' ? 'models' : 'providers'}`;
|
|
406
|
+
const filters = picker.mode === 'model' ? filterSummary(picker) : '';
|
|
407
|
+
const count = filters ? `${filters} · ${counted}` : counted;
|
|
408
|
+
searchRow(f, body, picker.query, picker.mode === 'model' ? 'Search models' : 'Search providers', count);
|
|
409
|
+
body += 2;
|
|
460
410
|
}
|
|
461
411
|
|
|
462
|
-
const
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
} else if (picker.mode === 'contextCap') {
|
|
468
|
-
renderContextCapRows(picker, listLines, listLines.length, contentX, contentW - 1);
|
|
469
|
-
} else if (picker.mode === 'embeddingProvider') {
|
|
470
|
-
renderEmbeddingProviderRows(picker, listLines, listLines.length, contentX, contentW - 1);
|
|
412
|
+
const x1 = sideBySide ? f.l + Math.floor(inner * 0.5) - 1 : f.r;
|
|
413
|
+
let result: KitListResult | null = null;
|
|
414
|
+
const scrollKey = { owner: picker, name: picker.mode };
|
|
415
|
+
if (picker.mode === 'contextCap') {
|
|
416
|
+
drawContextCap(f, picker, body, f.l, x1);
|
|
471
417
|
} else {
|
|
472
|
-
|
|
418
|
+
const listed = picker.mode === 'model' ? modelRows(picker, !sideBySide)
|
|
419
|
+
: picker.mode === 'provider' ? providerRows(picker)
|
|
420
|
+
: picker.mode === 'embeddingProvider' ? embeddingRows(picker)
|
|
421
|
+
: effortRows(picker);
|
|
422
|
+
// The picker opens on its cached catalog; the rest fills in while this row shows.
|
|
423
|
+
const rows = picker.catalogLoading && picker.mode !== 'effort' ? [LOADING_ROW, ...listed] : listed;
|
|
424
|
+
result = drawList(f.canvas, { rows, top: body, bottom: f.bottom, x0: f.l, x1, scrollKey });
|
|
473
425
|
}
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
const hints = picker.searchFocused
|
|
484
|
-
? `${targetHint} • Typing filters search; Esc clears search • Down: list shortcuts • Tab price • Esc close`
|
|
485
|
-
: `${targetHint} • Up/Down navigate • Left/Right pane • Enter select • / search • Tab price • C caps • A available • B benchmark • G group • Esc close`;
|
|
486
|
-
writeText(footer, 2, safeWidth - 4, hints, { fg: PALETTE.muted, bg: PALETTE.footerBg });
|
|
487
|
-
lines.push(footer);
|
|
488
|
-
lines.push(borderLine(safeWidth, GLYPHS.frame.bottomLeft, GLYPHS.frame.horizontal, GLYPHS.frame.bottomRight));
|
|
489
|
-
|
|
490
|
-
while (lines.length < safeHeight) lines.push(makeLine(safeWidth));
|
|
491
|
-
const result = lines.slice(0, safeHeight);
|
|
492
|
-
renderCache.set(picker, { key: cacheKey, lines: result });
|
|
493
|
-
return result;
|
|
426
|
+
if (sideBySide) {
|
|
427
|
+
const p = inset(f.canvas, x1 + 3, body, f.r + 2 - (x1 + 3) + 1, f.bottom - body + 1);
|
|
428
|
+
if (picker.mode === 'model' || picker.mode === 'contextCap') drawModelDetail(f, picker, p);
|
|
429
|
+
else drawStepDetail(f, picker, p);
|
|
430
|
+
}
|
|
431
|
+
if (result) f.hintRight = scrollCountText(result.above, result.below);
|
|
432
|
+
const layer = finishModal(f);
|
|
433
|
+
renderCache.set(picker, { key: cacheKey, tokens: activeTokens(), layer });
|
|
434
|
+
return layer;
|
|
494
435
|
}
|
|
495
436
|
|
|
496
|
-
|
|
437
|
+
// ---------------------------------------------------------------------------
|
|
438
|
+
// Render cache key (the catalog can hold thousands of models)
|
|
439
|
+
// ---------------------------------------------------------------------------
|
|
440
|
+
|
|
441
|
+
const objectIds = new WeakMap<object, number>();
|
|
442
|
+
let nextObjectId = 1;
|
|
443
|
+
|
|
444
|
+
function getRenderCacheKey(picker: ModelPickerModal, width: number, height: number): string {
|
|
497
445
|
const base: Array<string | number> = [
|
|
498
446
|
width,
|
|
499
|
-
|
|
447
|
+
height,
|
|
500
448
|
picker.mode,
|
|
501
449
|
picker.target,
|
|
502
|
-
picker.focusPane,
|
|
503
450
|
picker.targetIndex,
|
|
504
451
|
picker.query,
|
|
505
|
-
picker.searchFocused ? 1 : 0,
|
|
506
452
|
picker.selectedIndex,
|
|
507
|
-
picker.scrollOffset,
|
|
508
453
|
picker.categoryFilter,
|
|
509
454
|
picker.capabilityFilter,
|
|
510
455
|
picker.availableOnly ? 1 : 0,
|
|
511
456
|
picker.benchmarkSort,
|
|
512
457
|
picker.groupBy,
|
|
458
|
+
picker.contextCapError ?? '',
|
|
513
459
|
keyForSet(picker.pinnedIds),
|
|
460
|
+
picker.recentIds.join('\u001f'),
|
|
514
461
|
keyForSet(picker.configuredProviders),
|
|
515
462
|
keyForMap(picker.configuredViaMap),
|
|
516
463
|
keyForTargets(picker.targetInfos),
|
|
464
|
+
picker.catalogLoading ? 1 : 0,
|
|
517
465
|
];
|
|
518
|
-
|
|
519
466
|
if (picker.mode === 'model') {
|
|
520
467
|
const filtered = picker.getFilteredModels();
|
|
521
468
|
const selected = filtered[picker.selectedIndex];
|
|
@@ -528,8 +475,7 @@ function getRenderCacheKey(picker: ModelPickerModal, width: number, viewportHeig
|
|
|
528
475
|
objectId(picker.effortLevels),
|
|
529
476
|
picker.effortLevels.join('\u001f'),
|
|
530
477
|
picker.pendingModel?.registryKey ?? picker.pendingModel?.id ?? '',
|
|
531
|
-
// The headline and caveat are rendered content too
|
|
532
|
-
// same level list but different spec kinds must not share a cache entry.
|
|
478
|
+
// The headline and caveat are rendered content too.
|
|
533
479
|
picker.effortPresentation?.headline ?? '',
|
|
534
480
|
picker.effortPresentation?.caveat ?? '',
|
|
535
481
|
);
|
|
@@ -538,7 +484,6 @@ function getRenderCacheKey(picker: ModelPickerModal, width: number, viewportHeig
|
|
|
538
484
|
} else if (picker.mode === 'embeddingProvider') {
|
|
539
485
|
base.push(objectId(picker.embeddingProviders), picker.embeddingProviders.length);
|
|
540
486
|
}
|
|
541
|
-
|
|
542
487
|
return base.join('\u001e');
|
|
543
488
|
}
|
|
544
489
|
|