@pellux/goodvibes-tui 2.0.21 → 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 +58 -0
- package/README.md +16 -16
- package/docs/README.md +4 -4
- package/docs/foundation-artifacts/operator-contract.json +1 -1
- 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,59 +1,41 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* renderBlockActionsMenu,
|
|
3
|
-
* as a
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* to its content, and every text section wraps rather than clips, so the
|
|
7
|
-
* block summary and action list are always shown in full.
|
|
2
|
+
* renderBlockActionsMenu, the block-actions menu (Enter on an empty composer)
|
|
3
|
+
* as a small centered kit dialog: the target block's summary wrapped in full,
|
|
4
|
+
* then one kit row per action with its key right-aligned, the selected action
|
|
5
|
+
* as the gradient row, and keycap hints. Each action's key fires it directly.
|
|
8
6
|
*/
|
|
9
7
|
|
|
10
|
-
import { type Line } from '@pellux/goodvibes-sdk/platform/types';
|
|
11
|
-
import { ModalFactory, type ModalSection } from './modal-factory.ts';
|
|
12
8
|
import type { BlockActionsMenu } from './block-actions.ts';
|
|
13
9
|
import { describeBlockForReceipt } from '../input/handler-content-actions.ts';
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
10
|
+
import { activeTokens } from './theme.ts';
|
|
11
|
+
import { beginModal, finishModal, type KitHint, type SurfaceLayer } from './surface-kit.ts';
|
|
12
|
+
import { drawList, type KitRow } from './surface-kit-list.ts';
|
|
13
|
+
import { drawTextBlock, listHeight, modalHeightFor, modalTextWidth, textBlockHeight, type TextLine } from './surface-kit-extra.ts';
|
|
14
|
+
|
|
15
|
+
const HINTS: readonly KitHint[] = [['↑↓', 'move'], ['⏎', 'select']];
|
|
16
|
+
/** Preferred width: a menu, not a workspace. */
|
|
17
|
+
const MENU_WIDTH = 72;
|
|
16
18
|
|
|
17
19
|
export function renderBlockActionsMenu(
|
|
18
20
|
menu: BlockActionsMenu,
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
):
|
|
22
|
-
if (!menu.active || !menu.block) return
|
|
23
|
-
|
|
24
|
-
const summary = describeBlockForReceipt(menu.block);
|
|
25
|
-
const metrics = getOverlaySurfaceMetrics(width, viewportHeight, {
|
|
26
|
-
margin: 4,
|
|
27
|
-
maxWidth: 64,
|
|
28
|
-
chromeRows: 5,
|
|
29
|
-
minContentRows: 3,
|
|
30
|
-
maxContentRows: Math.max(6, viewportHeight - 8),
|
|
31
|
-
});
|
|
21
|
+
screenWidth: number,
|
|
22
|
+
screenHeight = 24,
|
|
23
|
+
): SurfaceLayer | null {
|
|
24
|
+
if (!menu.active || !menu.block) return null;
|
|
25
|
+
const t = activeTokens();
|
|
32
26
|
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
},
|
|
43
|
-
];
|
|
27
|
+
const target: TextLine[] = [{ text: `Target: ${describeBlockForReceipt(menu.block)}`, style: { fg: t.textMuted } }];
|
|
28
|
+
const rows: KitRow[] = menu.actions.map((action, i) => ({
|
|
29
|
+
label: action.label,
|
|
30
|
+
right: action.key === 'Tab' ? 'tab' : action.key,
|
|
31
|
+
selected: i === menu.selectedIndex,
|
|
32
|
+
}));
|
|
33
|
+
const width = modalTextWidth(screenWidth, screenHeight, MENU_WIDTH);
|
|
34
|
+
const body = textBlockHeight(target, width) + 1 + listHeight(rows, 0, width - 1);
|
|
35
|
+
const height = modalHeightFor(screenWidth, screenHeight, { width: MENU_WIDTH, hints: HINTS }, body);
|
|
44
36
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
margin: metrics.margin,
|
|
50
|
-
sections,
|
|
51
|
-
hints: [formatHints([
|
|
52
|
-
{ key: 'Up/Down', verb: 'Navigate' },
|
|
53
|
-
{ key: 'Enter', verb: 'Select' },
|
|
54
|
-
{ key: 'Esc', verb: 'Close' },
|
|
55
|
-
])],
|
|
56
|
-
},
|
|
57
|
-
width,
|
|
58
|
-
);
|
|
37
|
+
const f = beginModal(screenWidth, screenHeight, { title: 'Block actions', hints: HINTS, width: MENU_WIDTH, height, center: true });
|
|
38
|
+
const y = drawTextBlock(f.canvas, f.l, f.top, f.r - f.l + 1, target, f.bottom) + 1;
|
|
39
|
+
drawList(f.canvas, { rows, top: y, bottom: f.bottom, x0: f.l, x1: f.r });
|
|
40
|
+
return finishModal(f);
|
|
59
41
|
}
|
|
@@ -1,101 +1,70 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* renderBookmarkModal,
|
|
2
|
+
* renderBookmarkModal, the /bookmarks modal drawn with the modal surface kit.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* Title with the selected position, the always-live search row (filters by
|
|
5
|
+
* key and label), one kit row per bookmark (label, the block key muted after
|
|
6
|
+
* it, the time right-aligned), a muted scroll count and keycap hints:
|
|
7
|
+
* ↑↓ move · ⏎ jump · o open file · d remove.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import { type Line } from '@pellux/goodvibes-sdk/platform/types';
|
|
11
|
-
import { ModalFactory } from './modal-factory.ts';
|
|
12
10
|
import { BookmarkModal } from '../input/bookmark-modal.ts';
|
|
13
11
|
import type { BookmarkEntry } from '@pellux/goodvibes-sdk/platform/bookmarks';
|
|
14
|
-
import {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
12
|
+
import { activeTokens } from './theme.ts';
|
|
13
|
+
import {
|
|
14
|
+
beginModal,
|
|
15
|
+
drawWrapped,
|
|
16
|
+
finishModal,
|
|
17
|
+
searchRow,
|
|
18
|
+
scrollCountText,
|
|
19
|
+
type KitHint,
|
|
20
|
+
type SurfaceLayer,
|
|
21
|
+
} from './surface-kit.ts';
|
|
22
|
+
import { drawList, type KitRow } from './surface-kit-list.ts';
|
|
23
|
+
import { listHeight, modalHeightFor, modalTextWidth } from './surface-kit-extra.ts';
|
|
19
24
|
|
|
20
25
|
function formatTime(ts: number): string {
|
|
21
26
|
const d = new Date(ts);
|
|
22
27
|
return d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' });
|
|
23
28
|
}
|
|
24
29
|
|
|
25
|
-
function
|
|
26
|
-
|
|
27
|
-
// Pad the key to a consistent width (truncated to 28 chars)
|
|
28
|
-
const keyPart = entry.key.length > 28
|
|
29
|
-
? entry.key.slice(0, 25) + '...'
|
|
30
|
-
: entry.key.padEnd(28);
|
|
31
|
-
const labelPart = entry.label.length > 30
|
|
32
|
-
? entry.label.slice(0, 27) + '...'
|
|
33
|
-
: entry.label;
|
|
34
|
-
return `${keyPart} ${labelPart} ${time}`;
|
|
30
|
+
function rowFor(entry: BookmarkEntry, selected: boolean): KitRow {
|
|
31
|
+
return { label: entry.label, desc: entry.key, right: formatTime(entry.timestamp), selected };
|
|
35
32
|
}
|
|
36
33
|
|
|
37
|
-
|
|
38
|
-
// Renderer
|
|
39
|
-
// ---------------------------------------------------------------------------
|
|
34
|
+
const HINTS: readonly KitHint[] = [['↑↓', 'move'], ['⏎', 'jump'], ['o', 'open file'], ['d', 'remove']];
|
|
40
35
|
|
|
41
36
|
/**
|
|
42
|
-
* Render the bookmark modal as
|
|
43
|
-
*
|
|
44
|
-
* @param modal BookmarkModal state object.
|
|
45
|
-
* @param width Terminal width.
|
|
37
|
+
* Render the bookmark modal as a SurfaceLayer in screen coordinates.
|
|
46
38
|
*/
|
|
47
39
|
export function renderBookmarkModal(
|
|
48
40
|
modal: BookmarkModal,
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
):
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
const
|
|
60
|
-
const relSelected = Math.max(0, modal.selectedIndex - modal.scrollOffset);
|
|
41
|
+
screenWidth: number,
|
|
42
|
+
screenHeight = 24,
|
|
43
|
+
): SurfaceLayer {
|
|
44
|
+
const t = activeTokens();
|
|
45
|
+
const visible = modal.visibleEntries;
|
|
46
|
+
const rows = visible.map((entry, i) => rowFor(entry, i === modal.selectedIndex));
|
|
47
|
+
// Size to every bookmark (not the filtered set) so typing never resizes the modal.
|
|
48
|
+
const width = modalTextWidth(screenWidth, screenHeight);
|
|
49
|
+
const allRows = modal.entries.map((entry) => rowFor(entry, false));
|
|
50
|
+
const body = 2 + Math.max(1, listHeight(allRows, 0, width - 1));
|
|
51
|
+
const height = modalHeightFor(screenWidth, screenHeight, { hints: HINTS }, body);
|
|
61
52
|
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
}));
|
|
68
|
-
const sections: import('./modal-factory.ts').ModalSection[] = [
|
|
69
|
-
{
|
|
70
|
-
type: 'text',
|
|
71
|
-
content: ' Key Label Time',
|
|
72
|
-
style: { dim: true },
|
|
73
|
-
},
|
|
74
|
-
{ type: 'separator' },
|
|
75
|
-
{ type: 'list', items },
|
|
76
|
-
];
|
|
77
|
-
if (modal.entries.length > visRows) {
|
|
78
|
-
sections.push({ type: 'separator' });
|
|
79
|
-
sections.push({
|
|
80
|
-
type: 'text',
|
|
81
|
-
content: `[${modal.scrollOffset + 1}-${Math.min(modal.entries.length, modal.scrollOffset + visRows)} of ${modal.entries.length}]`,
|
|
82
|
-
style: { fg: '244', dim: true },
|
|
83
|
-
});
|
|
84
|
-
}
|
|
53
|
+
const position = visible.length > 0 ? `${Math.min(modal.selectedIndex + 1, visible.length)}/${visible.length}` : '';
|
|
54
|
+
const f = beginModal(screenWidth, screenHeight, { title: 'Bookmarks', sub: position, hints: HINTS, height });
|
|
55
|
+
const total = modal.entries.length;
|
|
56
|
+
const count = modal.query ? `${visible.length} of ${total}` : `${total} saved`;
|
|
57
|
+
searchRow(f, f.top, modal.query, 'Filter bookmarks', count);
|
|
85
58
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
hints: ['Up/Down Navigate', 'Enter Jump', 'o Open file', 'd Remove', 'Esc Close'],
|
|
98
|
-
},
|
|
99
|
-
width,
|
|
100
|
-
);
|
|
59
|
+
const top = f.top + 2;
|
|
60
|
+
if (rows.length === 0) {
|
|
61
|
+
const message = total === 0
|
|
62
|
+
? 'No bookmarks yet. Press Ctrl+B on a block to bookmark it.'
|
|
63
|
+
: `No bookmarks match "${modal.query}".`;
|
|
64
|
+
drawWrapped(f.canvas, f.l, top, f.r - f.l + 1, message, { fg: t.textMuted }, f.bottom);
|
|
65
|
+
return finishModal(f);
|
|
66
|
+
}
|
|
67
|
+
const res = drawList(f.canvas, { rows, top, bottom: f.bottom, x0: f.l, x1: f.r });
|
|
68
|
+
f.hintRight = scrollCountText(res.above, res.below);
|
|
69
|
+
return finishModal(f);
|
|
101
70
|
}
|
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* renderChangesModal, files, a tinted diff and review actions (replaces the
|
|
3
|
+
* Git, Diff and Review views).
|
|
4
|
+
*
|
|
5
|
+
* ✦ Changes main · 2 files · +45 −1 · this session esc
|
|
6
|
+
*
|
|
7
|
+
* ✦ files 2 ◈ 2 semantic changes ~ fn withRetry + const backoff
|
|
8
|
+
* src/net/retry.ts +3 −1
|
|
9
|
+
* ✓ test/retry.test.ts +42 ┌ element inset ──────────────────────────────┐
|
|
10
|
+
* ⋯ line 9 hunk 1 of 2
|
|
11
|
+
* ✦ recent commits 9 try {
|
|
12
|
+
* a41c09e init 13 - await sleep(opts.baseDelayMs);
|
|
13
|
+
* 13 + if (i === opts.attempts - 1) break;
|
|
14
|
+
*
|
|
15
|
+
* ] [ next / prev hunk space stage hunk c comment m mark reviewed o open in editor
|
|
16
|
+
*
|
|
17
|
+
* Changed rows keep their syntax colors; the change shows as a background tint
|
|
18
|
+
* across the row and a separate tint on the line-number gutter (the theme's
|
|
19
|
+
* diff tokens, pushed further from the inset fill when a theme's tint sits too
|
|
20
|
+
* close to it; see diff-tint.ts). Long lines wrap under their own gutter. The semantic summary
|
|
21
|
+
* (tree-sitter) leads the diff side. A preview (a fleet candidate, a rewind or an
|
|
22
|
+
* attempt) can carry a question with its two buttons on the first body row.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import { activeDiffTones, activeTokens } from './theme.ts';
|
|
26
|
+
import { beginModal, finishModal, clipText, scrollCountText, wrapLines, type KitHint, type ModalFrame, type SurfaceCanvas, type SurfaceLayer } from './surface-kit.ts';
|
|
27
|
+
import { drawList, type KitRow } from './surface-kit-list.ts';
|
|
28
|
+
import { button, buttonWidth, inset, type KitInset } from './surface-kit-parts.ts';
|
|
29
|
+
import { drawTextBlock, splitListDetail } from './surface-kit-extra.ts';
|
|
30
|
+
import { highlightCodeLines, syntaxHighlightGeneration, syntaxHighlightMisses } from './code-block.ts';
|
|
31
|
+
import type { SemanticDiff } from './semantic-diff.ts';
|
|
32
|
+
import { diffRowTints } from './diff-tint.ts';
|
|
33
|
+
import { getDisplayWidth } from '../utils/terminal-width.ts';
|
|
34
|
+
import { languageForPath, type ChangeFile, type ChangeHunk, type ChangeLine } from '../input/changes-model.ts';
|
|
35
|
+
|
|
36
|
+
/** A question asked over a preview (pick this candidate? restore this checkpoint?). */
|
|
37
|
+
export interface ChangesQuestion {
|
|
38
|
+
readonly text: string;
|
|
39
|
+
readonly confirmLabel: string;
|
|
40
|
+
readonly cancelLabel: string;
|
|
41
|
+
readonly tone: 'danger' | 'warning' | 'primary';
|
|
42
|
+
/** 0 = cancel (left), 1 = confirm (right). */
|
|
43
|
+
readonly chosen: 0 | 1;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** One line of text under the diff (a comment, a status, the comment being typed). */
|
|
47
|
+
export interface ChangesFootLine {
|
|
48
|
+
readonly text: string;
|
|
49
|
+
readonly tone: 'muted' | 'faint' | 'text' | 'error' | 'success';
|
|
50
|
+
/** Draw a cursor after the text (an input being typed). */
|
|
51
|
+
readonly cursor?: boolean;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** What the renderer reads from the modal. */
|
|
55
|
+
export interface ChangesModalView {
|
|
56
|
+
readonly title: string;
|
|
57
|
+
readonly crumbs: readonly string[];
|
|
58
|
+
readonly sub: string;
|
|
59
|
+
/** The left list: files, then recent commits (built by the modal, selection included). */
|
|
60
|
+
readonly listRows: readonly KitRow[];
|
|
61
|
+
/** Files shown in the diff side (one file, or every file of a commit or preview). */
|
|
62
|
+
readonly diffFiles: readonly ChangeFile[];
|
|
63
|
+
/** Draw a path row above each file's hunks (more than one file in the diff side). */
|
|
64
|
+
readonly fileHeaders: boolean;
|
|
65
|
+
/** The selected hunk, counted across diffFiles. */
|
|
66
|
+
readonly hunkIndex: number;
|
|
67
|
+
/** Lines scrolled from the selected hunk's first row (pgup / pgdn). */
|
|
68
|
+
readonly scrollDelta: number;
|
|
69
|
+
/** The semantic summary for the shown file: undefined while loading, null when there is none. */
|
|
70
|
+
readonly semantic: SemanticDiff | null | undefined;
|
|
71
|
+
/** Shown instead of a diff (a preview that is text, an empty state, an error). */
|
|
72
|
+
readonly message: string | null;
|
|
73
|
+
readonly foot: readonly ChangesFootLine[];
|
|
74
|
+
readonly hints: readonly KitHint[];
|
|
75
|
+
readonly question: ChangesQuestion | null;
|
|
76
|
+
/** Owner for remembered list scroll positions. */
|
|
77
|
+
readonly scrollOwner: object;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Width of the line-number gutter inside the diff inset. */
|
|
81
|
+
const DIFF_GUTTER = 5;
|
|
82
|
+
const GUTTER = DIFF_GUTTER;
|
|
83
|
+
|
|
84
|
+
/** One drawn row of a diff: a file header, a hunk separator, or a (wrapped) code line. */
|
|
85
|
+
export type DiffRow =
|
|
86
|
+
| { readonly kind: 'file'; readonly text: string }
|
|
87
|
+
| { readonly kind: 'sep'; readonly hunk: number; readonly text: string }
|
|
88
|
+
| { readonly kind: 'line'; readonly hunk: number; readonly line: ChangeLine; readonly first: boolean; readonly text: string; readonly tokens: ReadonlyArray<{ text: string; fg: string; bold?: boolean; italic?: boolean }> | null };
|
|
89
|
+
|
|
90
|
+
/** Syntax tokens per hunk, remembered until the theme changes. */
|
|
91
|
+
/** A hunk's tokens; `generation` is set when they were the regex placeholder (a parse was on its way). */
|
|
92
|
+
const tokenMemo = new WeakMap<ChangeHunk, { theme: object; lines: ReturnType<typeof highlightCodeLines>; generation: number | undefined }>();
|
|
93
|
+
|
|
94
|
+
function hunkTokens(file: ChangeFile, hunk: ChangeHunk): ReturnType<typeof highlightCodeLines> {
|
|
95
|
+
const theme = activeTokens();
|
|
96
|
+
const memo = tokenMemo.get(hunk);
|
|
97
|
+
// Placeholder tokens are kept only until the hunk's parse lands.
|
|
98
|
+
if (memo && memo.theme === theme && (memo.generation === undefined || memo.generation === syntaxHighlightGeneration())) return memo.lines;
|
|
99
|
+
const missesBefore = syntaxHighlightMisses();
|
|
100
|
+
const lines = highlightCodeLines(hunk.lines.map((line) => line.text), languageForPath(file.path));
|
|
101
|
+
tokenMemo.set(hunk, { theme, lines, generation: syntaxHighlightMisses() !== missesBefore ? syntaxHighlightGeneration() : undefined });
|
|
102
|
+
return lines;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Split syntax tokens into rows of at most `width` cells. */
|
|
106
|
+
function wrapTokens(tokens: ReadonlyArray<{ text: string; fg: string; bold?: boolean; italic?: boolean }>, width: number): Array<Array<{ text: string; fg: string; bold?: boolean; italic?: boolean }>> {
|
|
107
|
+
const rows: Array<Array<{ text: string; fg: string; bold?: boolean; italic?: boolean }>> = [[]];
|
|
108
|
+
let used = 0;
|
|
109
|
+
for (const token of tokens) {
|
|
110
|
+
let piece = '';
|
|
111
|
+
for (const ch of token.text.replace(/\t/g, ' ')) {
|
|
112
|
+
const w = getDisplayWidth(ch);
|
|
113
|
+
if (w <= 0) continue;
|
|
114
|
+
if (used + w > width) {
|
|
115
|
+
if (piece) rows[rows.length - 1]!.push({ ...token, text: piece });
|
|
116
|
+
rows.push([]);
|
|
117
|
+
piece = '';
|
|
118
|
+
used = 0;
|
|
119
|
+
}
|
|
120
|
+
piece += ch;
|
|
121
|
+
used += w;
|
|
122
|
+
}
|
|
123
|
+
if (piece) rows[rows.length - 1]!.push({ ...token, text: piece });
|
|
124
|
+
}
|
|
125
|
+
return rows;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function buildRows(view: ChangesModalView, codeWidth: number): { rows: DiffRow[]; hunkStarts: number[] } {
|
|
129
|
+
return buildDiffRows(view.diffFiles, codeWidth, view.fileHeaders);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* The rows a diff draws as, code wrapped to `codeWidth` under its own gutter.
|
|
134
|
+
* Shared with the conversation work tree, which draws an opened edit with the
|
|
135
|
+
* same rows (lane-graph/paint.ts).
|
|
136
|
+
*/
|
|
137
|
+
export function buildDiffRows(diffFiles: readonly ChangeFile[], codeWidth: number, fileHeaders: boolean): { rows: DiffRow[]; hunkStarts: number[] } {
|
|
138
|
+
const rows: DiffRow[] = [];
|
|
139
|
+
const hunkStarts: number[] = [];
|
|
140
|
+
let hunkNo = 0;
|
|
141
|
+
const total = diffFiles.reduce((n, f) => n + f.hunks.length, 0);
|
|
142
|
+
for (const file of diffFiles) {
|
|
143
|
+
if (fileHeaders) rows.push({ kind: 'file', text: `${file.path} +${file.added} −${file.removed}` });
|
|
144
|
+
if (file.headerOnly) {
|
|
145
|
+
const why = file.note ?? (file.headerLines.some((l) => l.startsWith('Binary')) ? 'binary file, no line changes to show' : 'no line changes (mode or rename only)');
|
|
146
|
+
rows.push({ kind: 'file', text: why });
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
for (const hunk of file.hunks) {
|
|
150
|
+
hunkStarts.push(rows.length);
|
|
151
|
+
rows.push({ kind: 'sep', hunk: hunkNo, text: `⋯ line ${hunk.review.newStart}` + (total > 1 ? ` · hunk ${hunkNo + 1} of ${total}` : '') });
|
|
152
|
+
const tokens = hunkTokens(file, hunk);
|
|
153
|
+
hunk.lines.forEach((line, i) => {
|
|
154
|
+
if (line.kind === 'note') {
|
|
155
|
+
rows.push({ kind: 'line', hunk: hunkNo, line, first: true, text: line.text, tokens: null });
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
const wrapped = wrapTokens(tokens[i] ?? [{ text: line.text, fg: activeTokens().text }], codeWidth);
|
|
159
|
+
wrapped.forEach((part, k) => rows.push({ kind: 'line', hunk: hunkNo, line, first: k === 0, text: '', tokens: part }));
|
|
160
|
+
});
|
|
161
|
+
hunkNo++;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return { rows, hunkStarts };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Draw one diff row inside inset `p`: the change tint across the row, a
|
|
169
|
+
* separate tint on the line-number gutter, the number, the sign and the
|
|
170
|
+
* syntax-colored code. `lineNumbers: false` leaves the numbers out (a diff
|
|
171
|
+
* whose line numbers are not known).
|
|
172
|
+
*/
|
|
173
|
+
export function drawDiffRow(canvas: SurfaceCanvas, p: KitInset, y: number, row: DiffRow, selectedHunk: number, options: { readonly lineNumbers?: boolean } = {}): void {
|
|
174
|
+
const t = activeTokens();
|
|
175
|
+
if (row.kind === 'file') {
|
|
176
|
+
canvas.put(p.l, y, clipText(row.text, p.r - p.l + 1), { fg: t.text, bold: true, bg: p.bg });
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
const tones = activeDiffTones();
|
|
180
|
+
if (row.kind === 'sep') {
|
|
181
|
+
const on = row.hunk === selectedHunk;
|
|
182
|
+
canvas.put(p.l, y, clipText(row.text, p.r - p.l + 1), { fg: on ? t.brand : tones.hunk, bold: on, bg: p.bg });
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
const line = row.line;
|
|
186
|
+
const gutterEnd = p.l + GUTTER - 1;
|
|
187
|
+
if (line.kind === 'add' || line.kind === 'del') {
|
|
188
|
+
const tints = diffRowTints(p.bg);
|
|
189
|
+
canvas.tint(p.x, y, p.w, line.kind === 'add' ? tints.addedRow : tints.removedRow);
|
|
190
|
+
canvas.tint(p.x, y, gutterEnd - p.x + 2, line.kind === 'add' ? tints.addedGutter : tints.removedGutter);
|
|
191
|
+
}
|
|
192
|
+
if (line.kind === 'note') {
|
|
193
|
+
canvas.put(p.l + GUTTER + 3, y, clipText(row.text, p.r - p.l - GUTTER - 2), { fg: t.textFaint });
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
if (row.first) {
|
|
197
|
+
const n = line.kind === 'del' ? line.oldNo : line.newNo;
|
|
198
|
+
if (n !== null && options.lineNumbers !== false) canvas.right(gutterEnd, y, String(n).slice(-GUTTER), { fg: t.diffLineNumber });
|
|
199
|
+
const sign = line.kind === 'add' ? '+' : line.kind === 'del' ? '-' : ' ';
|
|
200
|
+
canvas.put(gutterEnd + 2, y, sign, { fg: line.kind === 'add' ? tones.add : line.kind === 'del' ? tones.del : t.textFaint });
|
|
201
|
+
}
|
|
202
|
+
let x = gutterEnd + 4;
|
|
203
|
+
for (const token of row.tokens ?? []) x = canvas.put(x, y, token.text, { fg: token.fg, bold: token.bold, italic: token.italic });
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** The ◈ summary's chips ("~ fn withRetry", "+ import x"), colored by kind. */
|
|
207
|
+
export function semanticChips(diff: SemanticDiff): Array<{ text: string; fg: string }> {
|
|
208
|
+
const t = activeTokens();
|
|
209
|
+
const chips: Array<{ text: string; fg: string }> = [];
|
|
210
|
+
for (const s of diff.symbols) {
|
|
211
|
+
const glyph = s.kind === 'added' ? '+' : s.kind === 'removed' ? '-' : '~';
|
|
212
|
+
chips.push({ text: `${glyph} ${s.symbolKind === 'function' ? 'fn' : s.symbolKind} ${s.name}`, fg: s.kind === 'added' ? t.success : s.kind === 'removed' ? t.error : t.warning });
|
|
213
|
+
}
|
|
214
|
+
for (const i of diff.imports) {
|
|
215
|
+
const glyph = i.kind === 'added' ? '+' : i.kind === 'removed' ? '-' : '~';
|
|
216
|
+
chips.push({ text: `${glyph} import ${i.specifier}`, fg: i.kind === 'added' ? t.success : i.kind === 'removed' ? t.error : t.warning });
|
|
217
|
+
}
|
|
218
|
+
return chips;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** The ◈ summary row(s); returns the row after them. */
|
|
222
|
+
function drawSemantic(f: ModalFrame, view: ChangesModalView, x: number, xr: number, y: number): number {
|
|
223
|
+
const t = activeTokens();
|
|
224
|
+
const { canvas } = f;
|
|
225
|
+
if (view.semantic === undefined) {
|
|
226
|
+
canvas.put(x, y, clipText('◈ reading the structure of this file…', xr - x + 1), { fg: t.textFaint });
|
|
227
|
+
return y + 2;
|
|
228
|
+
}
|
|
229
|
+
if (view.semantic === null || view.semantic.totalChanges === 0) return y;
|
|
230
|
+
canvas.put(x, y, '◈', { fg: t.brandEnd });
|
|
231
|
+
const head = `${view.semantic.totalChanges} semantic change${view.semantic.totalChanges === 1 ? '' : 's'}`;
|
|
232
|
+
let cx = canvas.put(x + 2, y, clipText(head, xr - x - 1), { fg: t.text, bold: true }) + 2;
|
|
233
|
+
let yy = y;
|
|
234
|
+
const chips = semanticChips(view.semantic);
|
|
235
|
+
chips.forEach((chip, k) => {
|
|
236
|
+
const w = getDisplayWidth(chip.text);
|
|
237
|
+
const rest = chips.length - k;
|
|
238
|
+
if (cx + w > xr) {
|
|
239
|
+
// Wrap chips onto one more row, then fold the rest into a count.
|
|
240
|
+
if (yy > y) return;
|
|
241
|
+
yy++;
|
|
242
|
+
cx = x + 2;
|
|
243
|
+
}
|
|
244
|
+
if (yy > y && cx + w > xr - 8 && rest > 1) {
|
|
245
|
+
canvas.put(cx, yy, `+${rest} more`, { fg: t.textFaint });
|
|
246
|
+
cx = xr + 1;
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
if (cx + w > xr) return;
|
|
250
|
+
cx = canvas.put(cx, yy, chip.text, { fg: chip.fg }) + 2;
|
|
251
|
+
});
|
|
252
|
+
return yy + 2;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function drawQuestion(f: ModalFrame, q: ChangesQuestion, y: number): number {
|
|
256
|
+
const t = activeTokens();
|
|
257
|
+
const width = f.r - f.l + 1;
|
|
258
|
+
const buttonsW = buttonWidth(q.cancelLabel) + 2 + buttonWidth(q.confirmLabel);
|
|
259
|
+
const textW = width >= 70 ? width - buttonsW - 3 : width;
|
|
260
|
+
const lines = wrapLines(q.text, textW);
|
|
261
|
+
lines.forEach((line, k) => f.canvas.put(f.l, y + k, line, { fg: t.text, bold: k === 0 }));
|
|
262
|
+
const by = width >= 70 ? y : y + lines.length;
|
|
263
|
+
let bx = f.r - buttonsW + 1;
|
|
264
|
+
bx = button(f.canvas, bx, by, q.cancelLabel, q.chosen === 0) + 2;
|
|
265
|
+
button(f.canvas, bx, by, q.confirmLabel, q.chosen === 1, q.tone);
|
|
266
|
+
return Math.max(y + lines.length, by + 1) + 1;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function drawDiffColumn(f: ModalFrame, view: ChangesModalView, x: number, xr: number, top: number, bottom: number): void {
|
|
270
|
+
const t = activeTokens();
|
|
271
|
+
const { canvas } = f;
|
|
272
|
+
let y = top;
|
|
273
|
+
if (view.message !== null) {
|
|
274
|
+
drawTextBlock(canvas, x, y, xr - x + 1, view.message.split('\n').map((text) => ({ text, style: { fg: t.textMuted } })), bottom);
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
y = drawSemantic(f, view, x, xr, y);
|
|
278
|
+
// Foot lines under the diff (comment, status), each wrapped in full.
|
|
279
|
+
const footRows = view.foot.reduce((n, line) => n + wrapLines(line.text, xr - x + 1).length, 0);
|
|
280
|
+
const insetBottom = bottom - (footRows > 0 ? footRows + 1 : 0);
|
|
281
|
+
if (insetBottom - y + 1 >= 3) {
|
|
282
|
+
const p = inset(canvas, x - 2, y, xr - x + 5, insetBottom - y + 1);
|
|
283
|
+
const codeWidth = Math.max(8, p.r - (p.l + GUTTER + 3) + 1);
|
|
284
|
+
const { rows, hunkStarts } = buildRows(view, codeWidth);
|
|
285
|
+
const capacity = p.bottom - p.top + 1;
|
|
286
|
+
const anchor = hunkStarts[Math.max(0, Math.min(view.hunkIndex, hunkStarts.length - 1))] ?? 0;
|
|
287
|
+
const start = Math.max(0, Math.min(anchor + view.scrollDelta, Math.max(0, rows.length - capacity)));
|
|
288
|
+
for (let k = 0; k < capacity && start + k < rows.length; k++) drawDiffRow(canvas, p, p.top + k, rows[start + k]!, view.hunkIndex);
|
|
289
|
+
const hidden = scrollCountText(start, Math.max(0, rows.length - start - capacity)).replace(/more/g, 'lines');
|
|
290
|
+
if (hidden) f.hintRight = hidden;
|
|
291
|
+
}
|
|
292
|
+
let fy = insetBottom + 2;
|
|
293
|
+
for (const line of view.foot) {
|
|
294
|
+
const fg = line.tone === 'error' ? t.error : line.tone === 'success' ? t.success : line.tone === 'faint' ? t.textFaint : line.tone === 'text' ? t.text : t.textMuted;
|
|
295
|
+
const wrapped = wrapLines(line.text, xr - x + 1);
|
|
296
|
+
wrapped.forEach((part, k) => {
|
|
297
|
+
if (fy > bottom) return;
|
|
298
|
+
const end = canvas.put(x, fy, part, { fg });
|
|
299
|
+
if (line.cursor && k === wrapped.length - 1) canvas.put(end, fy, '▏', { fg: t.brand });
|
|
300
|
+
fy++;
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
export function renderChangesModal(view: ChangesModalView, screenWidth: number, screenHeight: number): SurfaceLayer {
|
|
306
|
+
const f = beginModal(screenWidth, screenHeight, {
|
|
307
|
+
title: view.title,
|
|
308
|
+
crumbs: view.crumbs,
|
|
309
|
+
sub: view.sub,
|
|
310
|
+
hints: view.hints,
|
|
311
|
+
cap: view.question ? (view.question.tone === 'danger' ? 'danger' : 'warning') : 'brand',
|
|
312
|
+
});
|
|
313
|
+
let top = f.top;
|
|
314
|
+
if (view.question) top = drawQuestion(f, view.question, top);
|
|
315
|
+
if (view.listRows.length === 0) {
|
|
316
|
+
drawDiffColumn(f, view, f.l, f.r, top, f.bottom);
|
|
317
|
+
return finishModal(f);
|
|
318
|
+
}
|
|
319
|
+
const split = splitListDetail(f.l, f.r, top, f.bottom, 0.3, 4);
|
|
320
|
+
drawList(f.canvas, { rows: view.listRows, top: split.top, bottom: split.bottom, x0: split.x0, x1: split.x1, scrollKey: { owner: view.scrollOwner, name: 'files' } });
|
|
321
|
+
const paneX = split.stacked ? f.l : split.detailX + 2;
|
|
322
|
+
const paneTop = split.stacked ? split.detailY : split.top;
|
|
323
|
+
drawDiffColumn(f, view, paneX, f.r, paneTop, f.bottom);
|
|
324
|
+
return finishModal(f);
|
|
325
|
+
}
|