@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
|
@@ -1,355 +1,115 @@
|
|
|
1
|
-
import { type Line } from '@pellux/goodvibes-sdk/platform/types';
|
|
2
|
-
import { fitDisplay, getDisplayWidth, truncateDisplay } from '../utils/terminal-width.ts';
|
|
3
|
-
import type { SelectionItem, SelectionModal } from '../input/selection-modal.ts';
|
|
4
|
-
import {
|
|
5
|
-
createOverlayBoxLayout,
|
|
6
|
-
createOverlayContentLine,
|
|
7
|
-
createOverlayFilledBorderLine,
|
|
8
|
-
DEFAULT_OVERLAY_PALETTE,
|
|
9
|
-
OVERLAY_GLYPHS,
|
|
10
|
-
putOverlayText,
|
|
11
|
-
} from './overlay-box.ts';
|
|
12
|
-
import { getOverlaySurfaceMetrics } from './overlay-viewport.ts';
|
|
13
|
-
import { fitLabelDetailColumns, wrapWithHangingIndent } from './text-layout.ts';
|
|
14
|
-
|
|
15
|
-
const BORDER_FG = DEFAULT_OVERLAY_PALETTE.borderFg;
|
|
16
|
-
const TITLE_FG = DEFAULT_OVERLAY_PALETTE.titleFg;
|
|
17
|
-
const BODY_FG = DEFAULT_OVERLAY_PALETTE.bodyFg;
|
|
18
|
-
const MUTED_FG = DEFAULT_OVERLAY_PALETTE.mutedFg;
|
|
19
|
-
const CATEGORY_FG = '#4488cc';
|
|
20
|
-
const SELECTED_BG = DEFAULT_OVERLAY_PALETTE.selectedBg;
|
|
21
|
-
|
|
22
|
-
interface CellStyle {
|
|
23
|
-
fg: string;
|
|
24
|
-
bg?: string;
|
|
25
|
-
bold?: boolean;
|
|
26
|
-
dim?: boolean;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function putText(line: Line, startX: number, maxWidth: number, text: string, style: CellStyle): void {
|
|
30
|
-
putOverlayText(line, startX, maxWidth, text, style);
|
|
31
|
-
}
|
|
32
|
-
|
|
33
1
|
/**
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
2
|
+
* renderSelectionModalOverlay, the generic picker (used by many commands and
|
|
3
|
+
* by the startup recovery questions), drawn with the modal surface kit.
|
|
4
|
+
*
|
|
5
|
+
* Title row, the always-live search row (when the picker searches), items as
|
|
6
|
+
* kit rows grouped under ✦ category headers, the selected item as the
|
|
7
|
+
* gradient row, details wrapped in full after the label, a muted scroll count
|
|
8
|
+
* and keycap hints. The modal sizes to its content: a short question is a
|
|
9
|
+
* small centered dialog that shows every answer; a long list takes the
|
|
10
|
+
* standard height and scrolls around the selected row.
|
|
40
11
|
*/
|
|
41
|
-
interface DetailLayout {
|
|
42
|
-
readonly labelWidth: number;
|
|
43
|
-
readonly detailWidth: number;
|
|
44
|
-
readonly wrappedDetail: readonly string[] | null;
|
|
45
|
-
}
|
|
46
12
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
13
|
+
import type { SelectionItem, SelectionModal } from '../input/selection-modal.ts';
|
|
14
|
+
import { activeTokens } from './theme.ts';
|
|
15
|
+
import {
|
|
16
|
+
beginModal,
|
|
17
|
+
drawWrapped,
|
|
18
|
+
finishModal,
|
|
19
|
+
maxModalHeight,
|
|
20
|
+
searchRow,
|
|
21
|
+
scrollCountText,
|
|
22
|
+
type KitHint,
|
|
23
|
+
type SurfaceLayer,
|
|
24
|
+
} from './surface-kit.ts';
|
|
25
|
+
import { drawList, type KitRow } from './surface-kit-list.ts';
|
|
26
|
+
import { kitHintsFromStrings, listHeight, modalHeightFor, modalTextWidth } from './surface-kit-extra.ts';
|
|
27
|
+
|
|
28
|
+
function primaryVerb(modal: SelectionModal, item: SelectionItem | null): string {
|
|
29
|
+
if (modal.primaryVerbLabel) return modal.primaryVerbLabel.toLowerCase();
|
|
30
|
+
switch (item?.primaryAction) {
|
|
31
|
+
case 'toggle': return 'toggle';
|
|
32
|
+
case 'edit': return 'edit';
|
|
33
|
+
case 'delete': return 'delete';
|
|
34
|
+
default: return 'select';
|
|
55
35
|
}
|
|
56
|
-
// Nothing shares this row with the label once the detail moves below it,
|
|
57
|
-
// so the label itself gets the full row width too.
|
|
58
|
-
return {
|
|
59
|
-
labelWidth: remaining,
|
|
60
|
-
detailWidth: cols.detailWidth,
|
|
61
|
-
wrappedDetail: wrapWithHangingIndent(item.detail, Math.max(8, remaining), ''),
|
|
62
|
-
};
|
|
63
36
|
}
|
|
64
37
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
if (!item.
|
|
69
|
-
|
|
38
|
+
function hintsFor(modal: SelectionModal): KitHint[] {
|
|
39
|
+
const item = modal.getSelected();
|
|
40
|
+
const base: KitHint[] = [['↑↓', 'move'], ['⏎', primaryVerb(modal, item)]];
|
|
41
|
+
if (item?.primaryAction === 'toggle' && !item.actions) base.push(['space', 'toggle']);
|
|
42
|
+
if (item?.adjustable) base.push(['←→', 'adjust']);
|
|
43
|
+
// Item action strings are legacy "[d] delete" / "d delete" text; each one
|
|
44
|
+
// becomes a keycap. Several may share one string separated by spaces.
|
|
45
|
+
const actionStrings = item?.actions ? splitActionString(item.actions) : [];
|
|
46
|
+
return kitHintsFromStrings(actionStrings, base).hints;
|
|
70
47
|
}
|
|
71
48
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
/**
|
|
79
|
-
* Rows the box spends on its own frame, counted as they are actually pushed
|
|
80
|
-
* below: top and bottom border, the title, the section row (a search label,
|
|
81
|
-
* search input and divider when the modal searches), the "Results" header and
|
|
82
|
-
* the footer hint. `chromeRows` is the budget-math constant the surface
|
|
83
|
-
* metrics subtract from a total; this is the physical count, and it is what
|
|
84
|
-
* decides how tall the box may really grow.
|
|
85
|
-
*/
|
|
86
|
-
function frameRowsFor(modal: SelectionModal): number {
|
|
87
|
-
return modal.allowSearch ? 8 : 6;
|
|
49
|
+
/** "[d] delete [r] rename" → ["[d] delete", "[r] rename"]. */
|
|
50
|
+
function splitActionString(text: string): string[] {
|
|
51
|
+
const bracketed = text.match(/\[[^\]]+\][^[]*/g);
|
|
52
|
+
if (bracketed && bracketed.length > 0) return bracketed.map((part) => part.trim());
|
|
53
|
+
return text.split(/\s{2,}|\s·\s/).map((part) => part.trim()).filter(Boolean);
|
|
88
54
|
}
|
|
89
55
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
* height the box would have to give the list for nothing to be scrolled away.
|
|
93
|
-
*/
|
|
94
|
-
function totalContentRowsFor(modal: SelectionModal, remaining: number): number {
|
|
95
|
-
let rows = 0;
|
|
56
|
+
function rowsFor(items: readonly SelectionItem[], selectedIndex: number): KitRow[] {
|
|
57
|
+
const rows: KitRow[] = [];
|
|
96
58
|
let lastCategory: string | undefined;
|
|
97
|
-
|
|
59
|
+
items.forEach((item, index) => {
|
|
98
60
|
if (item.category && item.category !== lastCategory) {
|
|
99
61
|
lastCategory = item.category;
|
|
100
|
-
rows
|
|
62
|
+
rows.push({ header: item.category });
|
|
101
63
|
}
|
|
102
|
-
rows
|
|
103
|
-
|
|
64
|
+
rows.push({
|
|
65
|
+
label: item.label,
|
|
66
|
+
desc: item.detail,
|
|
67
|
+
selected: index === selectedIndex,
|
|
68
|
+
labelFg: item.fg,
|
|
69
|
+
});
|
|
70
|
+
});
|
|
104
71
|
return rows;
|
|
105
72
|
}
|
|
106
73
|
|
|
107
74
|
/**
|
|
108
|
-
* Render the selection modal as
|
|
75
|
+
* Render the selection modal as a SurfaceLayer in screen coordinates.
|
|
109
76
|
*/
|
|
110
77
|
export function renderSelectionModalOverlay(
|
|
111
78
|
modal: SelectionModal,
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
):
|
|
115
|
-
const
|
|
116
|
-
const
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
//
|
|
120
|
-
//
|
|
121
|
-
const
|
|
122
|
-
const
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
//
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
// and scrolls as before.
|
|
131
|
-
const neededContentRows = totalContentRowsFor(modal, remaining);
|
|
132
|
-
const fittableContentRows = viewportHeight - VIEWPORT_INSET_ROWS - frameRowsFor(modal);
|
|
133
|
-
const growToFitTotalRows = neededContentRows > 0 && neededContentRows <= fittableContentRows
|
|
134
|
-
? neededContentRows + chromeRows
|
|
135
|
-
: undefined;
|
|
136
|
-
|
|
137
|
-
const metrics = getOverlaySurfaceMetrics(width, viewportHeight, {
|
|
138
|
-
margin: MODAL_MARGIN,
|
|
139
|
-
maxWidth: MODAL_MAX_WIDTH,
|
|
140
|
-
chromeRows,
|
|
141
|
-
minContentRows: 6,
|
|
142
|
-
// Modals size to content: items whose detail wraps onto extra lines can
|
|
143
|
-
// need more physical rows than a plain one-row-per-item modal, so this
|
|
144
|
-
// ceiling grows with the viewport rather than staying pinned at a small
|
|
145
|
-
// constant (still bounded, never larger than the terminal can show).
|
|
146
|
-
maxContentRows: Math.max(10, viewportHeight - chromeRows - 4),
|
|
147
|
-
// A floor only. The shared budget keeps its own target and ceiling, so a
|
|
148
|
-
// modal whose content is shorter than that target is unaffected: this can
|
|
149
|
-
// raise the budget to fit the list, never lower it.
|
|
150
|
-
minTotalRows: growToFitTotalRows,
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
lines.push(createOverlayFilledBorderLine(width, layout, OVERLAY_GLYPHS.topLeft, OVERLAY_GLYPHS.horizontal, OVERLAY_GLYPHS.topRight, BORDER_FG, DEFAULT_OVERLAY_PALETTE.titleBg));
|
|
154
|
-
|
|
155
|
-
const titleLine = createOverlayContentLine(width, layout, BORDER_FG, DEFAULT_OVERLAY_PALETTE.titleBg);
|
|
156
|
-
putText(
|
|
157
|
-
titleLine,
|
|
158
|
-
layout.margin + 2,
|
|
159
|
-
layout.innerWidth,
|
|
160
|
-
fitDisplay(truncateDisplay(modal.title, layout.innerWidth), layout.innerWidth),
|
|
161
|
-
{ fg: TITLE_FG, bold: true },
|
|
162
|
-
);
|
|
163
|
-
lines.push(titleLine);
|
|
164
|
-
|
|
79
|
+
screenWidth: number,
|
|
80
|
+
screenHeight = 24,
|
|
81
|
+
): SurfaceLayer {
|
|
82
|
+
const t = activeTokens();
|
|
83
|
+
const hints = hintsFor(modal);
|
|
84
|
+
const rows = rowsFor(modal.filteredItems, modal.selectedIndex);
|
|
85
|
+
const textWidth = modalTextWidth(screenWidth, screenHeight);
|
|
86
|
+
// Size to every item (not the filtered set) so typing never resizes the
|
|
87
|
+
// modal; measure against the same text span drawList uses.
|
|
88
|
+
const allRows = rowsFor(modal.items, -1);
|
|
89
|
+
const needed = allRows.length === 0 ? 1 : listHeight(allRows, 0, textWidth - 1);
|
|
90
|
+
const searchRows = modal.allowSearch ? 2 : 0;
|
|
91
|
+
const height = modalHeightFor(screenWidth, screenHeight, { hints }, searchRows + needed);
|
|
92
|
+
// A short picker (it fits well inside the screen) is a small centered dialog.
|
|
93
|
+
const center = height <= Math.round(maxModalHeight(screenHeight) * 0.6);
|
|
94
|
+
const f = beginModal(screenWidth, screenHeight, { title: modal.title, hints, height, center });
|
|
95
|
+
|
|
96
|
+
let top = f.top;
|
|
165
97
|
if (modal.allowSearch) {
|
|
166
|
-
const
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
const searchLine = createOverlayContentLine(width, layout, BORDER_FG, DEFAULT_OVERLAY_PALETTE.inputBg);
|
|
173
|
-
const prefix = '/ ';
|
|
174
|
-
const queryAreaWidth = layout.innerWidth - getDisplayWidth(prefix);
|
|
175
|
-
const queryValue = modal.query + (modal.searchFocused ? OVERLAY_GLYPHS.cursor : '');
|
|
176
|
-
const queryText = fitDisplay(
|
|
177
|
-
truncateDisplay(queryValue, queryAreaWidth),
|
|
178
|
-
queryAreaWidth,
|
|
179
|
-
);
|
|
180
|
-
putText(searchLine, layout.margin + 2, getDisplayWidth(prefix), prefix, { fg: modal.searchFocused ? BODY_FG : MUTED_FG });
|
|
181
|
-
putText(searchLine, layout.margin + 2 + getDisplayWidth(prefix), queryAreaWidth, queryText, {
|
|
182
|
-
fg: modal.query.length > 0 || modal.searchFocused ? BODY_FG : MUTED_FG,
|
|
183
|
-
});
|
|
184
|
-
lines.push(searchLine);
|
|
185
|
-
lines.push(createOverlayFilledBorderLine(width, layout, OVERLAY_GLYPHS.teeLeft, OVERLAY_GLYPHS.horizontal, OVERLAY_GLYPHS.teeRight, BORDER_FG, DEFAULT_OVERLAY_PALETTE.sectionBg));
|
|
186
|
-
} else {
|
|
187
|
-
lines.push(createOverlayContentLine(width, layout, BORDER_FG, DEFAULT_OVERLAY_PALETTE.sectionBg));
|
|
98
|
+
const total = modal.items.length;
|
|
99
|
+
const count = modal.query.length > 0
|
|
100
|
+
? `${modal.filteredItems.length} of ${total}`
|
|
101
|
+
: `${total} ${total === 1 ? 'item' : 'items'}`;
|
|
102
|
+
searchRow(f, top, modal.query, 'Type to filter', count);
|
|
103
|
+
top += 2;
|
|
188
104
|
}
|
|
189
105
|
|
|
190
|
-
|
|
191
|
-
putText(listTitle, layout.margin + 2, layout.innerWidth, fitDisplay(' Results', layout.innerWidth), {
|
|
192
|
-
fg: CATEGORY_FG,
|
|
193
|
-
dim: true,
|
|
194
|
-
});
|
|
195
|
-
lines.push(listTitle);
|
|
196
|
-
|
|
197
|
-
const items = modal.filteredItems;
|
|
198
|
-
if (items.length === 0) {
|
|
199
|
-
const line = createOverlayContentLine(width, layout, BORDER_FG, DEFAULT_OVERLAY_PALETTE.bodyBg);
|
|
106
|
+
if (rows.length === 0) {
|
|
200
107
|
const message = modal.query ? 'No matching items' : 'No items';
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
} else {
|
|
204
|
-
const rowBudget = Math.max(1, metrics.contentRows);
|
|
205
|
-
|
|
206
|
-
// Row-budget-aware windowing: rather than assuming one physical row per
|
|
207
|
-
// item, account for items whose detail wraps onto extra lines, so the
|
|
208
|
-
// visible window never silently shows fewer full items than the budget
|
|
209
|
-
// allows just because a fixed item-count cap ran out. Always shows the
|
|
210
|
-
// selected item (even alone, if its wrapped detail is taller than the
|
|
211
|
-
// whole budget, descriptive text is never clipped to make it fit),
|
|
212
|
-
// then grows outward, roughly centered, until the budget is spent.
|
|
213
|
-
const selected = Math.max(0, Math.min(modal.selectedIndex, items.length - 1));
|
|
214
|
-
let startIdx = selected;
|
|
215
|
-
let endIdx = selected + 1;
|
|
216
|
-
let usedRows = rowCostFor(items[selected], remaining);
|
|
217
|
-
let growBefore = true;
|
|
218
|
-
while (startIdx > 0 || endIdx < items.length) {
|
|
219
|
-
const canGrowBefore = growBefore && startIdx > 0;
|
|
220
|
-
const canGrowAfter = !growBefore && endIdx < items.length;
|
|
221
|
-
if (canGrowBefore) {
|
|
222
|
-
const cost = rowCostFor(items[startIdx - 1], remaining);
|
|
223
|
-
if (usedRows + cost > rowBudget) break;
|
|
224
|
-
startIdx -= 1;
|
|
225
|
-
usedRows += cost;
|
|
226
|
-
} else if (canGrowAfter) {
|
|
227
|
-
const cost = rowCostFor(items[endIdx], remaining);
|
|
228
|
-
if (usedRows + cost > rowBudget) break;
|
|
229
|
-
endIdx += 1;
|
|
230
|
-
usedRows += cost;
|
|
231
|
-
} else if (startIdx > 0) {
|
|
232
|
-
const cost = rowCostFor(items[startIdx - 1], remaining);
|
|
233
|
-
if (usedRows + cost > rowBudget) break;
|
|
234
|
-
startIdx -= 1;
|
|
235
|
-
usedRows += cost;
|
|
236
|
-
} else if (endIdx < items.length) {
|
|
237
|
-
const cost = rowCostFor(items[endIdx], remaining);
|
|
238
|
-
if (usedRows + cost > rowBudget) break;
|
|
239
|
-
endIdx += 1;
|
|
240
|
-
usedRows += cost;
|
|
241
|
-
} else {
|
|
242
|
-
break;
|
|
243
|
-
}
|
|
244
|
-
growBefore = !growBefore;
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
let lastCategory: string | undefined;
|
|
248
|
-
|
|
249
|
-
for (let i = startIdx; i < endIdx; i++) {
|
|
250
|
-
const item = items[i];
|
|
251
|
-
const isSelected = i === modal.selectedIndex;
|
|
252
|
-
|
|
253
|
-
if (item.category && item.category !== lastCategory) {
|
|
254
|
-
lastCategory = item.category;
|
|
255
|
-
const categoryLine = createOverlayContentLine(width, layout, BORDER_FG, DEFAULT_OVERLAY_PALETTE.sectionBg);
|
|
256
|
-
putText(categoryLine, layout.margin + 2, layout.innerWidth, fitDisplay(` ${item.category}`, layout.innerWidth), {
|
|
257
|
-
fg: CATEGORY_FG,
|
|
258
|
-
dim: true,
|
|
259
|
-
});
|
|
260
|
-
lines.push(categoryLine);
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
const indicator = isSelected ? `${OVERLAY_GLYPHS.selected} ` : ' ';
|
|
264
|
-
const labelColor = isSelected ? TITLE_FG : (item.fg ?? BODY_FG);
|
|
265
|
-
const detailColor = isSelected ? BODY_FG : MUTED_FG;
|
|
266
|
-
const detailLayout = computeDetailLayout(item, remaining);
|
|
267
|
-
const labelWidth = detailLayout.labelWidth;
|
|
268
|
-
const labelLine = createOverlayContentLine(width, layout, BORDER_FG, isSelected ? SELECTED_BG : DEFAULT_OVERLAY_PALETTE.bodyBg);
|
|
269
|
-
putText(labelLine, layout.margin + 2, INDICATOR_WIDTH, indicator, {
|
|
270
|
-
fg: isSelected ? TITLE_FG : MUTED_FG,
|
|
271
|
-
bg: isSelected ? SELECTED_BG : DEFAULT_OVERLAY_PALETTE.bodyBg,
|
|
272
|
-
bold: isSelected,
|
|
273
|
-
});
|
|
274
|
-
putText(labelLine, layout.margin + 2 + INDICATOR_WIDTH, labelWidth, fitDisplay(truncateDisplay(item.label, labelWidth), labelWidth), {
|
|
275
|
-
fg: labelColor,
|
|
276
|
-
bg: isSelected ? SELECTED_BG : DEFAULT_OVERLAY_PALETTE.bodyBg,
|
|
277
|
-
bold: isSelected,
|
|
278
|
-
});
|
|
279
|
-
if (item.detail) {
|
|
280
|
-
if (detailLayout.wrappedDetail === null) {
|
|
281
|
-
// Fits beside the label at a readable width, no truncation needed.
|
|
282
|
-
putText(labelLine, layout.margin + 2 + INDICATOR_WIDTH + labelWidth, 2, ' ', {
|
|
283
|
-
fg: BODY_FG,
|
|
284
|
-
bg: isSelected ? SELECTED_BG : DEFAULT_OVERLAY_PALETTE.bodyBg,
|
|
285
|
-
});
|
|
286
|
-
putText(labelLine, layout.margin + 2 + INDICATOR_WIDTH + labelWidth + 2, detailLayout.detailWidth, fitDisplay(item.detail, detailLayout.detailWidth), {
|
|
287
|
-
fg: detailColor,
|
|
288
|
-
bg: isSelected ? SELECTED_BG : DEFAULT_OVERLAY_PALETTE.bodyBg,
|
|
289
|
-
});
|
|
290
|
-
lines.push(labelLine);
|
|
291
|
-
} else {
|
|
292
|
-
// Doesn't fit beside the label, wrap the FULL detail text onto as
|
|
293
|
-
// many lines as it needs below the label, rather than clipping it.
|
|
294
|
-
lines.push(labelLine);
|
|
295
|
-
for (const detailLineText of detailLayout.wrappedDetail) {
|
|
296
|
-
const detailLine = createOverlayContentLine(width, layout, BORDER_FG, isSelected ? SELECTED_BG : DEFAULT_OVERLAY_PALETTE.bodyBg);
|
|
297
|
-
putText(detailLine, layout.margin + 2 + INDICATOR_WIDTH, remaining, fitDisplay(detailLineText, remaining), {
|
|
298
|
-
fg: detailColor,
|
|
299
|
-
bg: isSelected ? SELECTED_BG : DEFAULT_OVERLAY_PALETTE.bodyBg,
|
|
300
|
-
dim: !isSelected,
|
|
301
|
-
});
|
|
302
|
-
lines.push(detailLine);
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
} else {
|
|
306
|
-
lines.push(labelLine);
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
if (startIdx > 0 || endIdx < items.length) {
|
|
311
|
-
const above = startIdx;
|
|
312
|
-
const below = items.length - endIdx;
|
|
313
|
-
const scrollHint = above > 0 && below > 0
|
|
314
|
-
? `(${above} above, ${below} below)`
|
|
315
|
-
: below > 0
|
|
316
|
-
? `(${below} below)`
|
|
317
|
-
: `(${above} above)`;
|
|
318
|
-
const hintLine = createOverlayContentLine(width, layout, BORDER_FG, DEFAULT_OVERLAY_PALETTE.sectionBg);
|
|
319
|
-
putText(hintLine, layout.margin + 2, layout.innerWidth, fitDisplay(scrollHint, layout.innerWidth), { fg: MUTED_FG, dim: true });
|
|
320
|
-
lines.push(hintLine);
|
|
321
|
-
}
|
|
108
|
+
drawWrapped(f.canvas, f.l, top, f.r - f.l + 1, message, { fg: t.textMuted }, f.bottom);
|
|
109
|
+
return finishModal(f);
|
|
322
110
|
}
|
|
323
111
|
|
|
324
|
-
const
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
// "Run", matching the slash-command palette) wins over the generic
|
|
328
|
-
// per-primaryAction default.
|
|
329
|
-
const primaryVerb = modal.primaryVerbLabel
|
|
330
|
-
? `[Enter] ${modal.primaryVerbLabel}`
|
|
331
|
-
: selectedItem?.primaryAction === 'toggle'
|
|
332
|
-
? '[Enter] Toggle'
|
|
333
|
-
: selectedItem?.primaryAction === 'edit'
|
|
334
|
-
? '[Enter] Edit'
|
|
335
|
-
: selectedItem?.primaryAction === 'delete'
|
|
336
|
-
? '[Enter] Delete'
|
|
337
|
-
: '[Enter] Select';
|
|
338
|
-
let hints = `[Up/Down] Navigate ${primaryVerb}`;
|
|
339
|
-
if (modal.allowSearch) hints += ' [/] Search';
|
|
340
|
-
if (selectedItem?.primaryAction === 'toggle' && !selectedItem.actions) hints += ' [Space] Toggle';
|
|
341
|
-
if (selectedItem?.actions) hints += ` ${selectedItem.actions}`;
|
|
342
|
-
// hint-grammar: Esc is the conventional "way out" and always sorts last.
|
|
343
|
-
hints += ' [Esc] Close';
|
|
344
|
-
putText(
|
|
345
|
-
footerLine,
|
|
346
|
-
layout.margin + 2,
|
|
347
|
-
layout.innerWidth,
|
|
348
|
-
fitDisplay(truncateDisplay(hints, layout.innerWidth), layout.innerWidth),
|
|
349
|
-
{ fg: MUTED_FG, dim: true },
|
|
350
|
-
);
|
|
351
|
-
lines.push(footerLine);
|
|
352
|
-
lines.push(createOverlayFilledBorderLine(width, layout, OVERLAY_GLYPHS.bottomLeft, OVERLAY_GLYPHS.horizontal, OVERLAY_GLYPHS.bottomRight, BORDER_FG, DEFAULT_OVERLAY_PALETTE.sectionBg));
|
|
353
|
-
|
|
354
|
-
return lines;
|
|
112
|
+
const res = drawList(f.canvas, { rows, top, bottom: f.bottom, x0: f.l, x1: f.r });
|
|
113
|
+
f.hintRight = scrollCountText(res.above, res.below);
|
|
114
|
+
return finishModal(f);
|
|
355
115
|
}
|
|
@@ -318,11 +318,11 @@ const CHANGE_GLYPH: Record<ChangeKind, string> = {
|
|
|
318
318
|
|
|
319
319
|
/**
|
|
320
320
|
* Render a SemanticDiff as an array of compact summary strings, one change
|
|
321
|
-
* per line. Suitable for display in the
|
|
321
|
+
* per line. Suitable for display in the Changes modal's status area or a tooltip.
|
|
322
322
|
*
|
|
323
323
|
* Format examples:
|
|
324
324
|
* + fn renderDiffView
|
|
325
|
-
* ~ method
|
|
325
|
+
* ~ method DiffModal.showDiff (signature changed)
|
|
326
326
|
* - class OldWidget
|
|
327
327
|
* + import ./utils { A, B }
|
|
328
328
|
* ~ import ./types (+NewType, -OldType)
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* session-chips.ts, the one row under the header that lists every session
|
|
3
|
+
* there is to switch to: main, each running agent, each background process.
|
|
4
|
+
*
|
|
5
|
+
* col 3 the first chip; chips sit 1 column apart
|
|
6
|
+
* chip 2 columns of padding, the status mark, 1 space, the name,
|
|
7
|
+
* 2 columns of padding
|
|
8
|
+
* current filled in the session's own color, dark bold text
|
|
9
|
+
* others on the surface fill, muted name, the mark in its status color
|
|
10
|
+
* right `tab next shift+tab prev` keycaps ending at width-4
|
|
11
|
+
*
|
|
12
|
+
* Shown only when there is more than one session (the caller decides).
|
|
13
|
+
* Chips that do not fit are left out whole and counted in a faint "+N".
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { type Line, createEmptyLine } from '@pellux/goodvibes-sdk/platform/types';
|
|
17
|
+
import { getDisplayWidth, truncateDisplay } from '../utils/terminal-width.ts';
|
|
18
|
+
import { activeTokens } from './theme.ts';
|
|
19
|
+
import { keycapHintsWidth, paintKeycapHints } from './surface-kit-parts.ts';
|
|
20
|
+
import type { KitHint } from './surface-kit.ts';
|
|
21
|
+
|
|
22
|
+
export interface SessionChip {
|
|
23
|
+
/** The status mark: ◐ running, ● idle, ✓ finished, ✕ failed, ○ cancelled, ▶ a running process. */
|
|
24
|
+
readonly mark: string;
|
|
25
|
+
/** The mark's color on an unselected chip. */
|
|
26
|
+
readonly markFg: string;
|
|
27
|
+
readonly name: string;
|
|
28
|
+
/** The session's own color (main: brand; an agent: its lane color; a process: success). */
|
|
29
|
+
readonly color: string;
|
|
30
|
+
readonly current: boolean;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const LEFT_X = 3;
|
|
34
|
+
const CHIP_PAD = 2;
|
|
35
|
+
const CHIP_GAP = 1;
|
|
36
|
+
/** Longest name a chip shows before it is cut with an ellipsis. */
|
|
37
|
+
const NAME_MAX = 24;
|
|
38
|
+
const HINTS: readonly KitHint[] = [['tab', 'next'], ['shift+tab', 'prev']];
|
|
39
|
+
|
|
40
|
+
function put(line: Line, x: number, endX: number, text: string, style: { fg: string; bg: string; bold?: boolean }): number {
|
|
41
|
+
let cx = x;
|
|
42
|
+
for (const ch of text) {
|
|
43
|
+
const w = getDisplayWidth(ch);
|
|
44
|
+
if (w <= 0) continue;
|
|
45
|
+
if (cx + w > endX || cx >= line.length) break;
|
|
46
|
+
line[cx] = { char: ch, fg: style.fg, bg: style.bg, bold: style.bold ?? false, dim: false, underline: false, italic: false, strikethrough: false };
|
|
47
|
+
if (w === 2 && cx + 1 < line.length) line[cx + 1] = { ...line[cx]!, char: '' };
|
|
48
|
+
cx += w;
|
|
49
|
+
}
|
|
50
|
+
return cx;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function chipWidth(chip: SessionChip): number {
|
|
54
|
+
return CHIP_PAD * 2 + getDisplayWidth(chip.mark) + 1 + getDisplayWidth(truncateDisplay(chip.name, NAME_MAX));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function renderSessionChips(width: number, chips: readonly SessionChip[]): Line {
|
|
58
|
+
const t = activeTokens();
|
|
59
|
+
const line = createEmptyLine(width);
|
|
60
|
+
for (const cell of line) cell.bg = '';
|
|
61
|
+
const rightEdge = width - 3; // exclusive: right-aligned text ends at width-4
|
|
62
|
+
const hintsW = keycapHintsWidth(HINTS);
|
|
63
|
+
const showHints = LEFT_X + hintsW + 3 + (chips[0] ? chipWidth(chips[0]) : 0) <= rightEdge;
|
|
64
|
+
const chipsEnd = showHints ? rightEdge - hintsW - 3 : rightEdge;
|
|
65
|
+
|
|
66
|
+
// The current chip is always drawn: when the row is short, the ones before it make way first.
|
|
67
|
+
const order = chips.map((chip, i) => ({ chip, i }));
|
|
68
|
+
const currentIdx = chips.findIndex((c) => c.current);
|
|
69
|
+
const shown = new Set<number>();
|
|
70
|
+
let used = 0;
|
|
71
|
+
const moreW = (n: number): number => (n > 0 ? CHIP_GAP + getDisplayWidth(`+${n}`) : 0);
|
|
72
|
+
const tryAdd = (i: number): boolean => {
|
|
73
|
+
const w = chipWidth(chips[i]!) + (shown.size > 0 ? CHIP_GAP : 0);
|
|
74
|
+
const remaining = chips.length - shown.size - 1;
|
|
75
|
+
if (LEFT_X + used + w + moreW(remaining) > chipsEnd) return false;
|
|
76
|
+
shown.add(i);
|
|
77
|
+
used += w;
|
|
78
|
+
return true;
|
|
79
|
+
};
|
|
80
|
+
if (currentIdx >= 0) tryAdd(currentIdx);
|
|
81
|
+
for (const { i } of order) if (i !== currentIdx && !tryAdd(i)) break;
|
|
82
|
+
|
|
83
|
+
let x = LEFT_X;
|
|
84
|
+
let first = true;
|
|
85
|
+
for (const { chip, i } of order) {
|
|
86
|
+
if (!shown.has(i)) continue;
|
|
87
|
+
if (!first) x += CHIP_GAP;
|
|
88
|
+
first = false;
|
|
89
|
+
const name = truncateDisplay(chip.name, NAME_MAX);
|
|
90
|
+
const bg = chip.current ? chip.color : t.backgroundPanel;
|
|
91
|
+
const end = x + chipWidth(chip);
|
|
92
|
+
put(line, x, end, ' '.repeat(end - x), { fg: t.text, bg });
|
|
93
|
+
let cx = x + CHIP_PAD;
|
|
94
|
+
cx = put(line, cx, end, chip.mark, chip.current ? { fg: t.selectedListItemText, bg, bold: true } : { fg: chip.markFg, bg, bold: true });
|
|
95
|
+
put(line, cx + 1, end, name, chip.current ? { fg: t.selectedListItemText, bg, bold: true } : { fg: t.textMuted, bg });
|
|
96
|
+
x = end;
|
|
97
|
+
}
|
|
98
|
+
const hidden = chips.length - shown.size;
|
|
99
|
+
if (hidden > 0) x = put(line, x + CHIP_GAP, chipsEnd, `+${hidden}`, { fg: t.textFaint, bg: '' });
|
|
100
|
+
if (showHints) paintKeycapHints(line, rightEdge - hintsW, rightEdge, HINTS, { fg: t.textFaint });
|
|
101
|
+
return line;
|
|
102
|
+
}
|