@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
|
@@ -4,6 +4,7 @@ import type { SelectionResult, SelectionAction } from './selection-modal.ts';
|
|
|
4
4
|
import type { CommandContext } from './command-registry.ts';
|
|
5
5
|
import type { ConfigModal } from './config-modal.ts';
|
|
6
6
|
import { openTtsProviderPicker, openTtsVoicePicker } from './tts-settings-actions.ts';
|
|
7
|
+
import { openThemePicker } from './theme-settings-actions.ts';
|
|
7
8
|
import { isTextBackspace } from './delete-key-policy.ts';
|
|
8
9
|
import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
|
|
9
10
|
|
|
@@ -11,14 +12,11 @@ type SelectionRouteState = {
|
|
|
11
12
|
selectionModal: {
|
|
12
13
|
active: boolean;
|
|
13
14
|
query: string;
|
|
14
|
-
searchFocused: boolean;
|
|
15
15
|
allowSearch: boolean;
|
|
16
16
|
customActions: Map<string, SelectionAction>;
|
|
17
17
|
selectedIndex: number;
|
|
18
18
|
getSelected: () => SelectionResult['item'] | null | undefined;
|
|
19
19
|
setQuery: (query: string) => void;
|
|
20
|
-
focusSearch: () => void;
|
|
21
|
-
blurSearch: () => void;
|
|
22
20
|
moveUp: () => void;
|
|
23
21
|
moveDown: () => void;
|
|
24
22
|
close: () => void;
|
|
@@ -77,44 +75,42 @@ export function handleSelectionModalToken(state: SelectionRouteState, token: Inp
|
|
|
77
75
|
return shift ? baseStep * 10 : baseStep;
|
|
78
76
|
};
|
|
79
77
|
|
|
78
|
+
const fireClaimedKey = (key: string): boolean => {
|
|
79
|
+
const action = state.selectionModal.customActions.get(key);
|
|
80
|
+
if (!action) return false;
|
|
81
|
+
const selected = state.selectionModal.getSelected();
|
|
82
|
+
if (selected) dispatchSelectionAction(action, selected);
|
|
83
|
+
return true;
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const fireSpace = (): void => {
|
|
87
|
+
const selected = state.selectionModal.getSelected();
|
|
88
|
+
const action = getSpaceAction(selected);
|
|
89
|
+
if (action && selected && state.selectionCallback) {
|
|
90
|
+
state.selectionCallback({ item: selected, action });
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
// The search row is always live (no search mode): printable text goes into
|
|
95
|
+
// the query, except a key the picker claims (a custom action, or Space for
|
|
96
|
+
// a toggle) which fires while the query is empty. Pickers without search
|
|
97
|
+
// only have their claimed keys.
|
|
80
98
|
if (token.type === 'text') {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
99
|
+
const queryEmpty = state.selectionModal.query.length === 0;
|
|
100
|
+
const single = [...token.value].length === 1;
|
|
101
|
+
if (queryEmpty && single && token.value === ' ' && getSpaceAction(state.selectionModal.getSelected())) {
|
|
102
|
+
fireSpace();
|
|
103
|
+
} else if (queryEmpty && single && state.selectionModal.customActions.has(token.value)) {
|
|
104
|
+
fireClaimedKey(token.value);
|
|
105
|
+
} else if (state.selectionModal.allowSearch) {
|
|
84
106
|
state.selectionModal.setQuery(state.selectionModal.query + token.value);
|
|
85
|
-
} else if (token.value === ' ') {
|
|
86
|
-
|
|
87
|
-
const action = getSpaceAction(selected);
|
|
88
|
-
if (action && selected && state.selectionCallback) {
|
|
89
|
-
state.selectionCallback({ item: selected, action });
|
|
90
|
-
}
|
|
91
|
-
} else {
|
|
92
|
-
const action = state.selectionModal.customActions.get(token.value);
|
|
93
|
-
if (action) {
|
|
94
|
-
const selected = state.selectionModal.getSelected();
|
|
95
|
-
if (selected) {
|
|
96
|
-
dispatchSelectionAction(action, selected);
|
|
97
|
-
}
|
|
98
|
-
} else if (state.selectionModal.allowSearch) {
|
|
99
|
-
// instant filter: an unclaimed keystroke arms search AND starts
|
|
100
|
-
// the query immediately, instead of silently doing nothing until the
|
|
101
|
-
// user discovers '/' first (the "help search needs '/' arming while
|
|
102
|
-
// the palette filters instantly" evaluator finding, /help has no
|
|
103
|
-
// customActions, so every letter used to be swallowed here). '/'
|
|
104
|
-
// above still works too; this is additive, not a replacement, and
|
|
105
|
-
// never fires for a claimed hotkey letter (checked first, above).
|
|
106
|
-
state.selectionModal.focusSearch();
|
|
107
|
-
state.selectionModal.setQuery(token.value);
|
|
108
|
-
}
|
|
107
|
+
} else if (single && token.value === ' ') {
|
|
108
|
+
fireSpace();
|
|
109
109
|
}
|
|
110
110
|
} else if (token.type === 'key') {
|
|
111
111
|
if (token.logicalName === 'escape') {
|
|
112
|
-
// ONE Escape always closes the modal,
|
|
113
|
-
//
|
|
114
|
-
// two-stage contract (1st Esc clears query, 2nd blurs search, 3rd
|
|
115
|
-
// finally closes) was the "help modal took 3 Escapes after searching"
|
|
116
|
-
// evaluator finding (searchFocused is not modal-specific, every
|
|
117
|
-
// SelectionModal-based picker had the same multi-Esc pattern).
|
|
112
|
+
// ONE Escape always closes the modal, whatever the query holds. Clearing
|
|
113
|
+
// the query is Backspace's job, not Esc's.
|
|
118
114
|
state.handleEscape();
|
|
119
115
|
return true;
|
|
120
116
|
}
|
|
@@ -124,24 +120,12 @@ export function handleSelectionModalToken(state: SelectionRouteState, token: Inp
|
|
|
124
120
|
dispatchSelectionAction(getPrimaryAction(selected) ?? 'select', selected);
|
|
125
121
|
}
|
|
126
122
|
} else if (token.logicalName === 'space') {
|
|
127
|
-
|
|
128
|
-
const action = getSpaceAction(selected);
|
|
129
|
-
if (action && selected && state.selectionCallback) {
|
|
130
|
-
state.selectionCallback({ item: selected, action });
|
|
131
|
-
}
|
|
123
|
+
fireSpace();
|
|
132
124
|
} else if (token.logicalName === 'up') {
|
|
133
|
-
|
|
134
|
-
state.selectionModal.focusSearch();
|
|
135
|
-
} else {
|
|
136
|
-
state.selectionModal.moveUp();
|
|
137
|
-
}
|
|
125
|
+
state.selectionModal.moveUp();
|
|
138
126
|
} else if (token.logicalName === 'down') {
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
} else {
|
|
142
|
-
state.selectionModal.moveDown();
|
|
143
|
-
}
|
|
144
|
-
} else if ((token.logicalName === 'left' || token.logicalName === 'right') && !state.selectionModal.searchFocused) {
|
|
127
|
+
state.selectionModal.moveDown();
|
|
128
|
+
} else if (token.logicalName === 'left' || token.logicalName === 'right') {
|
|
145
129
|
const selected = state.selectionModal.getSelected();
|
|
146
130
|
if (selected?.adjustable) {
|
|
147
131
|
dispatchSelectionAction(
|
|
@@ -151,22 +135,16 @@ export function handleSelectionModalToken(state: SelectionRouteState, token: Inp
|
|
|
151
135
|
);
|
|
152
136
|
}
|
|
153
137
|
} else if (isTextBackspace(token.logicalName ?? '')) {
|
|
154
|
-
if (state.selectionModal.allowSearch && state.selectionModal.
|
|
138
|
+
if (state.selectionModal.allowSearch && state.selectionModal.query.length > 0) {
|
|
155
139
|
state.selectionModal.setQuery(state.selectionModal.query.slice(0, -1));
|
|
156
140
|
}
|
|
157
141
|
// 'delete' is intentionally absent here: modal search filters are
|
|
158
142
|
// end-anchored with no cursor, so forward-delete is a no-op per the
|
|
159
143
|
// delete-key policy (src/input/delete-key-policy.ts).
|
|
160
|
-
} else if (
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
if (action) {
|
|
165
|
-
const selected = state.selectionModal.getSelected();
|
|
166
|
-
if (selected) {
|
|
167
|
-
dispatchSelectionAction(action, selected);
|
|
168
|
-
}
|
|
169
|
-
}
|
|
144
|
+
} else if (token.logicalName && [...token.logicalName].length === 1) {
|
|
145
|
+
// A modified letter (a CSI-u chord): fires a claimed action whatever
|
|
146
|
+
// the query holds, it is never typed.
|
|
147
|
+
fireClaimedKey(token.logicalName);
|
|
170
148
|
}
|
|
171
149
|
}
|
|
172
150
|
|
|
@@ -178,6 +156,8 @@ type BookmarkRouteState = {
|
|
|
178
156
|
bookmarkModal: {
|
|
179
157
|
active: boolean;
|
|
180
158
|
entries: Array<unknown>;
|
|
159
|
+
query: string;
|
|
160
|
+
setQuery: (query: string) => void;
|
|
181
161
|
moveUp: () => void;
|
|
182
162
|
moveDown: () => void;
|
|
183
163
|
getSelected: () => { key: string } | null;
|
|
@@ -190,33 +170,49 @@ type BookmarkRouteState = {
|
|
|
190
170
|
handleEscape: () => void;
|
|
191
171
|
};
|
|
192
172
|
|
|
173
|
+
/**
|
|
174
|
+
* Bookmarks: the search row is always live. `d` (remove) and `o` (open the
|
|
175
|
+
* saved file) fire while the query is empty; otherwise every printable key is
|
|
176
|
+
* typed into the query. Backspace edits it; Esc closes.
|
|
177
|
+
*/
|
|
193
178
|
export function handleBookmarkModalToken(state: BookmarkRouteState, token: InputToken): boolean {
|
|
194
179
|
if (!state.bookmarkModal.active) return false;
|
|
180
|
+
const modal = state.bookmarkModal;
|
|
181
|
+
|
|
182
|
+
const act = (key: string): boolean => {
|
|
183
|
+
if (key === 'd') {
|
|
184
|
+
modal.removeSelected();
|
|
185
|
+
if (modal.entries.length === 0) modal.close();
|
|
186
|
+
return true;
|
|
187
|
+
}
|
|
188
|
+
if (key === 'o') {
|
|
189
|
+
modal.openSelectedFile();
|
|
190
|
+
return true;
|
|
191
|
+
}
|
|
192
|
+
return false;
|
|
193
|
+
};
|
|
195
194
|
|
|
196
195
|
if (token.type === 'key') {
|
|
197
196
|
if (token.logicalName === 'escape') {
|
|
198
197
|
state.handleEscape();
|
|
199
198
|
return true;
|
|
200
199
|
}
|
|
201
|
-
if (token.logicalName === 'up')
|
|
202
|
-
else if (token.logicalName === 'down')
|
|
200
|
+
if (token.logicalName === 'up') modal.moveUp();
|
|
201
|
+
else if (token.logicalName === 'down') modal.moveDown();
|
|
203
202
|
else if (token.logicalName === 'enter') {
|
|
204
|
-
const entry =
|
|
203
|
+
const entry = modal.getSelected();
|
|
205
204
|
if (entry) state.commandContext?.jumpToBookmark?.(entry.key);
|
|
206
|
-
|
|
207
|
-
} else if (token.logicalName
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
205
|
+
modal.close();
|
|
206
|
+
} else if (isTextBackspace(token.logicalName ?? '')) {
|
|
207
|
+
if (modal.query.length > 0) modal.setQuery(modal.query.slice(0, -1));
|
|
208
|
+
} else if (token.logicalName) {
|
|
209
|
+
// Modified letters (CSI-u chords) fire their action whatever the query holds.
|
|
210
|
+
act(token.logicalName);
|
|
212
211
|
}
|
|
213
212
|
} else if (token.type === 'text') {
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
} else if (token.value === 'o') {
|
|
218
|
-
state.bookmarkModal.openSelectedFile();
|
|
219
|
-
}
|
|
213
|
+
const claimed = modal.query.length === 0 && (token.value === 'd' || token.value === 'o');
|
|
214
|
+
if (claimed) act(token.value);
|
|
215
|
+
else modal.setQuery(modal.query + token.value);
|
|
220
216
|
}
|
|
221
217
|
|
|
222
218
|
state.requestRender();
|
|
@@ -228,14 +224,14 @@ type SettingsRouteState = {
|
|
|
228
224
|
active: boolean;
|
|
229
225
|
editingMode: boolean;
|
|
230
226
|
currentCategory: string;
|
|
231
|
-
|
|
227
|
+
focusRegion?: 'categories' | 'settings';
|
|
232
228
|
/** True when the user is actively typing into the search input bar. */
|
|
233
229
|
searchFocused: boolean;
|
|
234
230
|
/** Current cross-category search query. */
|
|
235
231
|
searchQuery: string;
|
|
236
232
|
commitEdit: () => void;
|
|
237
233
|
toggleSelectedFlag: () => void;
|
|
238
|
-
/** Scroll the documentation
|
|
234
|
+
/** Scroll the documentation region (PgUp/PgDn); the renderer clamps and shows honest markers. */
|
|
239
235
|
scrollContext?: (delta: number) => void;
|
|
240
236
|
activateSelected: () => void;
|
|
241
237
|
handleSubscriptionLogoutKey?: (key: string) => 'confirmed' | 'cancelled' | 'absorbed' | 'inactive';
|
|
@@ -246,7 +242,7 @@ type SettingsRouteState = {
|
|
|
246
242
|
moveDown?: () => void;
|
|
247
243
|
focusCategories?: () => void;
|
|
248
244
|
focusSettings?: () => void;
|
|
249
|
-
|
|
245
|
+
toggleFocusRegion?: () => void;
|
|
250
246
|
nextCategory: () => void;
|
|
251
247
|
prevCategory?: () => void;
|
|
252
248
|
editBackspace: () => void;
|
|
@@ -263,7 +259,7 @@ type SettingsRouteState = {
|
|
|
263
259
|
cancelEdit: () => void;
|
|
264
260
|
pendingModelPickerTarget: import('./model-picker.ts').ModelPickerTarget | null;
|
|
265
261
|
pendingProviderModelPickerTarget?: import('./model-picker.ts').ModelPickerTarget | null;
|
|
266
|
-
pendingSettingsPickerAction?: 'tts-provider' | 'tts-voice' | null;
|
|
262
|
+
pendingSettingsPickerAction?: 'tts-provider' | 'tts-voice' | 'theme' | null;
|
|
267
263
|
resetSelected?: () => { key: string; value: unknown } | null;
|
|
268
264
|
initiateResetCategory?: () => void;
|
|
269
265
|
initiateResetAll?: () => void;
|
|
@@ -309,6 +305,10 @@ function consumeSettingsPickerRequest(state: SettingsRouteState): void {
|
|
|
309
305
|
openTtsProviderPicker(state.commandContext);
|
|
310
306
|
return;
|
|
311
307
|
}
|
|
308
|
+
if (settingsAction === 'theme') {
|
|
309
|
+
openThemePicker(state.commandContext);
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
312
|
void openTtsVoicePicker(state.commandContext).catch((error: unknown) => {
|
|
313
313
|
state.commandContext?.print(`Unable to list TTS voices: ${summarizeError(error)}`);
|
|
314
314
|
state.requestRender();
|
|
@@ -369,114 +369,104 @@ export function handleSettingsModalToken(state: SettingsRouteState, token: Input
|
|
|
369
369
|
}
|
|
370
370
|
}
|
|
371
371
|
|
|
372
|
+
// The search row is always live: printable text goes to the query. While
|
|
373
|
+
// the query is non-empty the list shows search results across every
|
|
374
|
+
// category (searchFocused); clearing it returns to the category view. Space
|
|
375
|
+
// toggles the selected setting only while the query is empty; resets are
|
|
376
|
+
// key chords (ctrl+r selected, shift+R category, ctrl+shift+R all) so every
|
|
377
|
+
// letter can be searched for.
|
|
378
|
+
const modal = state.settingsModal;
|
|
379
|
+
const searching = modal.searchQuery.length > 0;
|
|
380
|
+
const setQuery = (query: string): void => {
|
|
381
|
+
if (query.length === 0) modal.clearSearch();
|
|
382
|
+
else modal.setSearchQuery(query);
|
|
383
|
+
};
|
|
384
|
+
const resetSelected = (): void => {
|
|
385
|
+
const reset = modal.resetSelected?.();
|
|
386
|
+
if (reset) syncRuntimeAfterSettingReset(state.commandContext, reset.key, reset.value);
|
|
387
|
+
};
|
|
388
|
+
|
|
372
389
|
if (token.type === 'key') {
|
|
373
|
-
const
|
|
390
|
+
const focusRegion = modal.focusRegion ?? 'settings';
|
|
374
391
|
if (token.logicalName === 'escape') {
|
|
375
|
-
//
|
|
376
|
-
//
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
state.settingsModal.cancelEdit();
|
|
380
|
-
state.requestRender();
|
|
381
|
-
return true;
|
|
382
|
-
}
|
|
383
|
-
// Two-stage escape: if in search mode, first Esc exits search (clearSearch),
|
|
384
|
-
// second Esc closes the modal.
|
|
385
|
-
if (state.settingsModal.searchFocused) {
|
|
386
|
-
state.settingsModal.clearSearch();
|
|
392
|
+
// One level per press: an inline edit is a sub-dialog and is cancelled
|
|
393
|
+
// first (mirrors handler-modal-stack.ts); otherwise the modal closes.
|
|
394
|
+
if (modal.editingMode) {
|
|
395
|
+
modal.cancelEdit();
|
|
387
396
|
state.requestRender();
|
|
388
397
|
return true;
|
|
389
398
|
}
|
|
390
399
|
state.handleEscape();
|
|
391
400
|
return true;
|
|
392
401
|
}
|
|
393
|
-
if (token.logicalName === 'enter' || (token.logicalName === 'space' && !
|
|
394
|
-
if (
|
|
395
|
-
else if (
|
|
396
|
-
// Enter in search mode: activate the selected search result
|
|
397
|
-
state.settingsModal.activateSelected();
|
|
398
|
-
consumeSettingsPickerRequest(state);
|
|
399
|
-
} else if (focusPane === 'categories') state.settingsModal.focusSettings?.();
|
|
402
|
+
if (token.logicalName === 'enter' || (token.logicalName === 'space' && !modal.editingMode && !searching)) {
|
|
403
|
+
if (modal.editingMode) modal.commitEdit();
|
|
404
|
+
else if (focusRegion === 'categories' && !searching) modal.focusSettings?.();
|
|
400
405
|
else {
|
|
401
|
-
// Feature-unit toggle headers are boolean settings rows
|
|
402
|
-
// activateSelected toggles them
|
|
403
|
-
//
|
|
404
|
-
|
|
406
|
+
// Feature-unit toggle headers are boolean settings rows, so
|
|
407
|
+
// activateSelected toggles them the same way it edits/toggles any
|
|
408
|
+
// other setting (search results included).
|
|
409
|
+
modal.activateSelected();
|
|
405
410
|
consumeSettingsPickerRequest(state);
|
|
406
411
|
}
|
|
407
|
-
} else if (
|
|
408
|
-
|
|
409
|
-
|
|
412
|
+
} else if (token.logicalName === 'space' && searching && !modal.editingMode) {
|
|
413
|
+
setQuery(`${modal.searchQuery} `);
|
|
414
|
+
} else if ((token.logicalName === 'left' || token.logicalName === 'right') && !modal.editingMode && !searching) {
|
|
415
|
+
if (token.logicalName === 'left') modal.focusCategories?.();
|
|
416
|
+
else modal.focusSettings?.();
|
|
410
417
|
} else if (token.logicalName === 'up') {
|
|
411
|
-
if (
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
else state.settingsModal.moveUp?.();
|
|
418
|
+
if (searching) modal.moveUp?.();
|
|
419
|
+
else if (modal.moveFocusedUp) modal.moveFocusedUp();
|
|
420
|
+
else modal.moveUp?.();
|
|
415
421
|
} else if (token.logicalName === 'down') {
|
|
416
|
-
if (
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
else state.settingsModal.moveDown?.();
|
|
422
|
+
if (searching) modal.moveDown?.();
|
|
423
|
+
else if (modal.moveFocusedDown) modal.moveFocusedDown();
|
|
424
|
+
else modal.moveDown?.();
|
|
420
425
|
}
|
|
421
|
-
else if (token.logicalName === 'r' && token.shift && token.ctrl && !
|
|
422
|
-
|
|
426
|
+
else if (token.logicalName === 'r' && token.shift && token.ctrl && !modal.editingMode && !searching) {
|
|
427
|
+
modal.initiateResetAll?.();
|
|
423
428
|
}
|
|
424
|
-
else if (token.logicalName === 'r' && token.
|
|
425
|
-
|
|
429
|
+
else if (token.logicalName === 'r' && token.ctrl && !modal.editingMode) {
|
|
430
|
+
resetSelected();
|
|
426
431
|
}
|
|
427
|
-
else if (token.logicalName === 'r' && !
|
|
428
|
-
|
|
429
|
-
if (reset) syncRuntimeAfterSettingReset(state.commandContext, reset.key, reset.value);
|
|
432
|
+
else if (token.logicalName === 'r' && token.shift && !modal.editingMode && !searching) {
|
|
433
|
+
modal.initiateResetCategory?.();
|
|
430
434
|
}
|
|
431
|
-
else if (token.logicalName === 'pageup' && !
|
|
432
|
-
|
|
435
|
+
else if (token.logicalName === 'pageup' && !modal.editingMode) {
|
|
436
|
+
modal.scrollContext?.(-3);
|
|
433
437
|
}
|
|
434
|
-
else if (token.logicalName === 'pagedown' && !
|
|
435
|
-
|
|
438
|
+
else if (token.logicalName === 'pagedown' && !modal.editingMode) {
|
|
439
|
+
modal.scrollContext?.(3);
|
|
436
440
|
}
|
|
437
|
-
else if (token.logicalName === 'tab' && !
|
|
438
|
-
if (
|
|
439
|
-
else if (
|
|
440
|
-
else
|
|
441
|
+
else if (token.logicalName === 'tab' && !searching) {
|
|
442
|
+
if (modal.toggleFocusRegion) modal.toggleFocusRegion();
|
|
443
|
+
else if (focusRegion === 'categories') modal.focusSettings?.();
|
|
444
|
+
else modal.focusCategories?.();
|
|
441
445
|
}
|
|
442
446
|
else if (isTextBackspace(token.logicalName ?? '')) {
|
|
443
|
-
if (
|
|
444
|
-
|
|
445
|
-
} else if (state.settingsModal.searchFocused) {
|
|
446
|
-
// Backspace in search mode: trim query
|
|
447
|
-
const trimmed = state.settingsModal.searchQuery.slice(0, -1);
|
|
448
|
-
state.settingsModal.setSearchQuery(trimmed);
|
|
449
|
-
}
|
|
447
|
+
if (modal.editingMode) modal.editBackspace();
|
|
448
|
+
else if (searching) setQuery(modal.searchQuery.slice(0, -1));
|
|
450
449
|
}
|
|
451
450
|
// token.logicalName === 'delete' is intentionally absent: search filters
|
|
452
451
|
// are end-anchored with no cursor, so forward-delete is a no-op per
|
|
453
452
|
// delete-key policy (src/input/delete-key-policy.ts).
|
|
454
|
-
else if (!state.settingsModal.editingMode && !state.settingsModal.searchFocused && token.logicalName === '/') {
|
|
455
|
-
state.settingsModal.focusSearch();
|
|
456
|
-
}
|
|
457
453
|
} else if (token.type === 'text') {
|
|
458
|
-
if (
|
|
459
|
-
//
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
// / enters search mode
|
|
467
|
-
state.settingsModal.focusSearch();
|
|
468
|
-
} else if (token.value === ' ' && !state.settingsModal.editingMode) {
|
|
469
|
-
const focusPane = state.settingsModal.focusPane ?? 'settings';
|
|
470
|
-
if (focusPane === 'categories') state.settingsModal.focusSettings?.();
|
|
454
|
+
if (modal.editingMode) {
|
|
455
|
+
// Inline edit takes priority over search: characters go to the edit buffer.
|
|
456
|
+
modal.editChar(token.value);
|
|
457
|
+
} else if (searching) {
|
|
458
|
+
setQuery(modal.searchQuery + token.value);
|
|
459
|
+
} else if (token.value === ' ') {
|
|
460
|
+
const focusRegion = modal.focusRegion ?? 'settings';
|
|
461
|
+
if (focusRegion === 'categories') modal.focusSettings?.();
|
|
471
462
|
else {
|
|
472
|
-
|
|
473
|
-
// activateSelected now, so no flags-category special case is needed.
|
|
474
|
-
state.settingsModal.activateSelected();
|
|
463
|
+
modal.activateSelected();
|
|
475
464
|
consumeSettingsPickerRequest(state);
|
|
476
465
|
}
|
|
477
|
-
} else if (token.value
|
|
478
|
-
|
|
479
|
-
|
|
466
|
+
} else if (token.value !== '/') {
|
|
467
|
+
// '/' used to arm search; search is always live now, so it is ignored
|
|
468
|
+
// as a first character rather than searched for.
|
|
469
|
+
setQuery(token.value);
|
|
480
470
|
}
|
|
481
471
|
}
|
|
482
472
|
|
|
@@ -487,6 +477,8 @@ export function handleSettingsModalToken(state: SettingsRouteState, token: Input
|
|
|
487
477
|
type SessionPickerRouteState = {
|
|
488
478
|
sessionPickerModal: {
|
|
489
479
|
active: boolean;
|
|
480
|
+
query: string;
|
|
481
|
+
setQuery: (query: string) => void;
|
|
490
482
|
loadSelected: (conversationManager: CommandContext['session']['conversationManager']) => void;
|
|
491
483
|
moveUp: () => void;
|
|
492
484
|
moveDown: () => void;
|
|
@@ -497,8 +489,14 @@ type SessionPickerRouteState = {
|
|
|
497
489
|
handleEscape: () => void;
|
|
498
490
|
};
|
|
499
491
|
|
|
492
|
+
/**
|
|
493
|
+
* The session picker's search row is always live: printable keys go to the
|
|
494
|
+
* query. `d` arms (then confirms) delete only while the query is empty, so a
|
|
495
|
+
* search can still contain the letter d.
|
|
496
|
+
*/
|
|
500
497
|
export function handleSessionPickerToken(state: SessionPickerRouteState, token: InputToken): boolean {
|
|
501
498
|
if (!state.sessionPickerModal.active) return false;
|
|
499
|
+
const modal = state.sessionPickerModal;
|
|
502
500
|
|
|
503
501
|
if (token.type === 'key') {
|
|
504
502
|
if (token.logicalName === 'escape') {
|
|
@@ -508,13 +506,15 @@ export function handleSessionPickerToken(state: SessionPickerRouteState, token:
|
|
|
508
506
|
if (token.logicalName === 'enter') {
|
|
509
507
|
const conversationManager = state.commandContext?.session.conversationManager;
|
|
510
508
|
if (conversationManager) {
|
|
511
|
-
|
|
509
|
+
modal.loadSelected(conversationManager);
|
|
512
510
|
}
|
|
513
|
-
} else if (token.logicalName === 'up')
|
|
514
|
-
else if (token.logicalName === 'down')
|
|
515
|
-
else if (token.logicalName
|
|
516
|
-
|
|
517
|
-
|
|
511
|
+
} else if (token.logicalName === 'up') modal.moveUp();
|
|
512
|
+
else if (token.logicalName === 'down') modal.moveDown();
|
|
513
|
+
else if (isTextBackspace(token.logicalName ?? '')) modal.setQuery(modal.query.slice(0, -1));
|
|
514
|
+
else if (token.logicalName === 'd' && !token.ctrl && modal.query.length === 0) modal.deleteSelected();
|
|
515
|
+
} else if (token.type === 'text') {
|
|
516
|
+
if (token.value === 'd' && modal.query.length === 0) modal.deleteSelected();
|
|
517
|
+
else modal.setQuery(modal.query + token.value);
|
|
518
518
|
}
|
|
519
519
|
|
|
520
520
|
state.requestRender();
|
|
@@ -524,6 +524,9 @@ export function handleSessionPickerToken(state: SessionPickerRouteState, token:
|
|
|
524
524
|
type ProfilePickerRouteState = {
|
|
525
525
|
profilePickerModal: {
|
|
526
526
|
active: boolean;
|
|
527
|
+
query: string;
|
|
528
|
+
setQuery: (query: string) => void;
|
|
529
|
+
cancelDeleteConfirmation: () => boolean;
|
|
527
530
|
loadSelected: (configManager: CommandContext['platform']['configManager']) => void;
|
|
528
531
|
moveUp: () => void;
|
|
529
532
|
moveDown: () => void;
|
|
@@ -535,32 +538,48 @@ type ProfilePickerRouteState = {
|
|
|
535
538
|
handleEscape: () => void;
|
|
536
539
|
};
|
|
537
540
|
|
|
541
|
+
/**
|
|
542
|
+
* Profiles: the search row is always live. `d` (arm, then delete) and `s`
|
|
543
|
+
* (save the current settings) fire while the query is empty; otherwise every
|
|
544
|
+
* printable key is typed into the query. Esc drops an armed delete first,
|
|
545
|
+
* otherwise it closes.
|
|
546
|
+
*/
|
|
538
547
|
export function handleProfilePickerToken(state: ProfilePickerRouteState, token: InputToken): boolean {
|
|
539
548
|
if (!state.profilePickerModal.active) return false;
|
|
549
|
+
const modal = state.profilePickerModal;
|
|
540
550
|
|
|
541
551
|
const saveCurrent = (): void => {
|
|
542
552
|
if (state.commandContext?.platform.configManager) {
|
|
543
553
|
const name = `profile-${Date.now()}`;
|
|
544
|
-
|
|
554
|
+
modal.saveCurrentAs(name, state.commandContext.platform.configManager);
|
|
545
555
|
}
|
|
546
556
|
};
|
|
557
|
+
const act = (key: string): void => {
|
|
558
|
+
if (key === 'd') modal.deleteSelected();
|
|
559
|
+
else if (key === 's') saveCurrent();
|
|
560
|
+
};
|
|
547
561
|
|
|
548
562
|
if (token.type === 'key') {
|
|
549
563
|
if (token.logicalName === 'escape') {
|
|
564
|
+
if (modal.cancelDeleteConfirmation()) {
|
|
565
|
+
state.requestRender();
|
|
566
|
+
return true;
|
|
567
|
+
}
|
|
550
568
|
state.handleEscape();
|
|
551
569
|
return true;
|
|
552
570
|
}
|
|
553
571
|
if (token.logicalName === 'enter') {
|
|
554
572
|
if (state.commandContext?.platform.configManager) {
|
|
555
|
-
|
|
573
|
+
modal.loadSelected(state.commandContext.platform.configManager);
|
|
556
574
|
}
|
|
557
|
-
} else if (token.logicalName === 'up')
|
|
558
|
-
else if (token.logicalName === 'down')
|
|
559
|
-
else if (token.logicalName
|
|
560
|
-
|
|
575
|
+
} else if (token.logicalName === 'up') modal.moveUp();
|
|
576
|
+
else if (token.logicalName === 'down') modal.moveDown();
|
|
577
|
+
else if (isTextBackspace(token.logicalName ?? '')) {
|
|
578
|
+
if (modal.query.length > 0) modal.setQuery(modal.query.slice(0, -1));
|
|
579
|
+
} else if (token.logicalName) act(token.logicalName);
|
|
561
580
|
} else if (token.type === 'text') {
|
|
562
|
-
if (token.value === 'd')
|
|
563
|
-
else
|
|
581
|
+
if (modal.query.length === 0 && (token.value === 'd' || token.value === 's')) act(token.value);
|
|
582
|
+
else modal.setQuery(modal.query + token.value);
|
|
564
583
|
}
|
|
565
584
|
|
|
566
585
|
state.requestRender();
|
|
@@ -576,20 +595,18 @@ type ConfigModalRouteState = {
|
|
|
576
595
|
|
|
577
596
|
/**
|
|
578
597
|
* Route a key to the generic config-modal host (MIGRATE-TO-MODAL
|
|
579
|
-
* surfaces). The host owns the reserved navigation keys (Esc, up/down
|
|
580
|
-
* left/right
|
|
581
|
-
*
|
|
582
|
-
*
|
|
583
|
-
*
|
|
584
|
-
*
|
|
585
|
-
*
|
|
586
|
-
*
|
|
598
|
+
* surfaces). The host owns the reserved navigation keys (Esc, up/down, tab,
|
|
599
|
+
* left/right) and the always-live search row: a printable key the surface
|
|
600
|
+
* claims as an action fires the action while the query is empty; every other
|
|
601
|
+
* printable key (and any multi-char paste) is typed into the query, and
|
|
602
|
+
* Backspace edits it. Other named keys (Enter, ctrl chords) go to the
|
|
603
|
+
* surface's declarative action table (fireAction handles the two-press
|
|
604
|
+
* confirm for destructive actions). Any unrecognised key is absorbed so the
|
|
605
|
+
* modal stays modal.
|
|
587
606
|
*
|
|
588
|
-
*
|
|
589
|
-
*
|
|
590
|
-
*
|
|
591
|
-
* Escape, Backspace, and the arrow/tab nav keys keep their ordinary meaning
|
|
592
|
-
* (arrows navigate the FILTERED list; Enter still acts on the selected row).
|
|
607
|
+
* Esc pops exactly one level: an armed destructive confirm is cancelled
|
|
608
|
+
* first; otherwise the modal closes through handleEscape. Esc never clears
|
|
609
|
+
* the query as a separate step.
|
|
593
610
|
*/
|
|
594
611
|
export function handleConfigModalToken(state: ConfigModalRouteState, token: InputToken): boolean {
|
|
595
612
|
if (!state.configModal.active) return false;
|
|
@@ -600,32 +617,20 @@ export function handleConfigModalToken(state: ConfigModalRouteState, token: Inpu
|
|
|
600
617
|
// without a keypress (batch refutation finding 3).
|
|
601
618
|
state.configModal.noteInteraction();
|
|
602
619
|
|
|
603
|
-
const filtering = state.configModal.isFilterActive();
|
|
604
|
-
|
|
605
620
|
if (token.type === 'key') {
|
|
606
621
|
if (token.logicalName === 'escape') {
|
|
607
|
-
|
|
608
|
-
// documented exception to single-Esc-close): clearFilterOrFallthrough
|
|
609
|
-
// returns false on an empty query, so an armed-but-empty filter (or no
|
|
610
|
-
// filter at all) closes the modal in one press, same as every other
|
|
611
|
-
// modal here.
|
|
612
|
-
if (state.configModal.clearFilterOrFallthrough()) {
|
|
622
|
+
if (state.configModal.cancelPendingConfirm()) {
|
|
613
623
|
state.requestRender();
|
|
614
624
|
return true;
|
|
615
625
|
}
|
|
616
626
|
state.handleEscape();
|
|
617
627
|
return true;
|
|
618
628
|
}
|
|
619
|
-
if (
|
|
629
|
+
if (isTextBackspace(token.logicalName ?? '')) {
|
|
620
630
|
state.configModal.backspaceFilter();
|
|
621
631
|
state.requestRender();
|
|
622
632
|
return true;
|
|
623
633
|
}
|
|
624
|
-
if (!filtering && token.logicalName === '/') {
|
|
625
|
-
state.configModal.activateFilter();
|
|
626
|
-
state.requestRender();
|
|
627
|
-
return true;
|
|
628
|
-
}
|
|
629
634
|
switch (token.logicalName) {
|
|
630
635
|
case 'up':
|
|
631
636
|
state.configModal.moveUp();
|
|
@@ -645,25 +650,19 @@ export function handleConfigModalToken(state: ConfigModalRouteState, token: Inpu
|
|
|
645
650
|
state.requestRender();
|
|
646
651
|
return true;
|
|
647
652
|
}
|
|
648
|
-
// Any other 'key' token (
|
|
649
|
-
//
|
|
650
|
-
// by the filter (below), so Enter still fires on the current selection.
|
|
653
|
+
// Any other 'key' token (Enter, a ctrl chord) falls through to the
|
|
654
|
+
// action table below, whatever the query holds.
|
|
651
655
|
} else if (token.type === 'text') {
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
state.
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
// The WHOLE token value lands in the filter in one call, including a
|
|
659
|
-
// multi-char paste token, never split into per-char nav/action
|
|
660
|
-
// dispatch (the text-capture invariant this item's brief calls out).
|
|
656
|
+
const claimed = state.configModal.getFilterQuery().length === 0
|
|
657
|
+
&& [...token.value].length === 1
|
|
658
|
+
&& state.configModal.resolveAction(token.value) !== null;
|
|
659
|
+
if (!claimed) {
|
|
660
|
+
// The WHOLE token value lands in the query in one call, including a
|
|
661
|
+
// multi-char paste token, never split into per-char dispatch.
|
|
661
662
|
state.configModal.appendFilterText(token.value);
|
|
662
663
|
state.requestRender();
|
|
663
664
|
return true;
|
|
664
665
|
}
|
|
665
|
-
if (token.value === 'j') { state.configModal.moveDown(); state.requestRender(); return true; }
|
|
666
|
-
if (token.value === 'k') { state.configModal.moveUp(); state.requestRender(); return true; }
|
|
667
666
|
}
|
|
668
667
|
|
|
669
668
|
const actionKey = token.type === 'key' ? (token.logicalName ?? '') : token.type === 'text' ? token.value : '';
|