@pellux/goodvibes-tui 2.0.20 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +70 -0
- package/README.md +16 -16
- package/docs/README.md +4 -4
- package/docs/foundation-artifacts/operator-contract.json +59 -4
- package/package.json +16 -6
- 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-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/verification-ledger.ts +18 -16
- 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/{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
|
@@ -225,6 +225,14 @@ export function buildFilteredModels(
|
|
|
225
225
|
result = [...recent, ...rest];
|
|
226
226
|
}
|
|
227
227
|
|
|
228
|
+
// Pinned models lead (before recent), so the picker's Pinned group sits at
|
|
229
|
+
// the top and up/down walks the list in the order it is drawn. Pins may be
|
|
230
|
+
// recorded by model id or by registry key.
|
|
231
|
+
if (pinnedIds.size > 0) {
|
|
232
|
+
const isPinned = (m: ModelDefinition): boolean => pinnedIds.has(m.id) || pinnedIds.has(m.registryKey);
|
|
233
|
+
result = [...result.filter(isPinned), ...result.filter((m) => !isPinned(m))];
|
|
234
|
+
}
|
|
235
|
+
|
|
228
236
|
const newCache: FilteredModelsCache = {
|
|
229
237
|
modelsRef: models,
|
|
230
238
|
configuredProvidersKey,
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* model-picker-open.ts, opening the model picker without waiting on slow reads.
|
|
3
|
+
*
|
|
4
|
+
* The picker's model and provider lists come from the provider registry's
|
|
5
|
+
* cached catalog, which is synchronous. What used to hold the open back for
|
|
6
|
+
* seconds were the reads awaited before the modal appeared: one credential
|
|
7
|
+
* store lookup per configured provider (which credential source configured
|
|
8
|
+
* it) and the embedding registry's status probe (the Embeddings tab). Those
|
|
9
|
+
* only decorate rows or feed other tabs, so the picker now opens at once on
|
|
10
|
+
* the cached catalog with a muted "loading catalog…" row, and those reads,
|
|
11
|
+
* plus the live re-check of each provider's served models, fill in when they
|
|
12
|
+
* land. A close or a newer open drops any load still in flight.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type { ModelDefinition } from '@pellux/goodvibes-sdk/platform/providers';
|
|
16
|
+
import type {
|
|
17
|
+
EmbeddingProviderPickerEntry,
|
|
18
|
+
ModelPickerCatalogFill,
|
|
19
|
+
ModelPickerModal,
|
|
20
|
+
ModelPickerTargetInfo,
|
|
21
|
+
} from './model-picker.ts';
|
|
22
|
+
|
|
23
|
+
type ConfiguredVia = Map<string, 'env' | 'secrets' | 'subscription' | 'anonymous'>;
|
|
24
|
+
|
|
25
|
+
export interface ModelPickerOpenDeps {
|
|
26
|
+
readonly picker: ModelPickerModal;
|
|
27
|
+
/** Register the picker as the open modal (input focus, modal stack). */
|
|
28
|
+
readonly modalOpened: () => void;
|
|
29
|
+
readonly render: () => void;
|
|
30
|
+
/** The selectable models from the cached catalog (synchronous). */
|
|
31
|
+
readonly listModels: () => ModelDefinition[];
|
|
32
|
+
/** The selectable provider ids from the cached catalog (synchronous). */
|
|
33
|
+
readonly listProviders: () => string[];
|
|
34
|
+
readonly currentModelId: () => string;
|
|
35
|
+
readonly currentProviderId: () => string;
|
|
36
|
+
readonly configuredProviderIds: () => ReadonlySet<string>;
|
|
37
|
+
/** Which credential source configured each provider, given the providers the credential store holds keys for. */
|
|
38
|
+
readonly buildConfiguredVia: (providerIds: readonly string[], configured: ReadonlySet<string>, secretProviderIds: ReadonlySet<string>) => ConfiguredVia;
|
|
39
|
+
readonly buildTargets: (embeddingProviders: EmbeddingProviderPickerEntry[]) => ModelPickerTargetInfo[];
|
|
40
|
+
/** Slow: one credential store read per configured provider. */
|
|
41
|
+
readonly resolveSecretProviderIds: () => Promise<ReadonlySet<string>>;
|
|
42
|
+
/** Slow: the embedding registry's status probe. */
|
|
43
|
+
readonly resolveEmbeddingProviders: () => Promise<EmbeddingProviderPickerEntry[]>;
|
|
44
|
+
/** Optional live re-check of each provider's served models; resolves true when a list changed. */
|
|
45
|
+
readonly refreshLiveModels?: () => Promise<boolean>;
|
|
46
|
+
/** Best-effort prefetches (pinned and recent models); failures stay silent. */
|
|
47
|
+
readonly prefetch?: () => Promise<void>;
|
|
48
|
+
readonly onError: (error: unknown) => void;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Open the picker on the cached catalog right now (it is visible when this
|
|
53
|
+
* returns), then fill in the slow parts. `mode` picks the model list (/model)
|
|
54
|
+
* or the provider list (/provider). The returned promise settles when every
|
|
55
|
+
* fill has landed or been dropped; callers do not need to await it.
|
|
56
|
+
*/
|
|
57
|
+
export function openModelPickerNow(deps: ModelPickerOpenDeps, mode: 'models' | 'providers' = 'models'): Promise<void> {
|
|
58
|
+
const { picker } = deps;
|
|
59
|
+
const configured = new Set(deps.configuredProviderIds());
|
|
60
|
+
const models = deps.listModels();
|
|
61
|
+
const providers = mode === 'providers' ? deps.listProviders() : [...new Set(models.map((m) => m.provider))];
|
|
62
|
+
picker.configuredProviders = configured;
|
|
63
|
+
picker.configuredViaMap = deps.buildConfiguredVia(providers, configured, new Set());
|
|
64
|
+
deps.modalOpened();
|
|
65
|
+
picker.setTargetInfos(deps.buildTargets(picker.embeddingProviders));
|
|
66
|
+
if (mode === 'providers') picker.openProviders(providers, deps.currentProviderId());
|
|
67
|
+
else picker.openAllModels(models, deps.currentModelId());
|
|
68
|
+
const ticket = picker.beginCatalogLoad();
|
|
69
|
+
deps.render();
|
|
70
|
+
|
|
71
|
+
const prefetch = (deps.prefetch?.() ?? Promise.resolve()).catch(() => {}).then(() => deps.render());
|
|
72
|
+
|
|
73
|
+
const decorations = (async () => {
|
|
74
|
+
const [secretIds, embedding] = await Promise.all([deps.resolveSecretProviderIds(), deps.resolveEmbeddingProviders()]);
|
|
75
|
+
const fill: ModelPickerCatalogFill = {
|
|
76
|
+
configuredViaMap: deps.buildConfiguredVia(providers, configured, secretIds),
|
|
77
|
+
embeddingProviders: embedding,
|
|
78
|
+
targetInfos: deps.buildTargets(embedding),
|
|
79
|
+
};
|
|
80
|
+
if (picker.fillCatalog(ticket, fill, false)) deps.render();
|
|
81
|
+
})();
|
|
82
|
+
|
|
83
|
+
const live = (async () => {
|
|
84
|
+
if (!deps.refreshLiveModels) return;
|
|
85
|
+
const changed = await deps.refreshLiveModels().catch(() => false);
|
|
86
|
+
if (!changed) return;
|
|
87
|
+
const fresh = mode === 'providers' ? { providers: deps.listProviders() } : { models: deps.listModels() };
|
|
88
|
+
if (picker.fillCatalog(ticket, fresh, false)) deps.render();
|
|
89
|
+
})();
|
|
90
|
+
|
|
91
|
+
return Promise.all([decorations, live])
|
|
92
|
+
.catch((error: unknown) => deps.onError(error))
|
|
93
|
+
.finally(() => {
|
|
94
|
+
// Loaded or failed, the loading row goes: the picker keeps what it has.
|
|
95
|
+
if (picker.fillCatalog(ticket, {})) deps.render();
|
|
96
|
+
return prefetch;
|
|
97
|
+
})
|
|
98
|
+
.then(() => {});
|
|
99
|
+
}
|
|
@@ -13,7 +13,7 @@ export type PickerMode = 'model' | 'provider' | 'effort' | 'contextCap' | 'embed
|
|
|
13
13
|
*/
|
|
14
14
|
export type ModelPickerTarget = 'main' | 'helper' | 'tool' | 'tts' | 'embeddings';
|
|
15
15
|
|
|
16
|
-
export type
|
|
16
|
+
export type ModelPickerFocusRegion = 'targets' | 'items';
|
|
17
17
|
|
|
18
18
|
export interface ModelPickerTargetInfo {
|
|
19
19
|
readonly target: ModelPickerTarget;
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
} from '../providers/reasoning-effort-surface.ts';
|
|
7
7
|
import type { FavoritesStore } from '@pellux/goodvibes-sdk/platform/providers';
|
|
8
8
|
import type { BenchmarkStore } from '@pellux/goodvibes-sdk/platform/providers';
|
|
9
|
+
import { compositeScore } from '@pellux/goodvibes-sdk/platform/providers';
|
|
9
10
|
import type { ProviderRegistry } from '@pellux/goodvibes-sdk/platform/providers';
|
|
10
11
|
import { detectFamily, POPULAR_PROVIDERS, tierToCategoryFilter } from './model-picker-types.ts';
|
|
11
12
|
import type {
|
|
@@ -17,7 +18,7 @@ import type {
|
|
|
17
18
|
FilteredProvidersCache,
|
|
18
19
|
GroupByMode,
|
|
19
20
|
ModelItemsCache,
|
|
20
|
-
|
|
21
|
+
ModelPickerFocusRegion,
|
|
21
22
|
ModelPickerTarget,
|
|
22
23
|
ModelPickerTargetInfo,
|
|
23
24
|
PickerItem,
|
|
@@ -42,6 +43,15 @@ import {
|
|
|
42
43
|
} from './model-picker-items.ts';
|
|
43
44
|
|
|
44
45
|
export { detectFamily, POPULAR_PROVIDERS, tierToCategoryFilter } from './model-picker-types.ts';
|
|
46
|
+
|
|
47
|
+
/** Catalog data that arrives after the picker opened (see ModelPickerModal.fillCatalog). */
|
|
48
|
+
export interface ModelPickerCatalogFill {
|
|
49
|
+
readonly models?: ModelDefinition[];
|
|
50
|
+
readonly providers?: string[];
|
|
51
|
+
readonly configuredViaMap?: Map<string, 'env' | 'secrets' | 'subscription' | 'anonymous'>;
|
|
52
|
+
readonly embeddingProviders?: EmbeddingProviderPickerEntry[];
|
|
53
|
+
readonly targetInfos?: ModelPickerTargetInfo[];
|
|
54
|
+
}
|
|
45
55
|
export type {
|
|
46
56
|
BenchmarkSort,
|
|
47
57
|
CapabilityFilter,
|
|
@@ -49,7 +59,7 @@ export type {
|
|
|
49
59
|
EmbeddingProviderPickerEntry,
|
|
50
60
|
GroupByMode,
|
|
51
61
|
ModelFamily,
|
|
52
|
-
|
|
62
|
+
ModelPickerFocusRegion,
|
|
53
63
|
ModelPickerTarget,
|
|
54
64
|
ModelPickerTargetInfo,
|
|
55
65
|
PickerItem,
|
|
@@ -81,7 +91,7 @@ export class ModelPickerModal {
|
|
|
81
91
|
public mode: PickerMode = 'model';
|
|
82
92
|
/** Which config target this picker session will write to on commit. */
|
|
83
93
|
public target: ModelPickerTarget = 'main';
|
|
84
|
-
public
|
|
94
|
+
public focusRegion: ModelPickerFocusRegion = 'items';
|
|
85
95
|
public targetInfos: ModelPickerTargetInfo[] = [];
|
|
86
96
|
public targetIndex = 0;
|
|
87
97
|
public searchFocused = false;
|
|
@@ -135,6 +145,15 @@ export class ModelPickerModal {
|
|
|
135
145
|
/** Current group-by mode. */
|
|
136
146
|
public groupBy: GroupByMode = 'provider';
|
|
137
147
|
|
|
148
|
+
/**
|
|
149
|
+
* True from the moment the picker opens on its cached catalog until the
|
|
150
|
+
* slower reads (credential sources, embedding providers, the live model
|
|
151
|
+
* lists) land through fillCatalog. The list shows a muted "loading catalog…"
|
|
152
|
+
* row meanwhile; the picker is usable the whole time.
|
|
153
|
+
*/
|
|
154
|
+
public catalogLoading = false;
|
|
155
|
+
private catalogTicket = 0;
|
|
156
|
+
|
|
138
157
|
private filteredModelsCache: FilteredModelsCache | null = null;
|
|
139
158
|
private filteredProvidersCache: FilteredProvidersCache | null = null;
|
|
140
159
|
private modelItemsCache: ModelItemsCache | null = null;
|
|
@@ -151,12 +170,12 @@ export class ModelPickerModal {
|
|
|
151
170
|
}
|
|
152
171
|
|
|
153
172
|
focusTargets(): void {
|
|
154
|
-
this.
|
|
173
|
+
this.focusRegion = 'targets';
|
|
155
174
|
this.searchFocused = false;
|
|
156
175
|
}
|
|
157
176
|
|
|
158
177
|
focusItems(): void {
|
|
159
|
-
this.
|
|
178
|
+
this.focusRegion = 'items';
|
|
160
179
|
}
|
|
161
180
|
|
|
162
181
|
moveTarget(delta: number): void {
|
|
@@ -294,7 +313,7 @@ export class ModelPickerModal {
|
|
|
294
313
|
this.mode = 'model';
|
|
295
314
|
this.active = true;
|
|
296
315
|
this.pendingModel = null;
|
|
297
|
-
this.
|
|
316
|
+
this.focusRegion = 'items';
|
|
298
317
|
this.searchFocused = false;
|
|
299
318
|
this.focusSearch();
|
|
300
319
|
this.query = '';
|
|
@@ -314,7 +333,7 @@ export class ModelPickerModal {
|
|
|
314
333
|
this.mode = 'provider';
|
|
315
334
|
this.active = true;
|
|
316
335
|
this.pendingModel = null;
|
|
317
|
-
this.
|
|
336
|
+
this.focusRegion = 'items';
|
|
318
337
|
this.searchFocused = false;
|
|
319
338
|
this.focusSearch();
|
|
320
339
|
this.query = '';
|
|
@@ -339,7 +358,7 @@ export class ModelPickerModal {
|
|
|
339
358
|
this.mode = 'embeddingProvider';
|
|
340
359
|
this.active = true;
|
|
341
360
|
this.pendingModel = null;
|
|
342
|
-
this.
|
|
361
|
+
this.focusRegion = 'items';
|
|
343
362
|
this.searchFocused = false;
|
|
344
363
|
this.query = '';
|
|
345
364
|
const idx = providers.findIndex((provider) => provider.id === currentId);
|
|
@@ -379,12 +398,53 @@ export class ModelPickerModal {
|
|
|
379
398
|
this.scrollOffset = 0;
|
|
380
399
|
}
|
|
381
400
|
|
|
401
|
+
/** Mark the catalog as loading; the returned ticket lets fillCatalog drop a load that a close or a newer open overtook. */
|
|
402
|
+
beginCatalogLoad(): number {
|
|
403
|
+
this.catalogLoading = true;
|
|
404
|
+
return ++this.catalogTicket;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Land catalog data that finished loading after the picker opened. The
|
|
409
|
+
* query, filters and the selected row (matched by id) stay as they are.
|
|
410
|
+
* Returns false, changing nothing, when the picker closed or a newer load
|
|
411
|
+
* started since `ticket` was issued.
|
|
412
|
+
*/
|
|
413
|
+
fillCatalog(ticket: number, fill: ModelPickerCatalogFill, done = true): boolean {
|
|
414
|
+
if (!this.active || ticket !== this.catalogTicket) return false;
|
|
415
|
+
const selectedId = this.selectedItemId();
|
|
416
|
+
if (fill.models) this.models = fill.models;
|
|
417
|
+
if (fill.providers) this.providers = fill.providers;
|
|
418
|
+
if (fill.configuredViaMap) this.configuredViaMap = fill.configuredViaMap;
|
|
419
|
+
if (fill.embeddingProviders) this.embeddingProviders = fill.embeddingProviders;
|
|
420
|
+
if (fill.targetInfos) this.targetInfos = fill.targetInfos;
|
|
421
|
+
this.clearCaches();
|
|
422
|
+
if (selectedId !== null && this.mode !== 'effort') {
|
|
423
|
+
const ids = this.mode === 'model' ? this.getFilteredModels().map((m) => m.id)
|
|
424
|
+
: this.mode === 'provider' ? this.getFilteredProviders()
|
|
425
|
+
: this.embeddingProviders.map((provider) => provider.id);
|
|
426
|
+
const idx = ids.indexOf(selectedId);
|
|
427
|
+
this.selectedIndex = idx >= 0 ? idx : Math.min(this.selectedIndex, Math.max(0, ids.length - 1));
|
|
428
|
+
}
|
|
429
|
+
if (done) this.catalogLoading = false;
|
|
430
|
+
return true;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
private selectedItemId(): string | null {
|
|
434
|
+
if (this.mode === 'model') return this.getFilteredModels()[this.selectedIndex]?.id ?? null;
|
|
435
|
+
if (this.mode === 'provider') return this.getFilteredProviders()[this.selectedIndex] ?? null;
|
|
436
|
+
if (this.mode === 'embeddingProvider') return this.embeddingProviders[this.selectedIndex]?.id ?? null;
|
|
437
|
+
return null;
|
|
438
|
+
}
|
|
439
|
+
|
|
382
440
|
/** Close the picker entirely. */
|
|
383
441
|
close(): void {
|
|
442
|
+
this.catalogLoading = false;
|
|
443
|
+
this.catalogTicket++;
|
|
384
444
|
this.active = false;
|
|
385
445
|
this.mode = 'model';
|
|
386
446
|
this.target = 'main';
|
|
387
|
-
this.
|
|
447
|
+
this.focusRegion = 'items';
|
|
388
448
|
this.targetInfos = [];
|
|
389
449
|
this.targetIndex = 0;
|
|
390
450
|
this.models = [];
|
|
@@ -407,6 +467,29 @@ export class ModelPickerModal {
|
|
|
407
467
|
|
|
408
468
|
// ── Search helpers ─────────────────────────────────────────────────────────────────────
|
|
409
469
|
|
|
470
|
+
/** Composite benchmark score (0..1) for a model, or null when there is no benchmark data. */
|
|
471
|
+
benchmarkScore(model: ModelDefinition): number | null {
|
|
472
|
+
const record = this.benchmarkStore.getBenchmarks(model.id) ?? this.benchmarkStore.getBenchmarks(model.displayName);
|
|
473
|
+
return record ? compositeScore(record.benchmarks) : null;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/** Whether a model is pinned (pins may be recorded by id or registry key). */
|
|
477
|
+
isPinned(model: ModelDefinition): boolean {
|
|
478
|
+
return this.pinnedIds.has(model.id) || this.pinnedIds.has(model.registryKey);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/** Flip a model's pin locally (the caller persists it); returns the new state. */
|
|
482
|
+
togglePinnedLocal(model: ModelDefinition): boolean {
|
|
483
|
+
const pinned = this.isPinned(model);
|
|
484
|
+
const next = new Set(this.pinnedIds);
|
|
485
|
+
next.delete(model.id);
|
|
486
|
+
next.delete(model.registryKey);
|
|
487
|
+
if (!pinned) next.add(model.registryKey);
|
|
488
|
+
this.pinnedIds = next;
|
|
489
|
+
this.clearCaches();
|
|
490
|
+
return !pinned;
|
|
491
|
+
}
|
|
492
|
+
|
|
410
493
|
/** Append a character to the search query and clamp selectedIndex. */
|
|
411
494
|
appendChar(ch: string): void {
|
|
412
495
|
this.query += ch;
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* notifications-modal.ts, the notification history (/notifications, ctrl+p
|
|
3
|
+
* "notifications").
|
|
4
|
+
*
|
|
5
|
+
* ↑↓ move, Enter goes to what the notification is about (when it is about
|
|
6
|
+
* something that can be opened), x dismisses the selected one, ctrl+x clears
|
|
7
|
+
* them all. Opening the modal marks everything as seen.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { InputToken } from '@pellux/goodvibes-sdk/platform/core';
|
|
11
|
+
import type { SurfaceModal, SurfaceModalHost } from './surface-modal-host.ts';
|
|
12
|
+
import type { SurfaceLayer } from '../renderer/surface-kit.ts';
|
|
13
|
+
import { renderNotificationsModal, type NotificationsModalView } from '../renderer/notifications-modal.ts';
|
|
14
|
+
import type { NotificationFeedEntry, NotificationFeed } from '../views/notifications-feed.ts';
|
|
15
|
+
|
|
16
|
+
export interface NotificationsModalOptions {
|
|
17
|
+
readonly feed: NotificationFeed;
|
|
18
|
+
/**
|
|
19
|
+
* The opener for the view a notification is about (views.ts resolves the
|
|
20
|
+
* name), or null when nothing holds it.
|
|
21
|
+
*/
|
|
22
|
+
readonly resolveSubject: (subject: string) => (() => void) | null;
|
|
23
|
+
/** Clock (tests pass a fixed one). */
|
|
24
|
+
readonly now?: () => number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export class NotificationsModal implements SurfaceModal, NotificationsModalView {
|
|
28
|
+
readonly name = 'notifications';
|
|
29
|
+
selectedIndex = 0;
|
|
30
|
+
status: string | null = null;
|
|
31
|
+
/** Unread when the modal opened (opening marks everything seen). */
|
|
32
|
+
readonly unread: number;
|
|
33
|
+
private readonly unreadKeys: ReadonlySet<string>;
|
|
34
|
+
private readonly unsubscribe: () => void;
|
|
35
|
+
|
|
36
|
+
constructor(private readonly options: NotificationsModalOptions) {
|
|
37
|
+
const feed = options.feed;
|
|
38
|
+
this.unread = feed.unreadCount();
|
|
39
|
+
this.unreadKeys = new Set(feed.list().filter((entry) => feed.isUnread(entry)).map((entry) => entry.key));
|
|
40
|
+
feed.markAllSeen();
|
|
41
|
+
this.unsubscribe = feed.subscribe(() => {
|
|
42
|
+
this.selectedIndex = Math.max(0, Math.min(this.selectedIndex, this.entries.length - 1));
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
get entries(): readonly NotificationFeedEntry[] {
|
|
47
|
+
return this.options.feed.list();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
get now(): number {
|
|
51
|
+
return (this.options.now ?? Date.now)();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
isUnread = (entry: NotificationFeedEntry): boolean => this.unreadKeys.has(entry.key);
|
|
55
|
+
|
|
56
|
+
onClose(): void {
|
|
57
|
+
this.unsubscribe();
|
|
58
|
+
this.options.feed.markAllSeen();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
private selected(): NotificationFeedEntry | undefined {
|
|
62
|
+
return this.entries[this.selectedIndex];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
private move(delta: number): void {
|
|
66
|
+
const count = this.entries.length;
|
|
67
|
+
if (count === 0) return;
|
|
68
|
+
this.selectedIndex = Math.max(0, Math.min(count - 1, this.selectedIndex + delta));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
private dismissSelected(): void {
|
|
72
|
+
const entry = this.selected();
|
|
73
|
+
if (!entry) return;
|
|
74
|
+
this.options.feed.dismiss(entry.key);
|
|
75
|
+
this.selectedIndex = Math.max(0, Math.min(this.selectedIndex, this.entries.length - 1));
|
|
76
|
+
this.status = `Dismissed "${entry.title}".`;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
private goTo(host: SurfaceModalHost): void {
|
|
80
|
+
const entry = this.selected();
|
|
81
|
+
if (!entry) return;
|
|
82
|
+
if (!entry.subject) {
|
|
83
|
+
this.status = 'This notification is not about anything that can be opened.';
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
const open = this.options.resolveSubject(entry.subject);
|
|
87
|
+
if (!open) {
|
|
88
|
+
this.status = `Nothing to open for "${entry.subject}".`;
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
host.close(this, 'done');
|
|
92
|
+
open();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
handleToken(token: InputToken, host: SurfaceModalHost): void {
|
|
96
|
+
if (token.type === 'text') {
|
|
97
|
+
if (token.value === 'x') this.dismissSelected();
|
|
98
|
+
else if (token.value === 'j') this.move(1);
|
|
99
|
+
else if (token.value === 'k') this.move(-1);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
if (token.type !== 'key') return;
|
|
103
|
+
const key = token.logicalName ?? '';
|
|
104
|
+
if (token.ctrl && key === 'x') {
|
|
105
|
+
const count = this.entries.length;
|
|
106
|
+
this.options.feed.clear();
|
|
107
|
+
this.selectedIndex = 0;
|
|
108
|
+
this.status = count > 0 ? `Cleared ${count} notification${count === 1 ? '' : 's'}.` : null;
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
if (key === 'up') this.move(-1);
|
|
112
|
+
else if (key === 'down') this.move(1);
|
|
113
|
+
else if (key === 'pageup') this.move(-10);
|
|
114
|
+
else if (key === 'pagedown') this.move(10);
|
|
115
|
+
else if (key === 'home') this.selectedIndex = 0;
|
|
116
|
+
else if (key === 'end') this.selectedIndex = Math.max(0, this.entries.length - 1);
|
|
117
|
+
else if (key === 'enter') this.goTo(host);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
render(screenWidth: number, screenHeight: number): SurfaceLayer {
|
|
121
|
+
return renderNotificationsModal(this, screenWidth, screenHeight);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* overlay-filter.ts, the always-live search row state for the help and
|
|
3
|
+
* keyboard-shortcuts overlays. Their visibility and scroll offsets are plain
|
|
4
|
+
* handler fields copied through the feed context; the query lives in these
|
|
5
|
+
* long-lived objects instead, shared by reference between the input route
|
|
6
|
+
* (which types into it) and the renderer (which filters with it and records
|
|
7
|
+
* how far the list can scroll, so ↓ never runs past the end).
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export class OverlayFilter {
|
|
11
|
+
/** Typed filter text. */
|
|
12
|
+
public query = '';
|
|
13
|
+
/** Largest useful scroll offset for the current query, recorded by the renderer. */
|
|
14
|
+
public maxScroll = 0;
|
|
15
|
+
|
|
16
|
+
clear(): void {
|
|
17
|
+
this.query = '';
|
|
18
|
+
this.maxScroll = 0;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** One filter per overlay. */
|
|
23
|
+
export class OverlayFilters {
|
|
24
|
+
public readonly help = new OverlayFilter();
|
|
25
|
+
public readonly shortcuts = new OverlayFilter();
|
|
26
|
+
}
|
|
@@ -57,6 +57,8 @@ export class ProfilePickerModal {
|
|
|
57
57
|
|
|
58
58
|
/** Last status message (success/error feedback). */
|
|
59
59
|
public statusMessage = '';
|
|
60
|
+
/** The always-live search row's query; selectedIndex indexes visibleProfiles. */
|
|
61
|
+
public query = '';
|
|
60
62
|
|
|
61
63
|
public constructor(private readonly profileManager: ProfileManager) {}
|
|
62
64
|
|
|
@@ -69,6 +71,7 @@ export class ProfilePickerModal {
|
|
|
69
71
|
this.scrollOffset = 0;
|
|
70
72
|
this.statusMessage = '';
|
|
71
73
|
this.deleteConfirmationTarget = null;
|
|
74
|
+
this.query = '';
|
|
72
75
|
this.active = true;
|
|
73
76
|
}
|
|
74
77
|
|
|
@@ -76,18 +79,44 @@ export class ProfilePickerModal {
|
|
|
76
79
|
this.active = false;
|
|
77
80
|
this.statusMessage = '';
|
|
78
81
|
this.deleteConfirmationTarget = null;
|
|
82
|
+
this.query = '';
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** The profiles the search row lets through, in order. */
|
|
86
|
+
get visibleProfiles(): ProfileInfo[] {
|
|
87
|
+
const q = this.query.trim().toLowerCase();
|
|
88
|
+
if (!q) return this.profiles;
|
|
89
|
+
return this.profiles.filter((profile) => profile.name.toLowerCase().includes(q));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Replace the query; the selection moves to the first match and any armed delete is dropped. */
|
|
93
|
+
setQuery(query: string): void {
|
|
94
|
+
this.query = query;
|
|
95
|
+
this.selectedIndex = 0;
|
|
96
|
+
this.scrollOffset = 0;
|
|
97
|
+
this.deleteConfirmationTarget = null;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Esc's first level: drop an armed delete. Returns true when one was armed. */
|
|
101
|
+
cancelDeleteConfirmation(): boolean {
|
|
102
|
+
if (this.deleteConfirmationTarget === null) return false;
|
|
103
|
+
this.deleteConfirmationTarget = null;
|
|
104
|
+
this.statusMessage = '';
|
|
105
|
+
return true;
|
|
79
106
|
}
|
|
80
107
|
|
|
81
108
|
moveUp(): void {
|
|
82
|
-
|
|
83
|
-
|
|
109
|
+
const n = this.visibleProfiles.length;
|
|
110
|
+
if (n === 0) return;
|
|
111
|
+
this.selectedIndex = (this.selectedIndex - 1 + n) % n;
|
|
84
112
|
this._clampScroll();
|
|
85
113
|
this.deleteConfirmationTarget = null;
|
|
86
114
|
}
|
|
87
115
|
|
|
88
116
|
moveDown(): void {
|
|
89
|
-
|
|
90
|
-
|
|
117
|
+
const n = this.visibleProfiles.length;
|
|
118
|
+
if (n === 0) return;
|
|
119
|
+
this.selectedIndex = (this.selectedIndex + 1) % n;
|
|
91
120
|
this._clampScroll();
|
|
92
121
|
this.deleteConfirmationTarget = null;
|
|
93
122
|
}
|
|
@@ -98,7 +127,7 @@ export class ProfilePickerModal {
|
|
|
98
127
|
}
|
|
99
128
|
|
|
100
129
|
getSelected(): ProfileInfo | null {
|
|
101
|
-
return this.
|
|
130
|
+
return this.visibleProfiles[this.selectedIndex] ?? null;
|
|
102
131
|
}
|
|
103
132
|
|
|
104
133
|
/**
|
|
@@ -162,8 +191,8 @@ export class ProfilePickerModal {
|
|
|
162
191
|
return false;
|
|
163
192
|
}
|
|
164
193
|
this.profiles = this.profileManager.list();
|
|
165
|
-
if (this.selectedIndex >= this.
|
|
166
|
-
this.selectedIndex = Math.max(0, this.
|
|
194
|
+
if (this.selectedIndex >= this.visibleProfiles.length) {
|
|
195
|
+
this.selectedIndex = Math.max(0, this.visibleProfiles.length - 1);
|
|
167
196
|
}
|
|
168
197
|
this._clampScroll();
|
|
169
198
|
this.deleteConfirmationTarget = null;
|
|
@@ -216,7 +245,7 @@ export class ProfilePickerModal {
|
|
|
216
245
|
} else if (this.selectedIndex >= this.scrollOffset + visRows) {
|
|
217
246
|
this.scrollOffset = this.selectedIndex - visRows + 1;
|
|
218
247
|
}
|
|
219
|
-
const maxOffset = Math.max(0, this.
|
|
248
|
+
const maxOffset = Math.max(0, this.visibleProfiles.length - visRows);
|
|
220
249
|
this.scrollOffset = Math.max(0, Math.min(this.scrollOffset, maxOffset));
|
|
221
250
|
}
|
|
222
251
|
}
|
package/src/input/search.ts
CHANGED
|
@@ -146,7 +146,7 @@ export class SearchManager {
|
|
|
146
146
|
// attempt to reveal a header-corpus hit, since there is no single
|
|
147
147
|
// member to isolate it to.
|
|
148
148
|
const headerHits = countOccurrences(block.rawContent.toLowerCase(), lowerQuery);
|
|
149
|
-
const allKeys = [block.collapseKey, ...block.groupMemberIndexes.map((idx) => `msg_${idx}`)];
|
|
149
|
+
const allKeys = [block.collapseKey, ...block.groupMemberIndexes.map((idx, k) => block.groupMemberKeys?.[k] ?? `msg_${idx}`)];
|
|
150
150
|
for (let i = 0; i < headerHits; i++) {
|
|
151
151
|
hiddenMatches.push({
|
|
152
152
|
line: block.startLine, col: 0, length: query.length,
|
|
@@ -159,10 +159,10 @@ export class SearchManager {
|
|
|
159
159
|
// whole message array and search() runs on every keystroke, so a
|
|
160
160
|
// per-member read would stall typing outright.
|
|
161
161
|
const snapshot = conversationManager.getMessageSnapshot();
|
|
162
|
-
for (const memberIdx of block.groupMemberIndexes) {
|
|
162
|
+
for (const [k, memberIdx] of block.groupMemberIndexes.entries()) {
|
|
163
163
|
const content = snapshot[memberIdx]?.content;
|
|
164
164
|
if (typeof content !== 'string') continue;
|
|
165
|
-
const memberKey = `msg_${memberIdx}`;
|
|
165
|
+
const memberKey = block.groupMemberKeys?.[k] ?? `msg_${memberIdx}`;
|
|
166
166
|
const memberHits = countOccurrences(content.toLowerCase(), lowerQuery);
|
|
167
167
|
for (let i = 0; i < memberHits; i++) {
|
|
168
168
|
hiddenMatches.push({
|
|
@@ -40,6 +40,8 @@ export interface SelectionOpenOpts {
|
|
|
40
40
|
allowSearch?: boolean;
|
|
41
41
|
customActions?: Map<string, SelectionAction>;
|
|
42
42
|
primaryVerbLabel?: string;
|
|
43
|
+
/** Called with the highlighted row whenever the cursor lands on a different row (live preview). */
|
|
44
|
+
onHighlight?: (item: SelectionItem | null) => void;
|
|
43
45
|
}
|
|
44
46
|
|
|
45
47
|
export interface PendingSelectionRequest {
|