@pellux/goodvibes-tui 2.0.21 → 2.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +65 -1
- package/README.md +30 -29
- package/docs/README.md +8 -5
- package/docs/foundation-artifacts/operator-contract.json +1 -1
- package/package.json +28 -19
- package/src/cli/management.ts +5 -2
- package/src/cli/tui-startup.ts +34 -39
- package/src/core/agent-view-render.ts +157 -0
- package/src/core/approval-alert.ts +49 -11
- package/src/core/bookmark-navigation.ts +7 -1
- package/src/core/budget-breach-notifier.ts +27 -8
- package/src/core/compaction-receipt.ts +1 -1
- package/src/core/context-usage.ts +1 -1
- package/src/core/conversation-compaction-render.ts +3 -3
- package/src/core/conversation-display-only.ts +86 -0
- package/src/core/conversation-event-navigation.ts +27 -0
- package/src/core/conversation-line-cache.ts +216 -447
- package/src/core/conversation-render-context.ts +20 -27
- package/src/core/conversation-rendering.ts +157 -575
- package/src/core/conversation-splash-state.ts +6 -1
- package/src/core/conversation-types.ts +35 -0
- package/src/core/conversation-usage.ts +12 -0
- package/src/core/conversation.ts +130 -131
- package/src/core/long-task-notifier.ts +100 -31
- package/src/core/notices.ts +75 -0
- package/src/core/pairing-tailscale-gateway.ts +1 -1
- package/src/core/process-output-log.ts +143 -0
- package/src/core/rewind-receipt.ts +1 -1
- package/src/core/session-focus.ts +190 -0
- package/src/core/session-resume-core.ts +23 -81
- package/src/core/stream-event-wiring.ts +31 -5
- package/src/core/system-message-noise.ts +2 -2
- package/src/core/system-message-router.ts +16 -18
- package/src/core/turn-event-wiring.ts +272 -33
- package/src/core/work-tree-focus.ts +290 -0
- package/src/core/work-tree-fold-store.ts +143 -0
- package/src/core/work-tree-model.ts +799 -0
- package/src/core/work-tree-render.ts +384 -0
- package/src/core/work-tree-sources.ts +184 -0
- package/src/core/work-tree-wiring.ts +176 -0
- package/src/core/workstream-notification.ts +36 -6
- package/src/core/wrfc-notice-titles.ts +37 -0
- package/src/input/agents-modal-text.ts +119 -0
- package/src/input/agents-modal.ts +699 -0
- package/src/input/bookmark-modal.ts +33 -9
- package/src/input/changes-git.ts +251 -0
- package/src/input/changes-modal.ts +629 -0
- package/src/input/changes-model.ts +125 -0
- package/src/input/command-palette-types.ts +41 -0
- package/src/input/command-palette.ts +272 -0
- package/src/input/command-reference.ts +2 -2
- package/src/input/command-registry.ts +77 -35
- package/src/input/commands/acp-runtime.ts +19 -9
- package/src/input/commands/channel-runtime.ts +2 -2
- package/src/input/commands/ci-runtime.ts +1 -1
- package/src/input/commands/codebase-runtime.ts +1 -1
- package/src/input/commands/config.ts +7 -1
- package/src/input/commands/context-window.ts +25 -19
- package/src/input/commands/control-room-runtime.ts +18 -18
- package/src/input/commands/conversation-runtime.ts +0 -1
- package/src/input/commands/cost-runtime.ts +29 -20
- package/src/input/commands/diff-runtime.ts +35 -181
- package/src/input/commands/eval.ts +1 -1
- package/src/input/commands/experience-runtime.ts +3 -7
- package/src/input/commands/fresh-conversation.ts +86 -0
- package/src/input/commands/git-runtime.ts +5 -22
- package/src/input/commands/graph-runtime.ts +1 -1
- package/src/input/commands/guidance-runtime.ts +1 -1
- package/src/input/commands/health-runtime.ts +8 -9
- package/src/input/commands/hooks-runtime.ts +2 -2
- package/src/input/commands/hosted-runtime.ts +3 -3
- package/src/input/commands/incident-runtime.ts +3 -3
- package/src/input/commands/intelligence-runtime.ts +6 -9
- package/src/input/commands/knowledge.ts +2 -2
- package/src/input/commands/legacy-panel-command.ts +57 -0
- package/src/input/commands/local-auth-runtime.ts +8 -9
- package/src/input/commands/local-provider-runtime.ts +7 -4
- package/src/input/commands/local-runtime.ts +12 -15
- package/src/input/commands/local-setup-review.ts +1 -1
- package/src/input/commands/local-setup.ts +13 -19
- package/src/input/commands/memory.ts +1 -1
- package/src/input/commands/notify-runtime.ts +9 -0
- package/src/input/commands/operator-runtime.ts +10 -12
- package/src/input/commands/permissions-provenance.ts +3 -3
- package/src/input/commands/planning-runtime.ts +7 -7
- package/src/input/commands/platform-sandbox-runtime.ts +2 -2
- package/src/input/commands/platform-services-runtime.ts +1 -1
- package/src/input/commands/policy-dispatch.ts +2 -2
- package/src/input/commands/policy.ts +3 -3
- package/src/input/commands/provider.ts +1 -1
- package/src/input/commands/remote-runtime.ts +1 -1
- package/src/input/commands/review-runtime.ts +39 -94
- package/src/input/commands/rewind-runtime.ts +81 -108
- package/src/input/commands/runtime-services.ts +15 -17
- package/src/input/commands/services-runtime.ts +2 -2
- package/src/input/commands/session-content.ts +21 -5
- package/src/input/commands/session-workflow.ts +13 -38
- package/src/input/commands/settings-sync-runtime.ts +2 -2
- package/src/input/commands/shell-core.ts +19 -4
- package/src/input/commands/skills-runtime.ts +2 -2
- package/src/input/commands/status-runtime.ts +13 -0
- package/src/input/commands/subscription-runtime.ts +1 -1
- package/src/input/commands/tasks-runtime.ts +2 -8
- package/src/input/commands/work-plan-runtime.ts +4 -4
- package/src/input/commands/workstream-attempts.ts +24 -47
- package/src/input/commands/workstream-runtime.ts +2 -2
- package/src/input/commands/worktree-runtime.ts +3 -3
- package/src/input/commands.ts +3 -1
- package/src/input/composer-editor.ts +44 -0
- package/src/input/concealed-input.ts +1 -1
- package/src/input/config-modal-types.ts +20 -8
- package/src/input/config-modal.ts +106 -105
- package/src/input/confirm-dialog-types.ts +28 -0
- package/src/input/confirm-dialog.ts +70 -0
- package/src/input/delete-key-policy.ts +3 -3
- package/src/input/feed-context-factory.ts +15 -16
- package/src/input/handler-command-route.ts +7 -42
- package/src/input/handler-content-actions.ts +16 -15
- package/src/input/handler-feed-routes.ts +26 -211
- package/src/input/handler-feed.ts +68 -95
- package/src/input/handler-interactions.ts +5 -3
- package/src/input/handler-modal-routes.ts +215 -216
- package/src/input/handler-modal-stack.ts +81 -11
- package/src/input/handler-modal-token-routes.ts +4 -0
- package/src/input/handler-onboarding.ts +0 -1
- package/src/input/handler-picker-routes.ts +65 -107
- package/src/input/handler-session-view-route.ts +149 -0
- package/src/input/handler-shortcuts.ts +16 -154
- package/src/input/handler-types.ts +4 -2
- package/src/input/handler-ui-state.ts +45 -15
- package/src/input/handler-work-tree-route.ts +123 -0
- package/src/input/handler.ts +25 -70
- package/src/input/keybindings.ts +16 -71
- package/src/input/masked-entry-modal.ts +117 -0
- package/src/input/mcp-workspace.ts +48 -17
- package/src/input/model-picker-filter.ts +8 -0
- package/src/input/model-picker-open.ts +99 -0
- package/src/input/model-picker-types.ts +1 -1
- package/src/input/model-picker.ts +92 -9
- package/src/input/notifications-modal.ts +123 -0
- package/src/input/overlay-filter.ts +26 -0
- package/src/input/profile-picker-modal.ts +37 -8
- package/src/input/search.ts +3 -3
- package/src/input/selection-modal-queue.ts +2 -0
- package/src/input/selection-modal.ts +28 -16
- package/src/input/session-picker-modal.ts +33 -8
- package/src/input/settings-modal-activation.ts +7 -2
- package/src/input/settings-modal-data.ts +11 -6
- package/src/input/settings-modal-types.ts +1 -1
- package/src/input/settings-modal.ts +20 -20
- package/src/input/submission-intent.ts +1 -1
- package/src/input/submission-router.ts +4 -9
- package/src/input/surface-modal-host.ts +110 -0
- package/src/input/theme-settings-actions.ts +82 -0
- package/src/input/usage-modal.ts +183 -0
- package/src/input/views.ts +97 -0
- package/src/main.ts +175 -165
- package/src/permissions/hunk-selection.ts +1 -1
- package/src/permissions/prompt-card.ts +511 -0
- package/src/permissions/prompt.ts +59 -622
- package/src/renderer/agents-modal.ts +310 -0
- package/src/renderer/autocomplete-overlay.ts +42 -220
- package/src/renderer/back-to-bottom.ts +80 -0
- package/src/renderer/block-actions-overlay.ts +30 -48
- package/src/renderer/bookmark-modal.ts +48 -79
- package/src/renderer/changes-modal.ts +325 -0
- package/src/renderer/code-block.ts +184 -106
- package/src/renderer/command-palette.ts +93 -0
- package/src/renderer/composer-fragments.ts +4 -4
- package/src/renderer/composer.ts +146 -0
- package/src/renderer/compositor.ts +26 -160
- package/src/renderer/config-modal.ts +185 -81
- package/src/renderer/confirm-dialog.ts +50 -0
- package/src/renderer/context-inspector.ts +116 -154
- package/src/renderer/context-status-hint.ts +4 -4
- package/src/renderer/conversation-layout.ts +20 -0
- package/src/renderer/conversation-overlays.ts +94 -84
- package/src/renderer/conversation-surface.ts +19 -38
- package/src/renderer/diff-tint.ts +98 -0
- package/src/renderer/file-picker-overlay.ts +63 -90
- package/src/renderer/footer-targets.ts +38 -0
- package/src/renderer/git-status.ts +3 -4
- package/src/renderer/header-line.ts +140 -117
- package/src/renderer/help-overlay.ts +133 -219
- package/src/renderer/history-search-overlay.ts +15 -12
- package/src/renderer/lane-graph/bead.ts +800 -0
- package/src/renderer/lane-graph/glyphs.ts +134 -0
- package/src/renderer/lane-graph/layout.ts +248 -0
- package/src/renderer/lane-graph/paint.ts +537 -0
- package/src/renderer/lane-graph/semantic-memo.ts +77 -0
- package/src/renderer/layout-engine.ts +2 -57
- package/src/renderer/layout.ts +7 -13
- package/src/renderer/markdown-inline.ts +2 -1
- package/src/renderer/markdown-table.ts +17 -12
- package/src/renderer/markdown.ts +92 -34
- package/src/renderer/masked-entry-modal.ts +78 -0
- package/src/renderer/mcp-workspace.ts +166 -203
- package/src/renderer/model-workspace.ts +381 -436
- package/src/renderer/notifications-modal.ts +116 -0
- package/src/renderer/onboarding/onboarding-wizard.ts +178 -588
- package/src/renderer/process-view.ts +192 -0
- package/src/renderer/profile-picker-modal.ts +58 -110
- package/src/renderer/search-overlay.ts +6 -3
- package/src/renderer/selection-modal-overlay.ts +87 -327
- package/src/renderer/semantic-diff.ts +2 -2
- package/src/renderer/session-chips.ts +102 -0
- package/src/renderer/session-picker-modal.ts +177 -297
- package/src/renderer/settings-modal-connections.ts +14 -23
- package/src/renderer/settings-modal-context.ts +367 -0
- package/src/renderer/settings-modal-helpers.ts +6 -11
- package/src/renderer/settings-modal.ts +322 -726
- package/src/renderer/shell-surface.ts +277 -175
- package/src/renderer/shortcuts-overlay.ts +217 -0
- package/src/renderer/status-line.ts +475 -0
- package/src/renderer/surface-compose.ts +174 -0
- package/src/renderer/surface-kit-extra.ts +303 -0
- package/src/renderer/surface-kit-list.ts +366 -0
- package/src/renderer/surface-kit-parts.ts +360 -0
- package/src/renderer/surface-kit.ts +568 -0
- package/src/renderer/syntax-highlighter.ts +280 -197
- package/src/renderer/syntax-theme.ts +51 -0
- package/src/renderer/system-message.ts +0 -1
- package/src/renderer/term-caps.ts +42 -1
- package/src/renderer/terminal-bg-probe.ts +116 -19
- package/src/renderer/terminal-palette-probe.ts +322 -0
- package/src/renderer/terminal-palette-reader.ts +183 -0
- package/src/renderer/terminal-palette.ts +58 -0
- package/src/renderer/theme-mode-config.ts +16 -1
- package/src/renderer/theme.ts +234 -207
- package/src/renderer/thinking.ts +0 -1
- package/src/renderer/throbber.ts +188 -0
- package/src/renderer/toast-center.ts +119 -0
- package/src/renderer/ui-factory.ts +39 -602
- package/src/renderer/ui-primitives.ts +8 -25
- package/src/renderer/usage-modal.ts +353 -0
- package/src/renderer/user-message.ts +58 -0
- package/src/renderer/voice-capture-chip.ts +53 -0
- package/src/runtime/bootstrap-command-context.ts +0 -5
- package/src/runtime/bootstrap-command-parts.ts +38 -89
- package/src/runtime/bootstrap-core.ts +13 -15
- package/src/runtime/bootstrap-hook-bridge.ts +12 -21
- package/src/runtime/bootstrap-notifier-sync.ts +17 -1
- package/src/runtime/bootstrap-shell.ts +16 -50
- package/src/runtime/bootstrap.ts +14 -14
- package/src/runtime/client/fleet-union.ts +5 -5
- package/src/runtime/client/hosted-exit.ts +2 -2
- package/src/runtime/client/hosted-sessions.ts +1 -1
- package/src/runtime/client/spine-adoption.ts +1 -1
- package/src/runtime/context-accounting-source.ts +5 -4
- package/src/runtime/index.ts +1 -0
- package/src/runtime/notification-dispatch.ts +102 -29
- package/src/runtime/recovery-offer-wiring.ts +4 -1
- package/src/runtime/recovery-prompt.ts +31 -15
- package/src/runtime/runtime-services-types.ts +10 -6
- package/src/runtime/services.ts +17 -12
- package/src/runtime/session-continuity-hints.ts +2 -5
- package/src/runtime/session-inbound-inputs.ts +1 -1
- package/src/runtime/session-resume-liveness-confirm.ts +14 -2
- package/src/runtime/startup-typeahead-gate.ts +103 -0
- package/src/runtime/store/domains/domain-read-matrix.ts +0 -1
- package/src/runtime/store/domains/index.ts +0 -8
- package/src/runtime/store/selectors/index.ts +0 -28
- package/src/runtime/store/state.ts +0 -8
- package/src/runtime/ui-services.ts +4 -6
- package/src/runtime/usage-tracker.ts +348 -0
- package/src/runtime/wrfc-persistence.ts +4 -4
- package/src/shell/blocking-input.ts +92 -2
- package/src/shell/recovery-input-helpers.ts +1 -1
- package/src/shell/session-views.ts +539 -0
- package/src/shell/status-report.ts +86 -0
- package/src/shell/transcript-scroll.ts +68 -0
- package/src/shell/ui-openers.ts +84 -172
- package/src/shell/view-openers.ts +205 -0
- package/src/tools/wrfc-agent-guard.ts +22 -2
- package/src/utils/format-duration.ts +4 -8
- package/src/utils/splash-lines.ts +9 -1
- package/src/utils/terminal-width.ts +56 -0
- package/src/verification/live-verifier.ts +0 -10
- package/src/version.ts +1 -1
- package/src/{panels → views}/agent-inspector-shared.ts +2 -2
- package/src/{panels → views}/builtin-modals.ts +22 -21
- package/src/views/builtin-views.ts +135 -0
- package/src/{panels → views}/confirm-state.ts +9 -9
- package/src/{panels → views}/diff-review-model.ts +10 -3
- package/src/{panels → views}/eval-registry.ts +1 -1
- package/src/{panels → views}/fleet-acts.ts +49 -85
- package/src/{panels/fleet-panel-format.ts → views/fleet-format.ts} +40 -40
- package/src/{panels → views}/fleet-gateway.ts +5 -5
- package/src/{panels → views}/fleet-observed-render.ts +16 -16
- package/src/{panels → views}/fleet-read-model.ts +9 -9
- package/src/{panels/fleet-tabs.ts → views/fleet-session-tabs.ts} +13 -13
- package/src/{panels → views}/fleet-spawn.ts +33 -49
- package/src/{panels → views}/fleet-steer.ts +12 -12
- package/src/{panels → views}/fleet-stop.ts +4 -4
- package/src/{panels → views}/fleet-transcript.ts +26 -27
- package/src/{panels/fleet-panel-worktree-detail.ts → views/fleet-worktree-detail.ts} +7 -7
- package/src/{panels → views}/hosted-session-feed.ts +6 -6
- package/src/views/modal-surface-registry.ts +41 -0
- package/src/{panels → views}/modals/devices-modal.ts +2 -2
- package/src/{panels → views}/modals/hooks-modal.ts +13 -13
- package/src/{panels → views}/modals/keybindings-modal.ts +19 -27
- package/src/{panels → views}/modals/knowledge-modal.ts +6 -5
- package/src/{panels → views}/modals/local-auth-modal.ts +22 -21
- package/src/{panels → views}/modals/marketplace-modal.ts +11 -11
- package/src/{panels → views}/modals/memory-modal.ts +8 -8
- package/src/{panels → views}/modals/modal-surface-helpers.ts +15 -5
- package/src/{panels → views}/modals/pairing-modal.ts +9 -9
- package/src/{panels → views}/modals/planning-modal.ts +15 -20
- package/src/{panels → views}/modals/plugins-modal.ts +6 -6
- package/src/{panels → views}/modals/policy-modal.ts +13 -5
- package/src/{panels → views}/modals/provider-health-modal.ts +4 -4
- package/src/{panels → views}/modals/remote-modal.ts +3 -3
- package/src/{panels → views}/modals/sandbox-modal.ts +4 -4
- package/src/{panels → views}/modals/security-modal.ts +22 -16
- package/src/{panels → views}/modals/services-modal.ts +3 -3
- package/src/{panels → views}/modals/settings-sync-modal.ts +4 -4
- package/src/{panels → views}/modals/skills-modal.ts +9 -9
- package/src/{panels → views}/modals/subscription-modal.ts +3 -3
- package/src/{panels → views}/modals/work-plan-modal.ts +10 -13
- package/src/views/notifications-feed.ts +314 -0
- package/src/{panels → views}/polish-core.ts +38 -36
- package/src/{panels → views}/polish-tables.ts +35 -26
- package/src/{panels → views}/polish.ts +101 -95
- package/src/{panels → views}/project-planning-answer-actions.ts +3 -3
- package/src/{panels → views}/provider-health-routes.ts +14 -14
- package/src/{panels → views}/search-focus.ts +8 -8
- package/src/{panels → views}/session-maintenance.ts +11 -11
- package/src/views/skills-discovery.ts +159 -0
- package/src/views/view-deps.ts +80 -0
- package/src/views/view-panel-adapter.ts +53 -0
- package/src/{panels → views}/workstream-graph-render.ts +1 -1
- package/src/core/conversation-fold.ts +0 -101
- package/src/core/conversation-turn-structure.ts +0 -464
- package/src/core/conversation-user-receipts.ts +0 -42
- package/src/input/commands/operator-panel-runtime.ts +0 -186
- package/src/input/panel-integration-actions.ts +0 -33
- package/src/input/panel-mouse-geometry.ts +0 -97
- package/src/input/panel-paste-flood-guard.ts +0 -86
- package/src/panels/base-panel.ts +0 -301
- package/src/panels/builtin/agent.ts +0 -27
- package/src/panels/builtin/development.ts +0 -92
- package/src/panels/builtin/knowledge.ts +0 -12
- package/src/panels/builtin/operations.ts +0 -177
- package/src/panels/builtin/session.ts +0 -81
- package/src/panels/builtin/shared.ts +0 -357
- package/src/panels/builtin-panels.ts +0 -28
- package/src/panels/cost-tracker-panel.ts +0 -746
- package/src/panels/diff-panel.ts +0 -797
- package/src/panels/diff-review-panel.ts +0 -395
- package/src/panels/expandable-list-panel.ts +0 -189
- package/src/panels/fleet-deep-link.ts +0 -31
- package/src/panels/fleet-panel.ts +0 -799
- package/src/panels/git-panel.ts +0 -800
- package/src/panels/hosted-session-panel.ts +0 -221
- package/src/panels/index.ts +0 -21
- package/src/panels/local-auth-panel.ts +0 -478
- package/src/panels/notifications-feed.ts +0 -127
- package/src/panels/notifications-panel.ts +0 -160
- package/src/panels/panel-confirm-overlay.ts +0 -72
- package/src/panels/panel-manager.ts +0 -791
- package/src/panels/plugins-panel.ts +0 -371
- package/src/panels/scrollable-list-panel.ts +0 -498
- package/src/panels/skills-panel.ts +0 -454
- package/src/panels/token-budget-panel.ts +0 -688
- package/src/panels/types.ts +0 -191
- package/src/panels/wrfc-panel-format.ts +0 -133
- package/src/renderer/diff-view.ts +0 -134
- package/src/renderer/fleet-tab-strip.ts +0 -56
- package/src/renderer/footer-tips.ts +0 -53
- package/src/renderer/fullscreen-primitives.ts +0 -146
- package/src/renderer/fullscreen-workspace.ts +0 -194
- package/src/renderer/hint-grammar.ts +0 -52
- package/src/renderer/modal-factory.ts +0 -480
- package/src/renderer/overlay-box.ts +0 -154
- package/src/renderer/overlay-viewport.ts +0 -104
- package/src/renderer/panel-composite.ts +0 -145
- package/src/renderer/panel-workspace-bar.ts +0 -54
- package/src/renderer/process-indicator.ts +0 -163
- package/src/renderer/tab-strip.ts +0 -222
- package/src/renderer/text-layout.ts +0 -31
- package/src/renderer/tool-call.ts +0 -276
- package/src/renderer/tool-result-expanded-lines.ts +0 -48
- package/src/renderer/tool-result-summary.ts +0 -219
- package/src/runtime/diagnostics/panels/index.ts +0 -21
- package/src/runtime/diagnostics/panels/ops.ts +0 -156
- package/src/runtime/perf/panel-contracts.ts +0 -32
- package/src/runtime/store/domains/panels.ts +0 -117
- package/src/verification/verification-ledger.ts +0 -707
- /package/src/{panels → views}/memory-consolidation-gateway.ts +0 -0
- /package/src/{panels → views}/modals/modal-theme.ts +0 -0
- /package/src/{panels → views}/provider-health-domains.ts +0 -0
package/src/panels/diff-panel.ts
DELETED
|
@@ -1,797 +0,0 @@
|
|
|
1
|
-
// ---------------------------------------------------------------------------
|
|
2
|
-
// DiffPanel, unified diff view of agent file changes
|
|
3
|
-
// ---------------------------------------------------------------------------
|
|
4
|
-
|
|
5
|
-
import type { Line } from '@pellux/goodvibes-sdk/platform/types';
|
|
6
|
-
import { createStyledCell, createEmptyLine } from '@pellux/goodvibes-sdk/platform/types';
|
|
7
|
-
import { truncateDisplay, getDisplayWidth } from '../utils/terminal-width.ts';
|
|
8
|
-
import { GitService, type StructuredDiff, type StructuredDiffFile } from '@pellux/goodvibes-sdk/platform/git';
|
|
9
|
-
import { BasePanel } from './base-panel.ts';
|
|
10
|
-
import { UI_TONES, DIFF_TONES } from '../renderer/ui-primitives.ts';
|
|
11
|
-
import { logger } from '@pellux/goodvibes-sdk/platform/utils';
|
|
12
|
-
import { PanelConfirmOverlay } from './panel-confirm-overlay.ts';
|
|
13
|
-
import {
|
|
14
|
-
buildBodyText,
|
|
15
|
-
buildEmptyState,
|
|
16
|
-
buildPanelWorkspace,
|
|
17
|
-
resolveScrollablePanelSection,
|
|
18
|
-
buildStyledPanelLine,
|
|
19
|
-
type PanelWorkspaceSection,
|
|
20
|
-
DEFAULT_PANEL_PALETTE,
|
|
21
|
-
extendPalette,
|
|
22
|
-
} from './polish.ts';
|
|
23
|
-
|
|
24
|
-
// ---------------------------------------------------------------------------
|
|
25
|
-
// Colour palette, dedicated diff-viewer scheme (like a mini syntax
|
|
26
|
-
// highlighter). Each hex literal is named once and reused for every role
|
|
27
|
-
// that shares it, including the workspace-chrome aliases (info/dim/value/
|
|
28
|
-
// empty) so the raw-hex count never grows. The title band itself is NOT
|
|
29
|
-
// overridden, buildPanelWorkspace always falls back to the canonical
|
|
30
|
-
// DEFAULT_PANEL_PALETTE.headerBg (one title band everywhere).
|
|
31
|
-
// ---------------------------------------------------------------------------
|
|
32
|
-
|
|
33
|
-
// Hunk blue is the shared DIFF_TONES token, diff-view.ts (conversation)
|
|
34
|
-
// and git-panel.ts's inline diff converge onto this file's pre-existing value.
|
|
35
|
-
const HUNK_BLUE: string = DIFF_TONES.hunk;
|
|
36
|
-
// Context rows and line-number gutter use the shared theme's muted/dim
|
|
37
|
-
// foreground tones rather than dedicated gray hex literals.
|
|
38
|
-
const CONTEXT_GRAY = UI_TONES.fg.muted;
|
|
39
|
-
const FILENAME_WHITE = '#ffffff';
|
|
40
|
-
// Add/del text colors are the shared DIFF_TONES tokens, whose values ARE this
|
|
41
|
-
// panel's shipped colors, this panel is the reference diff look (diff-view
|
|
42
|
-
// was unwired dead code until, so "majority of surfaces" was a mirage);
|
|
43
|
-
// the conversation surface converges onto these, not the reverse.
|
|
44
|
-
const ADD_GREEN: string = DIFF_TONES.add;
|
|
45
|
-
const ADD_BG = '#001a0d';
|
|
46
|
-
const DEL_RED: string = DIFF_TONES.del;
|
|
47
|
-
const DEL_BG = '#1a0000';
|
|
48
|
-
const HUNK_BG = '#0a0a1a';
|
|
49
|
-
const MARKER_GRAY = '#aaaaaa';
|
|
50
|
-
const LINE_NUM_GRAY = UI_TONES.fg.dim;
|
|
51
|
-
const LINE_NUM_ADD = '#00aa55';
|
|
52
|
-
const LINE_NUM_DEL = '#aa2222';
|
|
53
|
-
const TAB_ACTIVE_BG = '#333333';
|
|
54
|
-
const TAB_INACTIVE_GRAY = '#666666';
|
|
55
|
-
const TAB_BG = '#222222';
|
|
56
|
-
const STATUS_BAR_BG = '#444444';
|
|
57
|
-
|
|
58
|
-
const COLOR = extendPalette(DEFAULT_PANEL_PALETTE, {
|
|
59
|
-
// Workspace-chrome aliases (title band excluded, no headerBg override)
|
|
60
|
-
info: HUNK_BLUE,
|
|
61
|
-
dim: CONTEXT_GRAY,
|
|
62
|
-
value: FILENAME_WHITE,
|
|
63
|
-
empty: CONTEXT_GRAY,
|
|
64
|
-
|
|
65
|
-
// Domain accents
|
|
66
|
-
addition: ADD_GREEN,
|
|
67
|
-
additionBg: ADD_BG,
|
|
68
|
-
deletion: DEL_RED,
|
|
69
|
-
deletionBg: DEL_BG,
|
|
70
|
-
hunk: HUNK_BLUE,
|
|
71
|
-
hunkBg: HUNK_BG,
|
|
72
|
-
markerText: MARKER_GRAY,
|
|
73
|
-
lineNum: LINE_NUM_GRAY,
|
|
74
|
-
lineNumAdd: LINE_NUM_ADD,
|
|
75
|
-
lineNumDel: LINE_NUM_DEL,
|
|
76
|
-
filename: FILENAME_WHITE,
|
|
77
|
-
tabActive: FILENAME_WHITE,
|
|
78
|
-
tabActiveBg: TAB_ACTIVE_BG,
|
|
79
|
-
tabInactive: TAB_INACTIVE_GRAY,
|
|
80
|
-
tabBg: TAB_BG,
|
|
81
|
-
context: CONTEXT_GRAY,
|
|
82
|
-
statusBar: STATUS_BAR_BG,
|
|
83
|
-
} as const);
|
|
84
|
-
|
|
85
|
-
// ---------------------------------------------------------------------------
|
|
86
|
-
// Types
|
|
87
|
-
// ---------------------------------------------------------------------------
|
|
88
|
-
|
|
89
|
-
interface DiffEntry {
|
|
90
|
-
filePath: string;
|
|
91
|
-
raw: string; // raw unified diff text
|
|
92
|
-
lines: ParsedLine[];
|
|
93
|
-
/** One-line semantic summary from computeSemanticDiff, if available. */
|
|
94
|
-
semanticSummary?: string;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
/** +added / -removed line counts for a parsed diff entry. */
|
|
98
|
-
function diffStat(entry: DiffEntry): { added: number; removed: number } {
|
|
99
|
-
let added = 0;
|
|
100
|
-
let removed = 0;
|
|
101
|
-
for (const line of entry.lines) {
|
|
102
|
-
if (line.kind === 'addition') added++;
|
|
103
|
-
else if (line.kind === 'deletion') removed++;
|
|
104
|
-
}
|
|
105
|
-
return { added, removed };
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
type LineKind = 'addition' | 'deletion' | 'context' | 'hunk' | 'header';
|
|
109
|
-
|
|
110
|
-
interface ParsedLine {
|
|
111
|
-
kind: LineKind;
|
|
112
|
-
text: string;
|
|
113
|
-
beforeNum: number | null; // line number in original file
|
|
114
|
-
afterNum: number | null; // line number in new file
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
// ---------------------------------------------------------------------------
|
|
118
|
-
// Diff parser
|
|
119
|
-
// ---------------------------------------------------------------------------
|
|
120
|
-
|
|
121
|
-
function parseDiff(raw: string): ParsedLine[] {
|
|
122
|
-
const result: ParsedLine[] = [];
|
|
123
|
-
let before = 0;
|
|
124
|
-
let after = 0;
|
|
125
|
-
|
|
126
|
-
for (const line of raw.split('\n')) {
|
|
127
|
-
// Hunk header: @@ -a,b +c,d @@
|
|
128
|
-
const hunkMatch = line.match(/^@@\s+-([0-9]+)(?:,[0-9]+)?\s+\+([0-9]+)(?:,[0-9]+)?\s+@@/);
|
|
129
|
-
if (hunkMatch) {
|
|
130
|
-
before = parseInt(hunkMatch[1]!, 10);
|
|
131
|
-
after = parseInt(hunkMatch[2]!, 10);
|
|
132
|
-
result.push({ kind: 'hunk', text: line, beforeNum: null, afterNum: null });
|
|
133
|
-
continue;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
if (line.startsWith('+++') || line.startsWith('---') ||
|
|
137
|
-
line.startsWith('diff ') || line.startsWith('index ') ||
|
|
138
|
-
line.startsWith('new file') || line.startsWith('old file') ||
|
|
139
|
-
line.startsWith('Binary')) {
|
|
140
|
-
result.push({ kind: 'header', text: line, beforeNum: null, afterNum: null });
|
|
141
|
-
continue;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
if (line.startsWith('+')) {
|
|
145
|
-
result.push({ kind: 'addition', text: line.slice(1), beforeNum: null, afterNum: after });
|
|
146
|
-
after++;
|
|
147
|
-
} else if (line.startsWith('-')) {
|
|
148
|
-
result.push({ kind: 'deletion', text: line.slice(1), beforeNum: before, afterNum: null });
|
|
149
|
-
before++;
|
|
150
|
-
} else if (line.startsWith('\\')) {
|
|
151
|
-
// "No newline at end of file" note, treat as header
|
|
152
|
-
result.push({ kind: 'header', text: line, beforeNum: null, afterNum: null });
|
|
153
|
-
} else {
|
|
154
|
-
// context line (starts with space, or empty for blank context)
|
|
155
|
-
const text = line.startsWith(' ') ? line.slice(1) : line;
|
|
156
|
-
result.push({ kind: 'context', text, beforeNum: before, afterNum: after });
|
|
157
|
-
before++;
|
|
158
|
-
after++;
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
return result;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
// ---------------------------------------------------------------------------
|
|
166
|
-
// Split a full `git diff` output into per-file entries
|
|
167
|
-
// ---------------------------------------------------------------------------
|
|
168
|
-
|
|
169
|
-
/**
|
|
170
|
-
* Synthetic placeholder entries ('(error)', '(no changes)', '(no staged
|
|
171
|
-
* changes)', '(not a git repo)') are not real diffs, parenthesized so they
|
|
172
|
-
* can be styled distinctly in the tab/status bar instead of reading as a fake
|
|
173
|
-
* diff of a real file.
|
|
174
|
-
*/
|
|
175
|
-
function isPlaceholderPath(filePath: string): boolean {
|
|
176
|
-
return filePath.startsWith('(') && filePath.endsWith(')');
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
/**
|
|
180
|
-
* Extract the file path from one `diff --git`-delimited chunk. Tries, in
|
|
181
|
-
* order: the standard two-sided header (`diff --git a/x b/y`, bare or
|
|
182
|
-
* quoted), the combined/merge-conflict header (`diff --cc <path>` /
|
|
183
|
-
* `diff --combined <path>`, which carries no a/b prefixes), then falls back
|
|
184
|
-
* to the `+++`/`---` file lines (present across every variant, uniformly),
|
|
185
|
-
* skipping a `/dev/null` side (deleted/new file). 'unknown' is reserved for
|
|
186
|
-
* genuinely unparseable input.
|
|
187
|
-
*/
|
|
188
|
-
function extractDiffFilePath(trimmed: string): string {
|
|
189
|
-
const quotedGit = trimmed.match(/^diff --git "a\/(.+)" "b\/(.+)"$/m);
|
|
190
|
-
if (quotedGit) return quotedGit[2]!;
|
|
191
|
-
const plainGit = trimmed.match(/^diff --git a\/.+? b\/(.+)$/m);
|
|
192
|
-
if (plainGit) return plainGit[1]!;
|
|
193
|
-
const combined = trimmed.match(/^diff --(?:cc|combined) "?([^"\n]+?)"?$/m);
|
|
194
|
-
if (combined) return combined[1]!;
|
|
195
|
-
const plus = trimmed.match(/^\+\+\+ (?:b\/)?"?([^"\n]+?)"?\s*$/m);
|
|
196
|
-
if (plus && plus[1] !== '/dev/null') return plus[1]!;
|
|
197
|
-
const minus = trimmed.match(/^--- (?:a\/)?"?([^"\n]+?)"?\s*$/m);
|
|
198
|
-
if (minus && minus[1] !== '/dev/null') return minus[1]!;
|
|
199
|
-
return 'unknown';
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
/** Reconstruct one file's unified-diff text from a StructuredDiffFile (header + hunks, prefixes re-attached; no size cap). */
|
|
203
|
-
function reconstructStructuredFile(file: StructuredDiffFile): string {
|
|
204
|
-
const out: string[] = [...file.headerLines];
|
|
205
|
-
for (const hunk of file.hunks) {
|
|
206
|
-
out.push(hunk.header);
|
|
207
|
-
for (const line of hunk.lines) {
|
|
208
|
-
const prefix = line.kind === 'add' ? '+' : line.kind === 'del' ? '-' : ' ';
|
|
209
|
-
out.push(`${prefix}${line.text}`);
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
return out.join('\n');
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
function splitIntoDiffEntries(raw: string): DiffEntry[] {
|
|
216
|
-
const entries: DiffEntry[] = [];
|
|
217
|
-
// Split on "diff --git" / "diff --cc" / "diff --combined" lines
|
|
218
|
-
const chunks = raw.split(/(?=^diff --git |^diff --cc |^diff --combined )/m);
|
|
219
|
-
for (const chunk of chunks) {
|
|
220
|
-
const trimmed = chunk.trim();
|
|
221
|
-
if (!trimmed) continue;
|
|
222
|
-
|
|
223
|
-
entries.push({
|
|
224
|
-
filePath: extractDiffFilePath(trimmed),
|
|
225
|
-
raw: chunk,
|
|
226
|
-
lines: parseDiff(chunk),
|
|
227
|
-
});
|
|
228
|
-
}
|
|
229
|
-
return entries;
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
// ---------------------------------------------------------------------------
|
|
233
|
-
// Rendering helpers
|
|
234
|
-
// ---------------------------------------------------------------------------
|
|
235
|
-
|
|
236
|
-
function makeLine(
|
|
237
|
-
width: number,
|
|
238
|
-
leftNum: string,
|
|
239
|
-
rightNum: string,
|
|
240
|
-
content: string,
|
|
241
|
-
fg: string,
|
|
242
|
-
bg: string,
|
|
243
|
-
numFg: string,
|
|
244
|
-
bold: boolean = false,
|
|
245
|
-
): Line {
|
|
246
|
-
// Left line number (5 chars + space)
|
|
247
|
-
const LEFT_W = 5;
|
|
248
|
-
const usedForNums = LEFT_W + 1 + LEFT_W + 1 + 2; // 14
|
|
249
|
-
const contentWidth = Math.max(0, width - usedForNums);
|
|
250
|
-
const truncated = truncateDisplay(content, contentWidth);
|
|
251
|
-
return buildStyledPanelLine(width, [
|
|
252
|
-
{ text: leftNum.padStart(LEFT_W), fg: numFg, bg, dim: true },
|
|
253
|
-
{ text: ' ', fg: '', bg },
|
|
254
|
-
{ text: rightNum.padStart(LEFT_W), fg: numFg, bg, dim: true },
|
|
255
|
-
{ text: ' ', fg: '', bg },
|
|
256
|
-
{ text: '| ', fg: COLOR.lineNum, bg },
|
|
257
|
-
{ text: truncated, fg, bg, bold },
|
|
258
|
-
]);
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
function renderText(width: number, text: string, fg: string, bg: string, bold = false): Line {
|
|
262
|
-
return buildStyledPanelLine(width, [{ text: truncateDisplay(text, width), fg, bg, bold }]);
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
// ---------------------------------------------------------------------------
|
|
266
|
-
// DiffPanel
|
|
267
|
-
// ---------------------------------------------------------------------------
|
|
268
|
-
|
|
269
|
-
export class DiffPanel extends BasePanel {
|
|
270
|
-
public override isTransient = true;
|
|
271
|
-
|
|
272
|
-
private readonly workingDirectory: string;
|
|
273
|
-
private entries: DiffEntry[] = [];
|
|
274
|
-
private selectedFile = 0;
|
|
275
|
-
private scrollOffset = 0;
|
|
276
|
-
|
|
277
|
-
/** One-line confirmation for the 'w'/'h'/'s' self-load hotkeys, otherwise a
|
|
278
|
-
* reload that produces identical content is visually indistinguishable
|
|
279
|
-
* from the keypress doing nothing at all. */
|
|
280
|
-
private hotkeyStatus: string | null = null;
|
|
281
|
-
|
|
282
|
-
public readonly confirmOverlay = new PanelConfirmOverlay(() => this.markDirty()); // armed by a command handler (e.g. /rewind); see panel-confirm-overlay.ts
|
|
283
|
-
constructor(
|
|
284
|
-
workingDirectory: string,
|
|
285
|
-
private readonly requestRender: () => void = () => {},
|
|
286
|
-
) {
|
|
287
|
-
super('diff', 'Diff', 'D', 'development');
|
|
288
|
-
this.workingDirectory = workingDirectory;
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
// -------------------------------------------------------------------------
|
|
292
|
-
// Public API
|
|
293
|
-
// -------------------------------------------------------------------------
|
|
294
|
-
|
|
295
|
-
/** Show a unified diff for a specific file. Adds or replaces the entry. */
|
|
296
|
-
showDiff(filePath: string, diff: string): void {
|
|
297
|
-
const idx = this.entries.findIndex(e => e.filePath === filePath);
|
|
298
|
-
const entry: DiffEntry = { filePath, raw: diff, lines: parseDiff(diff), semanticSummary: this.entries[idx]?.semanticSummary };
|
|
299
|
-
if (idx >= 0) {
|
|
300
|
-
this.entries[idx] = entry;
|
|
301
|
-
// Stay on this file if it was already selected
|
|
302
|
-
if (this.selectedFile !== idx) {
|
|
303
|
-
this.selectedFile = idx;
|
|
304
|
-
this.scrollOffset = 0;
|
|
305
|
-
}
|
|
306
|
-
} else {
|
|
307
|
-
this.entries.push(entry);
|
|
308
|
-
this.selectedFile = this.entries.length - 1;
|
|
309
|
-
this.scrollOffset = 0;
|
|
310
|
-
}
|
|
311
|
-
this.markDirty();
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
/** Load a raw multi-file unified diff string directly. */
|
|
315
|
-
loadRawDiff(raw: string): void {
|
|
316
|
-
this.entries = splitIntoDiffEntries(raw);
|
|
317
|
-
this.selectedFile = 0;
|
|
318
|
-
this.scrollOffset = 0;
|
|
319
|
-
this.markDirty();
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
/**
|
|
323
|
-
* Ingest a StructuredDiff (GitService.diffStructured, the FULL, uncapped
|
|
324
|
-
* working-tree diff). Each file becomes one DiffEntry (unified text
|
|
325
|
-
* reconstructed) so the existing tabs/renderer work unchanged; the old
|
|
326
|
-
* 4,000-char slice is gone and a diff of any length renders complete.
|
|
327
|
-
*/
|
|
328
|
-
loadStructuredDiff(diff: StructuredDiff): void {
|
|
329
|
-
this.entries = diff.files.map((file) => {
|
|
330
|
-
const raw = reconstructStructuredFile(file);
|
|
331
|
-
return {
|
|
332
|
-
filePath: file.newPath ?? file.oldPath ?? 'unknown',
|
|
333
|
-
raw,
|
|
334
|
-
lines: parseDiff(raw),
|
|
335
|
-
};
|
|
336
|
-
});
|
|
337
|
-
this.selectedFile = 0;
|
|
338
|
-
this.scrollOffset = 0;
|
|
339
|
-
this.markDirty();
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
/**
|
|
343
|
-
* Defensive gate mirroring GitPanel's own notGitRepo messaging, short-
|
|
344
|
-
* circuits before any of this panel's self-load git spawns run, instead of
|
|
345
|
-
* letting git fail per-subcommand with an inconsistent error shape.
|
|
346
|
-
*/
|
|
347
|
-
private showNotGitRepo(): void {
|
|
348
|
-
this.showDiff('(not a git repo)', '@@ -0,0 +0,0 @@\n Not a git repository here.');
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
/** Run `git diff` against specific files and populate entries. */
|
|
352
|
-
async showFileDiffs(files: string[], ref?: string): Promise<void> {
|
|
353
|
-
if (!GitService.isGitRepo(this.workingDirectory)) {
|
|
354
|
-
this.showNotGitRepo();
|
|
355
|
-
return;
|
|
356
|
-
}
|
|
357
|
-
const args = ['diff', ...(ref ? [ref] : []), '--', ...files];
|
|
358
|
-
const proc = Bun.spawn(['git', ...args], { stdout: 'pipe', stderr: 'pipe', cwd: this.workingDirectory });
|
|
359
|
-
const [raw, errText] = await Promise.all([
|
|
360
|
-
new Response(proc.stdout).text(),
|
|
361
|
-
new Response(proc.stderr).text(),
|
|
362
|
-
]);
|
|
363
|
-
const exitCode = await proc.exited;
|
|
364
|
-
if (exitCode !== 0) {
|
|
365
|
-
const errorText = errText.trim() || 'git diff failed';
|
|
366
|
-
this.showDiff('(error)', `--- error\n+++ error\n@@ -0,0 +1,1 @@\n+${errorText}`);
|
|
367
|
-
return;
|
|
368
|
-
}
|
|
369
|
-
this.loadRawDiff(raw);
|
|
370
|
-
this.enrichSemanticDiffs(files, ref ?? 'HEAD').catch((err) => { logger.debug('DiffPanel: semantic diff enrichment failed', { err }); });
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
/**
|
|
374
|
-
* Run `git diff` and populate all changed files. Self-load entry point for
|
|
375
|
-
* the 'w' (working tree, no ref) and 'h' (vs HEAD) keys. Automatically
|
|
376
|
-
* enriches every loaded file with a semantic-diff summary in the background.
|
|
377
|
-
*/
|
|
378
|
-
async showGitDiff(ref?: string): Promise<void> {
|
|
379
|
-
if (!GitService.isGitRepo(this.workingDirectory)) {
|
|
380
|
-
this.showNotGitRepo();
|
|
381
|
-
return;
|
|
382
|
-
}
|
|
383
|
-
const args = ['diff', ...(ref ? [ref] : [])];
|
|
384
|
-
const proc = Bun.spawn(['git', ...args], { stdout: 'pipe', stderr: 'pipe', cwd: this.workingDirectory });
|
|
385
|
-
const [raw, errText] = await Promise.all([
|
|
386
|
-
new Response(proc.stdout).text(),
|
|
387
|
-
new Response(proc.stderr).text(),
|
|
388
|
-
]);
|
|
389
|
-
const exitCode = await proc.exited;
|
|
390
|
-
if (exitCode !== 0) {
|
|
391
|
-
const errorText = errText.trim() || 'git diff failed';
|
|
392
|
-
this.showDiff('(error)', `--- error\n+++ error\n@@ -0,0 +1,1 @@\n+${errorText}`);
|
|
393
|
-
return;
|
|
394
|
-
}
|
|
395
|
-
if (!raw.trim()) {
|
|
396
|
-
this.showDiff('(no changes)', '@@ -0,0 +0,0 @@\n No changes in working tree.');
|
|
397
|
-
return;
|
|
398
|
-
}
|
|
399
|
-
const newEntries = splitIntoDiffEntries(raw);
|
|
400
|
-
// Merge: update existing, append new
|
|
401
|
-
for (const entry of newEntries) {
|
|
402
|
-
const idx = this.entries.findIndex(e => e.filePath === entry.filePath);
|
|
403
|
-
if (idx >= 0) {
|
|
404
|
-
this.entries[idx] = { ...entry, semanticSummary: this.entries[idx]!.semanticSummary };
|
|
405
|
-
} else {
|
|
406
|
-
this.entries.push(entry);
|
|
407
|
-
}
|
|
408
|
-
}
|
|
409
|
-
this.selectedFile = Math.min(this.selectedFile, Math.max(0, this.entries.length - 1));
|
|
410
|
-
this.scrollOffset = 0;
|
|
411
|
-
this.markDirty();
|
|
412
|
-
this.enrichSemanticDiffs(newEntries.map((e) => e.filePath), 'HEAD').catch((err) => { logger.debug('DiffPanel: semantic diff enrichment failed', { err }); });
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
/**
|
|
416
|
-
* Run `git diff --cached` and populate the staged changes. Self-load entry
|
|
417
|
-
* point for the 's' key, its own diff plumbing, independent of the
|
|
418
|
-
* `/diff staged` command handler.
|
|
419
|
-
*/
|
|
420
|
-
async showStagedDiff(): Promise<void> {
|
|
421
|
-
if (!GitService.isGitRepo(this.workingDirectory)) {
|
|
422
|
-
this.showNotGitRepo();
|
|
423
|
-
return;
|
|
424
|
-
}
|
|
425
|
-
const proc = Bun.spawn(['/bin/sh', '-c', 'git diff --cached'], { stdout: 'pipe', stderr: 'pipe', cwd: this.workingDirectory });
|
|
426
|
-
const [raw, errText] = await Promise.all([
|
|
427
|
-
new Response(proc.stdout).text(),
|
|
428
|
-
new Response(proc.stderr).text(),
|
|
429
|
-
]);
|
|
430
|
-
const exitCode = await proc.exited;
|
|
431
|
-
if (exitCode !== 0) {
|
|
432
|
-
const errorText = errText.trim() || 'git diff --cached failed';
|
|
433
|
-
this.showDiff('(error)', `--- error\n+++ error\n@@ -0,0 +1,1 @@\n+${errorText}`);
|
|
434
|
-
return;
|
|
435
|
-
}
|
|
436
|
-
if (!raw.trim()) {
|
|
437
|
-
this.showDiff('(no staged changes)', '@@ -0,0 +0,0 @@\n No staged changes.');
|
|
438
|
-
return;
|
|
439
|
-
}
|
|
440
|
-
this.loadRawDiff(raw);
|
|
441
|
-
this.enrichSemanticDiffs(this.entries.map((e) => e.filePath), 'HEAD').catch((err) => { logger.debug('DiffPanel: semantic diff enrichment failed', { err }); });
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
/**
|
|
445
|
-
* Best-effort semantic-diff enrichment for a set of files, run against the
|
|
446
|
-
* before-content at `ref` and the current on-disk after-content. Self-contained
|
|
447
|
-
* diff plumbing, does not depend on the `/diff` command handler.
|
|
448
|
-
*/
|
|
449
|
-
private async enrichSemanticDiffs(files: string[], ref: string): Promise<void> {
|
|
450
|
-
if (files.length === 0) return;
|
|
451
|
-
const { computeSemanticDiff, formatSemanticDiffSummary } = await import('../renderer/semantic-diff.ts');
|
|
452
|
-
const { join, relative: pathRelative } = await import('path');
|
|
453
|
-
const repoRootProc = Bun.spawn(['git', 'rev-parse', '--show-toplevel'], { stdout: 'pipe', stderr: 'pipe', cwd: this.workingDirectory });
|
|
454
|
-
await repoRootProc.exited;
|
|
455
|
-
const repoRoot = (await new Response(repoRootProc.stdout).text()).trim() || this.workingDirectory;
|
|
456
|
-
await Promise.allSettled(
|
|
457
|
-
files.map(async (filePath) => {
|
|
458
|
-
try {
|
|
459
|
-
const absPath = filePath.startsWith('/') ? filePath : join(this.workingDirectory, filePath);
|
|
460
|
-
const repoRelPath = filePath.startsWith('/') ? pathRelative(repoRoot, filePath) : filePath;
|
|
461
|
-
const [beforeResult, afterResult] = await Promise.allSettled([
|
|
462
|
-
(async () => {
|
|
463
|
-
const proc = Bun.spawn(
|
|
464
|
-
['git', 'show', `${ref}:${repoRelPath}`],
|
|
465
|
-
{ stdout: 'pipe', stderr: 'pipe', cwd: repoRoot },
|
|
466
|
-
);
|
|
467
|
-
const [text, exitCode] = await Promise.all([new Response(proc.stdout).text(), proc.exited]);
|
|
468
|
-
if (exitCode !== 0) throw new Error(`git show failed for ${repoRelPath}`);
|
|
469
|
-
return text;
|
|
470
|
-
})(),
|
|
471
|
-
Bun.file(absPath).text(),
|
|
472
|
-
]);
|
|
473
|
-
if (beforeResult.status !== 'fulfilled' || afterResult.status !== 'fulfilled') return;
|
|
474
|
-
const semanticDiff = await computeSemanticDiff(filePath, beforeResult.value, afterResult.value);
|
|
475
|
-
if (!semanticDiff) return;
|
|
476
|
-
const summary = formatSemanticDiffSummary(semanticDiff);
|
|
477
|
-
if (summary) this.setSemanticSummary(filePath, summary);
|
|
478
|
-
} catch {
|
|
479
|
-
// best-effort only
|
|
480
|
-
}
|
|
481
|
-
}),
|
|
482
|
-
);
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
/**
|
|
486
|
-
* Attach or update the semantic diff summary for a file entry.
|
|
487
|
-
* No-op if the file isn't currently loaded. Safe to call from an async
|
|
488
|
-
* callback after the entry has already been replaced.
|
|
489
|
-
*/
|
|
490
|
-
setSemanticSummary(filePath: string, summary: string): void {
|
|
491
|
-
const entry = this.entries.find(e => e.filePath === filePath);
|
|
492
|
-
if (entry) {
|
|
493
|
-
entry.semanticSummary = summary;
|
|
494
|
-
this.markDirty();
|
|
495
|
-
}
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
/** Clear all diff entries. */
|
|
499
|
-
clear(): void {
|
|
500
|
-
this.entries = [];
|
|
501
|
-
this.selectedFile = 0;
|
|
502
|
-
this.scrollOffset = 0;
|
|
503
|
-
this.markDirty();
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
// -------------------------------------------------------------------------
|
|
507
|
-
// Lifecycle
|
|
508
|
-
// -------------------------------------------------------------------------
|
|
509
|
-
|
|
510
|
-
override onActivate(): void {
|
|
511
|
-
this.needsRender = true;
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
// -------------------------------------------------------------------------
|
|
515
|
-
// Input
|
|
516
|
-
// -------------------------------------------------------------------------
|
|
517
|
-
|
|
518
|
-
/**
|
|
519
|
-
* Wraps a self-load hotkey ('w'/'h'/'s') with visible before/after status
|
|
520
|
-
* text in the status bar, otherwise a reload whose content is unchanged
|
|
521
|
-
* from what's already shown is indistinguishable from the keypress having
|
|
522
|
-
* done nothing at all (contrast with `/diff working`'s ctx.print() calls).
|
|
523
|
-
*/
|
|
524
|
-
private runHotkeyReload(label: string, load: () => Promise<void>): void {
|
|
525
|
-
this.hotkeyStatus = `Loading ${label} diff…`;
|
|
526
|
-
this.markDirty();
|
|
527
|
-
void load().then(() => {
|
|
528
|
-
this.hotkeyStatus = `Reloaded ${label} diff.`;
|
|
529
|
-
this.markDirty();
|
|
530
|
-
this.requestRender();
|
|
531
|
-
});
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
handleInput(key: string): boolean {
|
|
535
|
-
if (this.confirmOverlay.handleInput(key)) return true; // confirm takes priority (git-panel.ts:284-303 precedent)
|
|
536
|
-
switch (key) {
|
|
537
|
-
case 'up': this.scrollUp(); return true;
|
|
538
|
-
case 'down': this.scrollDown(); return true;
|
|
539
|
-
case 'tab': this.nextFile(); return true;
|
|
540
|
-
// Shift+Tab: most terminals send the bare CSI-Z ("backtab") escape
|
|
541
|
-
// sequence rather than a Tab keypress with a shift modifier, and the
|
|
542
|
-
// input tokenizer passes that sequence through unchanged as the
|
|
543
|
-
// logical key name, so it (and the friendlier aliases some terminals/
|
|
544
|
-
// future tokenizer versions may emit) are matched explicitly here.
|
|
545
|
-
case '\x1b[Z':
|
|
546
|
-
case 'shift-tab':
|
|
547
|
-
case 'backtab': this.prevFile(); return true;
|
|
548
|
-
case 'pageup': this.scrollPageUp(); return true;
|
|
549
|
-
case 'pagedown': this.scrollPageDown(); return true;
|
|
550
|
-
case 'w': this.runHotkeyReload('working tree', () => this.showGitDiff()); return true;
|
|
551
|
-
case 'h': this.runHotkeyReload('HEAD', () => this.showGitDiff('HEAD')); return true;
|
|
552
|
-
case 's': this.runHotkeyReload('staged', () => this.showStagedDiff()); return true;
|
|
553
|
-
default: return false;
|
|
554
|
-
}
|
|
555
|
-
}
|
|
556
|
-
|
|
557
|
-
// (the purge): 'o' used to open the currently selected file in the
|
|
558
|
-
// preview panel via a staged pendingOpenPreview flag + a
|
|
559
|
-
// handlePanelIntegrationAction cross-panel hook (the same bridge
|
|
560
|
-
// FileExplorerPanel used). 'preview' is DELETE-disposition with no
|
|
561
|
-
// successor surface (no file-picker-overlay preview to repoint to
|
|
562
|
-
// either, verified), so the key and the hook were removed rather than
|
|
563
|
-
// repointed; diff no longer has an 'o' action.
|
|
564
|
-
|
|
565
|
-
private scrollUp(): void {
|
|
566
|
-
if (this.scrollOffset > 0) {
|
|
567
|
-
this.scrollOffset--;
|
|
568
|
-
this.markDirty();
|
|
569
|
-
}
|
|
570
|
-
}
|
|
571
|
-
|
|
572
|
-
private scrollDown(): void {
|
|
573
|
-
const entry = this.currentEntry();
|
|
574
|
-
if (!entry) return;
|
|
575
|
-
const max = Math.max(0, entry.lines.length - 1);
|
|
576
|
-
if (this.scrollOffset < max) {
|
|
577
|
-
this.scrollOffset++;
|
|
578
|
-
this.markDirty();
|
|
579
|
-
}
|
|
580
|
-
}
|
|
581
|
-
|
|
582
|
-
private scrollPageUp(): void {
|
|
583
|
-
this.scrollOffset = Math.max(0, this.scrollOffset - 20);
|
|
584
|
-
this.markDirty();
|
|
585
|
-
}
|
|
586
|
-
|
|
587
|
-
private scrollPageDown(): void {
|
|
588
|
-
const entry = this.currentEntry();
|
|
589
|
-
if (!entry) return;
|
|
590
|
-
const max = Math.max(0, entry.lines.length - 1);
|
|
591
|
-
this.scrollOffset = Math.min(max, this.scrollOffset + 20);
|
|
592
|
-
this.markDirty();
|
|
593
|
-
}
|
|
594
|
-
|
|
595
|
-
private nextFile(): void {
|
|
596
|
-
if (this.entries.length === 0) return;
|
|
597
|
-
this.selectedFile = (this.selectedFile + 1) % this.entries.length;
|
|
598
|
-
this.scrollOffset = 0;
|
|
599
|
-
this.markDirty();
|
|
600
|
-
}
|
|
601
|
-
|
|
602
|
-
private prevFile(): void {
|
|
603
|
-
if (this.entries.length === 0) return;
|
|
604
|
-
this.selectedFile = (this.selectedFile - 1 + this.entries.length) % this.entries.length;
|
|
605
|
-
this.scrollOffset = 0;
|
|
606
|
-
this.markDirty();
|
|
607
|
-
}
|
|
608
|
-
|
|
609
|
-
private currentEntry(): DiffEntry | null {
|
|
610
|
-
return this.entries[this.selectedFile] ?? null;
|
|
611
|
-
}
|
|
612
|
-
|
|
613
|
-
// -------------------------------------------------------------------------
|
|
614
|
-
// Render
|
|
615
|
-
// -------------------------------------------------------------------------
|
|
616
|
-
|
|
617
|
-
render(width: number, height: number): Line[] {
|
|
618
|
-
return this.trackedRender(() => {
|
|
619
|
-
if (height <= 0 || width <= 0) return [];
|
|
620
|
-
|
|
621
|
-
const confirmLines = this.confirmOverlay.renderLines(width);
|
|
622
|
-
if (confirmLines) return buildPanelWorkspace(width, height, { title: 'Diff Workspace', palette: COLOR, sections: [{ title: 'Confirmation', lines: confirmLines }] });
|
|
623
|
-
if (this.entries.length === 0) {
|
|
624
|
-
return buildPanelWorkspace(width, height, {
|
|
625
|
-
title: 'Diff Workspace',
|
|
626
|
-
palette: COLOR,
|
|
627
|
-
sections: [{
|
|
628
|
-
title: 'Diff',
|
|
629
|
-
lines: buildEmptyState(
|
|
630
|
-
width,
|
|
631
|
-
' No diff to display.',
|
|
632
|
-
'Load a git diff or select a changed file to populate the workspace. w=working h=HEAD s=staged self-load right here.',
|
|
633
|
-
// corrected, bare /diff loads files changed *this
|
|
634
|
-
// session* (falling back to the full HEAD diff when none are
|
|
635
|
-
// tracked yet), not the working-tree diff the w key already
|
|
636
|
-
// loads in-panel; the old summary text described the wrong
|
|
637
|
-
// command.
|
|
638
|
-
[{ command: '/diff', summary: 'load files changed this session into the diff workspace (falls back to the HEAD diff if none yet)' }],
|
|
639
|
-
COLOR,
|
|
640
|
-
),
|
|
641
|
-
}],
|
|
642
|
-
});
|
|
643
|
-
}
|
|
644
|
-
|
|
645
|
-
const entry = this.currentEntry();
|
|
646
|
-
if (!entry) {
|
|
647
|
-
return Array.from({ length: height }, () => createEmptyLine(width));
|
|
648
|
-
}
|
|
649
|
-
|
|
650
|
-
const compact = height <= 12;
|
|
651
|
-
const summaryLines = entry.semanticSummary
|
|
652
|
-
? buildBodyText(width, `Semantic summary: ${entry.semanticSummary}`, COLOR, COLOR.context)
|
|
653
|
-
: [];
|
|
654
|
-
const previewSection = resolveScrollablePanelSection(width, height, {
|
|
655
|
-
palette: COLOR,
|
|
656
|
-
footerLines: [this.renderStatusBar(width, entry)],
|
|
657
|
-
beforeSections: [
|
|
658
|
-
{
|
|
659
|
-
title: compact ? undefined : 'Files',
|
|
660
|
-
lines: [
|
|
661
|
-
this.renderTabBar(width),
|
|
662
|
-
...summaryLines,
|
|
663
|
-
],
|
|
664
|
-
},
|
|
665
|
-
],
|
|
666
|
-
section: {
|
|
667
|
-
title: compact ? undefined : 'Changes',
|
|
668
|
-
scrollableLines: entry.lines.map((pl) => this.renderParsedLine(pl, width)),
|
|
669
|
-
scrollOffset: this.scrollOffset,
|
|
670
|
-
minRows: 1,
|
|
671
|
-
},
|
|
672
|
-
});
|
|
673
|
-
this.scrollOffset = previewSection.scrollOffset;
|
|
674
|
-
|
|
675
|
-
const sections: PanelWorkspaceSection[] = [
|
|
676
|
-
{
|
|
677
|
-
title: compact ? undefined : 'Files',
|
|
678
|
-
lines: [
|
|
679
|
-
this.renderTabBar(width),
|
|
680
|
-
...summaryLines,
|
|
681
|
-
],
|
|
682
|
-
},
|
|
683
|
-
{
|
|
684
|
-
title: previewSection.section.title,
|
|
685
|
-
lines: previewSection.section.lines,
|
|
686
|
-
},
|
|
687
|
-
];
|
|
688
|
-
return buildPanelWorkspace(width, height, {
|
|
689
|
-
title: 'Diff Workspace',
|
|
690
|
-
palette: COLOR,
|
|
691
|
-
sections,
|
|
692
|
-
footerLines: [this.renderStatusBar(width, entry)],
|
|
693
|
-
});
|
|
694
|
-
});
|
|
695
|
-
}
|
|
696
|
-
|
|
697
|
-
// ── Tab bar ──────────────────────────────────────────────────────────────
|
|
698
|
-
|
|
699
|
-
private renderTabBar(width: number): Line {
|
|
700
|
-
const cells: Line = [];
|
|
701
|
-
const push = (ch: string, fg: string, bg: string, bold = false): void => {
|
|
702
|
-
const cw = getDisplayWidth(ch);
|
|
703
|
-
if (cells.length + cw > width) return;
|
|
704
|
-
cells.push(createStyledCell(ch, { fg, bg, bold }));
|
|
705
|
-
if (cw === 2 && cells.length < width) cells.push(createStyledCell('', { fg, bg }));
|
|
706
|
-
};
|
|
707
|
-
|
|
708
|
-
for (let i = 0; i < this.entries.length; i++) {
|
|
709
|
-
const entry = this.entries[i]!;
|
|
710
|
-
const active = i === this.selectedFile;
|
|
711
|
-
const placeholder = isPlaceholderPath(entry.filePath);
|
|
712
|
-
const stat = diffStat(entry);
|
|
713
|
-
// Placeholder entries ('(error)', '(no changes)', ...) get a distinct
|
|
714
|
-
// warn color regardless of active/inactive, they are not a real diff
|
|
715
|
-
// of a real file and must not read as one.
|
|
716
|
-
const fg = placeholder ? COLOR.warn : (active ? COLOR.tabActive : COLOR.tabInactive);
|
|
717
|
-
const bg = active ? COLOR.tabActiveBg : COLOR.tabBg;
|
|
718
|
-
// Active file gets a leading marker; every tab shows +adds/-dels at a glance.
|
|
719
|
-
const marker = active ? '▸ ' : (placeholder ? '⚠ ' : ' ');
|
|
720
|
-
const label = `${marker}${basename(entry.filePath)} `;
|
|
721
|
-
for (const ch of label) push(ch, fg, bg, active);
|
|
722
|
-
if (stat.added > 0) for (const ch of `+${stat.added}`) push(ch, COLOR.addition, bg, active);
|
|
723
|
-
if (stat.added > 0 && stat.removed > 0) push(' ', fg, bg);
|
|
724
|
-
if (stat.removed > 0) for (const ch of `-${stat.removed}`) push(ch, COLOR.deletion, bg, active);
|
|
725
|
-
push(' ', fg, bg);
|
|
726
|
-
push('│', COLOR.lineNum, COLOR.tabBg);
|
|
727
|
-
}
|
|
728
|
-
|
|
729
|
-
while (cells.length < width) {
|
|
730
|
-
cells.push(createStyledCell(' ', { fg: '', bg: COLOR.tabBg }));
|
|
731
|
-
}
|
|
732
|
-
return cells.slice(0, width);
|
|
733
|
-
}
|
|
734
|
-
|
|
735
|
-
// ── Status bar ───────────────────────────────────────────────────────────
|
|
736
|
-
|
|
737
|
-
private renderStatusBar(width: number, entry: DiffEntry | null): Line {
|
|
738
|
-
if (!entry) {
|
|
739
|
-
return buildStyledPanelLine(width, [{ text: ' No file', fg: COLOR.tabInactive, bg: COLOR.statusBar }], { fillBg: COLOR.statusBar });
|
|
740
|
-
}
|
|
741
|
-
const stat = diffStat(entry);
|
|
742
|
-
const placeholder = isPlaceholderPath(entry.filePath);
|
|
743
|
-
// Keep the file path display-width-aware so a long/wide path can't overflow.
|
|
744
|
-
const pathBudget = Math.max(8, Math.floor(width / 2));
|
|
745
|
-
const fileInfo = truncateDisplay(entry.filePath, pathBudget);
|
|
746
|
-
const segments: Array<{ text: string; fg: string; bg?: string; bold?: boolean }> = [
|
|
747
|
-
// Placeholder states ('(error)', '(no changes)', ...) are not a real
|
|
748
|
-
// diff of a real file, a distinct warn color keeps them from being
|
|
749
|
-
// mistaken for one.
|
|
750
|
-
{ text: ` ${fileInfo} `, fg: placeholder ? COLOR.warn : COLOR.filename, bg: COLOR.statusBar, bold: true },
|
|
751
|
-
{ text: `[${this.selectedFile + 1}/${this.entries.length}]`, fg: COLOR.tabInactive, bg: COLOR.statusBar },
|
|
752
|
-
{ text: ' +', fg: COLOR.tabInactive, bg: COLOR.statusBar },
|
|
753
|
-
{ text: String(stat.added), fg: COLOR.addition, bg: COLOR.statusBar },
|
|
754
|
-
{ text: ' -', fg: COLOR.tabInactive, bg: COLOR.statusBar },
|
|
755
|
-
{ text: String(stat.removed), fg: COLOR.deletion, bg: COLOR.statusBar },
|
|
756
|
-
{ text: ` L${this.scrollOffset + 1}/${entry.lines.length}`, fg: COLOR.tabInactive, bg: COLOR.statusBar },
|
|
757
|
-
{ text: ' Tab/S-Tab file o open w/h/s load ↑/↓ scroll', fg: COLOR.context, bg: COLOR.statusBar },
|
|
758
|
-
];
|
|
759
|
-
if (entry.semanticSummary) {
|
|
760
|
-
segments.push({ text: ` ◈ ${entry.semanticSummary}`, fg: COLOR.hunk, bg: COLOR.statusBar });
|
|
761
|
-
}
|
|
762
|
-
if (this.hotkeyStatus) {
|
|
763
|
-
segments.push({ text: ` ${this.hotkeyStatus}`, fg: COLOR.info, bg: COLOR.statusBar, bold: true });
|
|
764
|
-
}
|
|
765
|
-
return buildStyledPanelLine(width, segments, { fillBg: COLOR.statusBar });
|
|
766
|
-
}
|
|
767
|
-
|
|
768
|
-
// ── Parsed line ──────────────────────────────────────────────────────────
|
|
769
|
-
|
|
770
|
-
private renderParsedLine(pl: ParsedLine, width: number): Line {
|
|
771
|
-
const left = pl.beforeNum !== null ? String(pl.beforeNum) : '';
|
|
772
|
-
const right = pl.afterNum !== null ? String(pl.afterNum) : '';
|
|
773
|
-
|
|
774
|
-
switch (pl.kind) {
|
|
775
|
-
case 'addition':
|
|
776
|
-
return makeLine(width, left, right, `+ ${pl.text}`, COLOR.addition, COLOR.additionBg, COLOR.lineNumAdd, true);
|
|
777
|
-
case 'deletion':
|
|
778
|
-
return makeLine(width, left, right, `- ${pl.text}`, COLOR.deletion, COLOR.deletionBg, COLOR.lineNumDel, false);
|
|
779
|
-
case 'hunk':
|
|
780
|
-
return renderText(width, pl.text, COLOR.hunk, COLOR.hunkBg, false);
|
|
781
|
-
case 'header':
|
|
782
|
-
return renderText(width, pl.text, COLOR.markerText, '', false);
|
|
783
|
-
case 'context':
|
|
784
|
-
default:
|
|
785
|
-
return makeLine(width, left, right, ` ${pl.text}`, COLOR.context, '', COLOR.lineNum, false);
|
|
786
|
-
}
|
|
787
|
-
}
|
|
788
|
-
}
|
|
789
|
-
|
|
790
|
-
// ---------------------------------------------------------------------------
|
|
791
|
-
// Helpers
|
|
792
|
-
// ---------------------------------------------------------------------------
|
|
793
|
-
|
|
794
|
-
function basename(p: string): string {
|
|
795
|
-
const parts = p.replace(/\\/g, '/').split('/');
|
|
796
|
-
return parts[parts.length - 1] ?? p;
|
|
797
|
-
}
|