@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
package/src/panels/git-panel.ts
DELETED
|
@@ -1,800 +0,0 @@
|
|
|
1
|
-
import { BasePanel } from './base-panel.ts';
|
|
2
|
-
import { createEmptyLine, createStyledCell, type Line } from '@pellux/goodvibes-sdk/platform/types';
|
|
3
|
-
import { truncateDisplay, getDisplayWidth } from '../utils/terminal-width.ts';
|
|
4
|
-
import { GitService } from '@pellux/goodvibes-sdk/platform/git';
|
|
5
|
-
import { logger } from '@pellux/goodvibes-sdk/platform/utils';
|
|
6
|
-
import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
|
|
7
|
-
import { UI_TONES, DIFF_TONES } from '../renderer/ui-primitives.ts';
|
|
8
|
-
import {
|
|
9
|
-
buildEmptyState,
|
|
10
|
-
buildKeyboardHints,
|
|
11
|
-
buildPanelLine,
|
|
12
|
-
buildPanelWorkspace,
|
|
13
|
-
buildSearchInputLine,
|
|
14
|
-
resolveScrollablePanelSection,
|
|
15
|
-
buildSelectablePanelLine,
|
|
16
|
-
buildStyledPanelLine,
|
|
17
|
-
DEFAULT_PANEL_PALETTE,
|
|
18
|
-
extendPalette,
|
|
19
|
-
} from './polish.ts';
|
|
20
|
-
import { type ConfirmState, handleConfirmInput, renderConfirmLines } from './confirm-state.ts';
|
|
21
|
-
import {
|
|
22
|
-
isPanelSearchBackspace,
|
|
23
|
-
isPanelSearchCancel,
|
|
24
|
-
isPanelSearchCommit,
|
|
25
|
-
isPanelSearchPrintable,
|
|
26
|
-
} from './search-focus.ts';
|
|
27
|
-
|
|
28
|
-
// Types
|
|
29
|
-
|
|
30
|
-
interface GitFileEntry {
|
|
31
|
-
path: string;
|
|
32
|
-
staged: boolean;
|
|
33
|
-
/** True when this file was touched during the current session (best-effort). */
|
|
34
|
-
sessionChanged?: boolean;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
interface CommitEntry {
|
|
38
|
-
hash: string;
|
|
39
|
-
message: string;
|
|
40
|
-
author: string;
|
|
41
|
-
date: string;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
interface GitData {
|
|
45
|
-
branch: string;
|
|
46
|
-
ahead: number;
|
|
47
|
-
behind: number;
|
|
48
|
-
stagedFiles: GitFileEntry[];
|
|
49
|
-
unstagedFiles: GitFileEntry[];
|
|
50
|
-
recentCommits: CommitEntry[];
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
type ViewItem =
|
|
54
|
-
| { kind: 'header' }
|
|
55
|
-
| { kind: 'section'; label: string }
|
|
56
|
-
| { kind: 'file'; entry: GitFileEntry }
|
|
57
|
-
| { kind: 'commit'; entry: CommitEntry }
|
|
58
|
-
| { kind: 'empty'; label: string };
|
|
59
|
-
|
|
60
|
-
/** Subject carried by the panel's single ConfirmState, either a repo init or a commit. */
|
|
61
|
-
type GitConfirmSubject =
|
|
62
|
-
| { kind: 'init' }
|
|
63
|
-
| { kind: 'commit'; message: string };
|
|
64
|
-
|
|
65
|
-
// Constants
|
|
66
|
-
|
|
67
|
-
/** Minimum number of diff lines kept visible when clamping scroll offset. */
|
|
68
|
-
const MIN_VISIBLE_DIFF_LINES = 5;
|
|
69
|
-
|
|
70
|
-
// Colors
|
|
71
|
-
|
|
72
|
-
const C = extendPalette(DEFAULT_PANEL_PALETTE, {
|
|
73
|
-
branch: '#00d7ff',
|
|
74
|
-
sectionHeader: '244',
|
|
75
|
-
commit: '250',
|
|
76
|
-
commitHash: '238',
|
|
77
|
-
commitAuthor: '244',
|
|
78
|
-
selected: '#1c1c1c',
|
|
79
|
-
selectedFg: '#ffffff',
|
|
80
|
-
diffMeta: DIFF_TONES.hunk, // shared diff-hunk token, was a local literal
|
|
81
|
-
diffNeutral: '250',
|
|
82
|
-
// Reuses the existing workflow accent token rather than adding a new hex
|
|
83
|
-
// literal (architecture gate ratchets the raw-hex-literal count).
|
|
84
|
-
sessionChanged: UI_TONES.accent.workflow,
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
// GitPanel
|
|
88
|
-
|
|
89
|
-
export class GitPanel extends BasePanel {
|
|
90
|
-
private readonly workingDirectory: string;
|
|
91
|
-
private readonly git: GitService;
|
|
92
|
-
private readonly getChangedFiles?: () => readonly string[];
|
|
93
|
-
|
|
94
|
-
private data: GitData = {
|
|
95
|
-
branch: '...',
|
|
96
|
-
ahead: 0,
|
|
97
|
-
behind: 0,
|
|
98
|
-
stagedFiles: [],
|
|
99
|
-
unstagedFiles: [],
|
|
100
|
-
recentCommits: [],
|
|
101
|
-
};
|
|
102
|
-
|
|
103
|
-
/** Flattened list of navigable rows (for arrow-key movement). */
|
|
104
|
-
private items: ViewItem[] = [];
|
|
105
|
-
|
|
106
|
-
/** Selected row index within `items`. */
|
|
107
|
-
private selectedIndex = 0;
|
|
108
|
-
|
|
109
|
-
/** When truthy, shows the diff for the selected file or commit. */
|
|
110
|
-
private expandedDiff: string[] | null = null;
|
|
111
|
-
|
|
112
|
-
/** Scroll offset for both main view and diff view. */
|
|
113
|
-
private scrollOffset = 0;
|
|
114
|
-
|
|
115
|
-
private refreshTimerId: ReturnType<typeof setInterval> | null = null;
|
|
116
|
-
private loading = true;
|
|
117
|
-
private error: string | null = null;
|
|
118
|
-
|
|
119
|
-
/** True when the last refresh failed because `workingDirectory` isn't a git repo. */
|
|
120
|
-
private notGitRepo = false;
|
|
121
|
-
|
|
122
|
-
/** Single pending confirm, either "init this repo" or "commit with this message". */
|
|
123
|
-
private confirm: ConfirmState<GitConfirmSubject> | null = null;
|
|
124
|
-
|
|
125
|
-
/** Non-null while composing a commit message; null means the compose UI is closed. */
|
|
126
|
-
private commitMessage: string | null = null;
|
|
127
|
-
|
|
128
|
-
constructor(
|
|
129
|
-
workingDirectory: string,
|
|
130
|
-
private readonly requestRender: () => void = () => {},
|
|
131
|
-
getChangedFiles?: () => readonly string[],
|
|
132
|
-
) {
|
|
133
|
-
super('git', 'Git', 'G', 'development');
|
|
134
|
-
this.workingDirectory = workingDirectory;
|
|
135
|
-
// I2: reuse a single GitService instance across the panel's lifetime instead
|
|
136
|
-
// of constructing a new one for every refresh/diff/stage/commit call.
|
|
137
|
-
this.git = new GitService(workingDirectory);
|
|
138
|
-
this.getChangedFiles = getChangedFiles;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
private _markDirtyAndRender(): void {
|
|
142
|
-
this.markDirty();
|
|
143
|
-
this.requestRender();
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
// Lifecycle
|
|
147
|
-
|
|
148
|
-
override onActivate(): void {
|
|
149
|
-
super.onActivate();
|
|
150
|
-
void this.refresh();
|
|
151
|
-
this.refreshTimerId = this.registerTimer(setInterval(() => {
|
|
152
|
-
void this.refresh();
|
|
153
|
-
}, 5_000));
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
override onDeactivate(): void {
|
|
157
|
-
if (this.refreshTimerId !== null) {
|
|
158
|
-
this.clearTimer(this.refreshTimerId);
|
|
159
|
-
this.refreshTimerId = null;
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
override onDestroy(): void {
|
|
164
|
-
this.onDeactivate();
|
|
165
|
-
super.onDestroy();
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
// Data fetching
|
|
169
|
-
|
|
170
|
-
private async refresh(): Promise<void> {
|
|
171
|
-
try {
|
|
172
|
-
const [statusResult, branchResult, logEntries] = await Promise.all([
|
|
173
|
-
this.git.status(),
|
|
174
|
-
this.git.branch(),
|
|
175
|
-
this.git.log(10),
|
|
176
|
-
]);
|
|
177
|
-
|
|
178
|
-
const changed = this.getChangedFiles ? new Set(this.getChangedFiles()) : null;
|
|
179
|
-
const markChanged = (path: string): boolean | undefined => (changed ? changed.has(path) : undefined);
|
|
180
|
-
|
|
181
|
-
// Classify from the raw per-file porcelain columns (index/working_dir), not simple-git's
|
|
182
|
-
// modified/staged arrays: those double-count a staged-with-no-further-edits file (index 'M',
|
|
183
|
-
// working_dir ' ') as both staged AND unstaged, landing 's'/'u' on a phantom row.
|
|
184
|
-
const stagedFiles: GitFileEntry[] = [];
|
|
185
|
-
const unstagedFiles: GitFileEntry[] = [];
|
|
186
|
-
for (const f of statusResult.files) {
|
|
187
|
-
const sessionChanged = markChanged(f.path);
|
|
188
|
-
if (f.index === '?' && f.working_dir === '?') {
|
|
189
|
-
unstagedFiles.push({ path: f.path, staged: false, sessionChanged }); // untracked
|
|
190
|
-
continue;
|
|
191
|
-
}
|
|
192
|
-
if (f.index !== ' ') stagedFiles.push({ path: f.path, staged: true, sessionChanged });
|
|
193
|
-
if (f.working_dir !== ' ') unstagedFiles.push({ path: f.path, staged: false, sessionChanged });
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
this.data = {
|
|
197
|
-
branch: branchResult.current || 'HEAD',
|
|
198
|
-
ahead: statusResult.ahead ?? 0,
|
|
199
|
-
behind: statusResult.behind ?? 0,
|
|
200
|
-
stagedFiles,
|
|
201
|
-
unstagedFiles,
|
|
202
|
-
recentCommits: logEntries.map((e) => ({
|
|
203
|
-
hash: e.hash.slice(0, 7),
|
|
204
|
-
message: e.message,
|
|
205
|
-
author: e.author,
|
|
206
|
-
date: e.date,
|
|
207
|
-
})),
|
|
208
|
-
};
|
|
209
|
-
|
|
210
|
-
this.loading = false;
|
|
211
|
-
this.error = null;
|
|
212
|
-
this.notGitRepo = false;
|
|
213
|
-
this.rebuildItems();
|
|
214
|
-
// Do not clear expandedDiff during auto-refresh, only clear on explicit user action
|
|
215
|
-
this._markDirtyAndRender();
|
|
216
|
-
} catch (err) {
|
|
217
|
-
const msg = summarizeError(err);
|
|
218
|
-
if (/not a git\b/i.test(msg)) {
|
|
219
|
-
// I4: no more auto `git init` side effect, surface the state and let
|
|
220
|
-
// the user explicitly confirm initialisation with 'i'.
|
|
221
|
-
this.notGitRepo = true;
|
|
222
|
-
this.error = 'Not a git repository here. Press i to initialize one (explicit confirm).';
|
|
223
|
-
} else {
|
|
224
|
-
this.notGitRepo = false;
|
|
225
|
-
this.error = msg;
|
|
226
|
-
}
|
|
227
|
-
this.loading = false;
|
|
228
|
-
logger.debug('GitPanel: refresh failed', { error: this.error });
|
|
229
|
-
this._markDirtyAndRender();
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
/** Push a labeled section (with count) plus its rows, or an empty-row placeholder. */
|
|
234
|
-
private pushSection<T>(items: ViewItem[], label: string, entries: readonly T[], emptyLabel: string, toItem: (entry: T) => ViewItem): void {
|
|
235
|
-
items.push({ kind: 'section', label: `${label} (${entries.length})` });
|
|
236
|
-
if (entries.length === 0) items.push({ kind: 'empty', label: emptyLabel });
|
|
237
|
-
else for (const entry of entries) items.push(toItem(entry));
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
/** Rebuild the flat navigable item list from current data. */
|
|
241
|
-
private rebuildItems(): void {
|
|
242
|
-
const items: ViewItem[] = [{ kind: 'header' }];
|
|
243
|
-
this.pushSection(items, 'Staged', this.data.stagedFiles, ' (no staged files)', (entry) => ({ kind: 'file', entry }));
|
|
244
|
-
this.pushSection(items, 'Unstaged', this.data.unstagedFiles, ' (no unstaged files)', (entry) => ({ kind: 'file', entry }));
|
|
245
|
-
this.pushSection(items, 'Recent Commits', this.data.recentCommits, ' (no commits)', (entry) => ({ kind: 'commit', entry }));
|
|
246
|
-
this.items = items;
|
|
247
|
-
|
|
248
|
-
// Keep selection in bounds and off header/section/empty filler rows (I5).
|
|
249
|
-
if (this.selectedIndex >= this.items.length) {
|
|
250
|
-
this.selectedIndex = Math.max(0, this.items.length - 1);
|
|
251
|
-
}
|
|
252
|
-
if (!this.isSelectable(this.items.at(this.selectedIndex))) {
|
|
253
|
-
const firstSelectable = this.items.findIndex((it) => this.isSelectable(it));
|
|
254
|
-
if (firstSelectable >= 0) this.selectedIndex = firstSelectable;
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
/** True for rows the cursor is allowed to rest on (file or commit rows). */
|
|
259
|
-
private isSelectable(item: ViewItem | undefined): boolean {
|
|
260
|
-
return item?.kind === 'file' || item?.kind === 'commit';
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
/** Move the selection to the next/previous selectable row, skipping filler rows. */
|
|
264
|
-
private moveSelection(direction: 1 | -1): void {
|
|
265
|
-
let idx = this.selectedIndex;
|
|
266
|
-
for (let i = 0; i < this.items.length; i++) {
|
|
267
|
-
idx += direction;
|
|
268
|
-
if (idx < 0 || idx >= this.items.length) return;
|
|
269
|
-
if (this.isSelectable(this.items[idx])) {
|
|
270
|
-
this.selectedIndex = idx;
|
|
271
|
-
this.markDirty();
|
|
272
|
-
return;
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
// Input handling
|
|
278
|
-
|
|
279
|
-
/** Commit-message entry wants every char of a burst delivered one at a time, see Panel.isCapturingTextBurst. */
|
|
280
|
-
isCapturingTextBurst(): boolean {
|
|
281
|
-
return this.commitMessage !== null;
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
handleInput(key: string): boolean {
|
|
285
|
-
// Confirm (init repo / commit) takes priority over everything else.
|
|
286
|
-
const confirmResult = handleConfirmInput(this.confirm, key);
|
|
287
|
-
if (confirmResult === 'confirmed') {
|
|
288
|
-
const subject = this.confirm!.subject;
|
|
289
|
-
this.confirm = null;
|
|
290
|
-
if (subject.kind === 'init') {
|
|
291
|
-
void this.performInit();
|
|
292
|
-
} else {
|
|
293
|
-
void this.performCommit(subject.message);
|
|
294
|
-
}
|
|
295
|
-
this.markDirty();
|
|
296
|
-
return true;
|
|
297
|
-
}
|
|
298
|
-
if (confirmResult === 'cancelled') {
|
|
299
|
-
this.confirm = null;
|
|
300
|
-
this.markDirty();
|
|
301
|
-
return true;
|
|
302
|
-
}
|
|
303
|
-
if (confirmResult === 'absorbed') return true;
|
|
304
|
-
|
|
305
|
-
if (this.commitMessage !== null) {
|
|
306
|
-
return this.handleCommitMessageInput(key);
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
if (this.expandedDiff !== null) {
|
|
310
|
-
return this.handleDiffInput(key);
|
|
311
|
-
}
|
|
312
|
-
return this.handleListInput(key);
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
private handleListInput(key: string): boolean {
|
|
316
|
-
switch (key) {
|
|
317
|
-
case 'up':
|
|
318
|
-
case 'k': {
|
|
319
|
-
this.moveSelection(-1);
|
|
320
|
-
return true;
|
|
321
|
-
}
|
|
322
|
-
case 'down':
|
|
323
|
-
case 'j': {
|
|
324
|
-
this.moveSelection(1);
|
|
325
|
-
return true;
|
|
326
|
-
}
|
|
327
|
-
case 'return': {
|
|
328
|
-
void this.openDiff();
|
|
329
|
-
return true;
|
|
330
|
-
}
|
|
331
|
-
case 'r': {
|
|
332
|
-
void this.refresh();
|
|
333
|
-
return true;
|
|
334
|
-
}
|
|
335
|
-
case 's': {
|
|
336
|
-
void this.setStaged(true);
|
|
337
|
-
return true;
|
|
338
|
-
}
|
|
339
|
-
case 'u': {
|
|
340
|
-
void this.setStaged(false);
|
|
341
|
-
return true;
|
|
342
|
-
}
|
|
343
|
-
case 'c': {
|
|
344
|
-
this.commitMessage = '';
|
|
345
|
-
this.markDirty();
|
|
346
|
-
return true;
|
|
347
|
-
}
|
|
348
|
-
case 'i': {
|
|
349
|
-
// Only meaningful when no repo is loaded, otherwise leave the key
|
|
350
|
-
// unconsumed instead of absorbing it as a silent no-op.
|
|
351
|
-
if (!this.notGitRepo) return false;
|
|
352
|
-
this.confirm = { subject: { kind: 'init' }, label: this.workingDirectory, verb: 'Init' };
|
|
353
|
-
this.markDirty();
|
|
354
|
-
return true;
|
|
355
|
-
}
|
|
356
|
-
default:
|
|
357
|
-
return false;
|
|
358
|
-
}
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
private handleCommitMessageInput(key: string): boolean {
|
|
362
|
-
if (isPanelSearchCancel(key)) {
|
|
363
|
-
this.commitMessage = null;
|
|
364
|
-
this.markDirty();
|
|
365
|
-
return true;
|
|
366
|
-
}
|
|
367
|
-
if (isPanelSearchCommit(key)) {
|
|
368
|
-
const message = (this.commitMessage ?? '').trim();
|
|
369
|
-
if (!message) return true; // absorb, require a non-empty message before confirming
|
|
370
|
-
this.confirm = { subject: { kind: 'commit', message }, label: message, verb: 'Commit' };
|
|
371
|
-
this.commitMessage = null;
|
|
372
|
-
this.markDirty();
|
|
373
|
-
return true;
|
|
374
|
-
}
|
|
375
|
-
if (isPanelSearchBackspace(key)) {
|
|
376
|
-
this.commitMessage = (this.commitMessage ?? '').slice(0, -1);
|
|
377
|
-
this.markDirty();
|
|
378
|
-
return true;
|
|
379
|
-
}
|
|
380
|
-
if (isPanelSearchPrintable(key)) {
|
|
381
|
-
this.commitMessage = (this.commitMessage ?? '') + key;
|
|
382
|
-
this.markDirty();
|
|
383
|
-
return true;
|
|
384
|
-
}
|
|
385
|
-
return true; // absorb every other key while composing
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
private handleDiffInput(key: string): boolean {
|
|
389
|
-
switch (key) {
|
|
390
|
-
case 'up':
|
|
391
|
-
case 'k': {
|
|
392
|
-
if (this.scrollOffset > 0) {
|
|
393
|
-
this.scrollOffset--;
|
|
394
|
-
this.markDirty();
|
|
395
|
-
}
|
|
396
|
-
return true;
|
|
397
|
-
}
|
|
398
|
-
case 'down':
|
|
399
|
-
case 'j': {
|
|
400
|
-
const diffLen = this.expandedDiff?.length ?? 0;
|
|
401
|
-
this.scrollOffset = Math.min(this.scrollOffset + 1, Math.max(0, diffLen - MIN_VISIBLE_DIFF_LINES));
|
|
402
|
-
this.markDirty();
|
|
403
|
-
return true;
|
|
404
|
-
}
|
|
405
|
-
case 'escape':
|
|
406
|
-
case 'q': {
|
|
407
|
-
this.expandedDiff = null;
|
|
408
|
-
this.scrollOffset = 0;
|
|
409
|
-
this.markDirty();
|
|
410
|
-
return true;
|
|
411
|
-
}
|
|
412
|
-
default:
|
|
413
|
-
return false;
|
|
414
|
-
}
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
/** Commit the given lines as the new expanded diff and reset scroll/dirty state. */
|
|
418
|
-
private finishDiff(lines: string[]): void {
|
|
419
|
-
this.expandedDiff = lines;
|
|
420
|
-
this.scrollOffset = 0;
|
|
421
|
-
this.markDirty();
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
// I3: withLoading guarantees the spinner is cleared even if the fetch throws.
|
|
425
|
-
private async openDiff(): Promise<void> {
|
|
426
|
-
const item = this.items.at(this.selectedIndex);
|
|
427
|
-
if (!item) return;
|
|
428
|
-
|
|
429
|
-
if (item.kind === 'file') {
|
|
430
|
-
try {
|
|
431
|
-
const raw = await this.withLoading('Loading diff…', async () => this.git.diffFile(item.entry.path, item.entry.staged));
|
|
432
|
-
this.finishDiff(raw ? raw.split('\n') : ['(no diff available)']);
|
|
433
|
-
} catch (err) {
|
|
434
|
-
this.finishDiff([`Error: ${summarizeError(err)}`]);
|
|
435
|
-
}
|
|
436
|
-
return;
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
if (item.kind === 'commit') {
|
|
440
|
-
// Root commits have no parent (`<hash>^` is invalid), the catch below
|
|
441
|
-
// surfaces that instead of pretending a diff exists.
|
|
442
|
-
const before = `${item.entry.hash}^`;
|
|
443
|
-
const after = item.entry.hash;
|
|
444
|
-
try {
|
|
445
|
-
const [patch, stat] = await this.withLoading('Loading commit…', async () => Promise.all([
|
|
446
|
-
this.git.diffBetween(before, after),
|
|
447
|
-
this.git.diffStat(before, after),
|
|
448
|
-
]));
|
|
449
|
-
const statLines = stat.split('\n').filter((l) => l.trim().length > 0);
|
|
450
|
-
const patchLines = patch ? patch.split('\n') : [];
|
|
451
|
-
this.finishDiff(statLines.length > 0 ? [...statLines, '', ...patchLines] : (patchLines.length > 0 ? patchLines : ['(no diff available)']));
|
|
452
|
-
} catch (err) {
|
|
453
|
-
this.finishDiff([`Error: ${summarizeError(err)}`]);
|
|
454
|
-
}
|
|
455
|
-
}
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
// -- Mutating actions: stage / unstage / commit / init ----------------------
|
|
459
|
-
|
|
460
|
-
/** Stage (target=true) or unstage (target=false) the selected file row. */
|
|
461
|
-
private async setStaged(target: boolean): Promise<void> {
|
|
462
|
-
const item = this.items.at(this.selectedIndex);
|
|
463
|
-
if (!item || item.kind !== 'file' || item.entry.staged === target) return;
|
|
464
|
-
try {
|
|
465
|
-
if (target) await this.git.add(item.entry.path);
|
|
466
|
-
else await this.git.reset(item.entry.path);
|
|
467
|
-
await this.refresh();
|
|
468
|
-
} catch (err) {
|
|
469
|
-
this.error = `${target ? 'Stage' : 'Unstage'} failed: ${summarizeError(err)}`;
|
|
470
|
-
this._markDirtyAndRender();
|
|
471
|
-
}
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
private async performCommit(message: string): Promise<void> {
|
|
475
|
-
try {
|
|
476
|
-
await this.withLoading('Committing…', async () => this.git.commit(message));
|
|
477
|
-
await this.refresh();
|
|
478
|
-
} catch (err) {
|
|
479
|
-
this.error = `Commit failed: ${summarizeError(err)}`;
|
|
480
|
-
this._markDirtyAndRender();
|
|
481
|
-
}
|
|
482
|
-
}
|
|
483
|
-
|
|
484
|
-
private async performInit(): Promise<void> {
|
|
485
|
-
const result = GitService.initRepo(this.workingDirectory);
|
|
486
|
-
if (result.success) {
|
|
487
|
-
logger.debug('GitPanel: repo initialised via explicit confirm', { cwd: this.workingDirectory });
|
|
488
|
-
this.notGitRepo = false;
|
|
489
|
-
this.error = null;
|
|
490
|
-
await this.refresh();
|
|
491
|
-
} else {
|
|
492
|
-
this.error = `Git init failed: ${result.error ?? 'unknown error'}`;
|
|
493
|
-
this._markDirtyAndRender();
|
|
494
|
-
}
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
// Rendering
|
|
498
|
-
|
|
499
|
-
override render(width: number, height: number): Line[] {
|
|
500
|
-
if (this.confirm) {
|
|
501
|
-
return buildPanelWorkspace(width, height, {
|
|
502
|
-
title: ' Git',
|
|
503
|
-
sections: [{ title: 'Confirmation', lines: renderConfirmLines(width, this.confirm) }],
|
|
504
|
-
palette: DEFAULT_PANEL_PALETTE,
|
|
505
|
-
});
|
|
506
|
-
}
|
|
507
|
-
if (this.commitMessage !== null) {
|
|
508
|
-
return this.renderCommitCompose(width, height);
|
|
509
|
-
}
|
|
510
|
-
if (this.loading) {
|
|
511
|
-
return this.renderMessage(width, height, 'Loading git status...', C.branch);
|
|
512
|
-
}
|
|
513
|
-
if (this.error) {
|
|
514
|
-
return this.renderMessage(width, height, this.error, this.notGitRepo ? C.warn : C.bad);
|
|
515
|
-
}
|
|
516
|
-
// I3: spinner during openDiff()/performCommit() async fetch
|
|
517
|
-
if (this.loadingState === 'loading') {
|
|
518
|
-
return this.renderMessage(width, height, 'Loading diff...', C.branch);
|
|
519
|
-
}
|
|
520
|
-
if (this.expandedDiff !== null) {
|
|
521
|
-
return this.renderDiff(width, height);
|
|
522
|
-
}
|
|
523
|
-
return this.renderList(width, height);
|
|
524
|
-
}
|
|
525
|
-
|
|
526
|
-
// -- Helpers -----------------------------------------------------------------
|
|
527
|
-
|
|
528
|
-
private renderMessage(width: number, height: number, msg: string, fg: string): Line[] {
|
|
529
|
-
const lines: Line[] = [buildStyledPanelLine(width, [{ text: msg, fg }])];
|
|
530
|
-
while (lines.length < height) lines.push(createEmptyLine(width));
|
|
531
|
-
return lines;
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
private renderCommitCompose(width: number, height: number): Line[] {
|
|
535
|
-
const inputLine = buildSearchInputLine(
|
|
536
|
-
width,
|
|
537
|
-
'Commit message: ',
|
|
538
|
-
`${this.commitMessage ?? ''}_`,
|
|
539
|
-
DEFAULT_PANEL_PALETTE,
|
|
540
|
-
{ active: true, bg: DEFAULT_PANEL_PALETTE.inputBg, valueColor: DEFAULT_PANEL_PALETTE.info },
|
|
541
|
-
);
|
|
542
|
-
const footerLines = [
|
|
543
|
-
buildKeyboardHints(width, [{ keys: 'Enter', label: 'review & confirm' }, { keys: 'Esc', label: 'cancel' }], DEFAULT_PANEL_PALETTE),
|
|
544
|
-
];
|
|
545
|
-
return buildPanelWorkspace(width, height, {
|
|
546
|
-
title: ' Git: Commit',
|
|
547
|
-
sections: [{ title: 'Message', lines: [inputLine] }],
|
|
548
|
-
footerLines,
|
|
549
|
-
palette: DEFAULT_PANEL_PALETTE,
|
|
550
|
-
});
|
|
551
|
-
}
|
|
552
|
-
|
|
553
|
-
/** Paint a single text string into a new Line at x=startX. */
|
|
554
|
-
private paintText(
|
|
555
|
-
line: Line,
|
|
556
|
-
text: string,
|
|
557
|
-
startX: number,
|
|
558
|
-
width: number,
|
|
559
|
-
fg: string,
|
|
560
|
-
opts: { bold?: boolean; dim?: boolean } = {},
|
|
561
|
-
): number {
|
|
562
|
-
let x = startX;
|
|
563
|
-
for (const ch of text) {
|
|
564
|
-
if (x >= width) break;
|
|
565
|
-
line[x++] = createStyledCell(ch, { fg, bold: opts.bold, dim: opts.dim });
|
|
566
|
-
}
|
|
567
|
-
return x;
|
|
568
|
-
}
|
|
569
|
-
|
|
570
|
-
private renderBranchLine(width: number): Line {
|
|
571
|
-
const isDirty = this.data.stagedFiles.length > 0 || this.data.unstagedFiles.length > 0;
|
|
572
|
-
const segments: Array<{ text: string; fg: string; bold?: boolean }> = [
|
|
573
|
-
{ text: ' ⎇ ', fg: C.sectionHeader },
|
|
574
|
-
{ text: this.data.branch, fg: C.branch, bold: true },
|
|
575
|
-
];
|
|
576
|
-
if (this.data.ahead > 0) segments.push({ text: ` ↑${this.data.ahead}`, fg: C.good });
|
|
577
|
-
if (this.data.behind > 0) segments.push({ text: ` ↓${this.data.behind}`, fg: C.bad });
|
|
578
|
-
if (this.data.ahead === 0 && this.data.behind === 0) {
|
|
579
|
-
segments.push({ text: ' ≡ up to date', fg: C.dim });
|
|
580
|
-
}
|
|
581
|
-
segments.push({
|
|
582
|
-
text: isDirty ? ' ● dirty' : ' ✓ clean',
|
|
583
|
-
fg: isDirty ? C.warn : C.good,
|
|
584
|
-
bold: true,
|
|
585
|
-
});
|
|
586
|
-
segments.push({ text: ` ${this.data.stagedFiles.length} staged`, fg: this.data.stagedFiles.length > 0 ? C.good : C.dim });
|
|
587
|
-
segments.push({ text: ` · ${this.data.unstagedFiles.length} unstaged`, fg: this.data.unstagedFiles.length > 0 ? C.warn : C.dim });
|
|
588
|
-
return buildStyledPanelLine(width, segments);
|
|
589
|
-
}
|
|
590
|
-
|
|
591
|
-
private renderSectionHeader(label: string, width: number): Line {
|
|
592
|
-
return buildStyledPanelLine(width, [{ text: ` ◆ ${label}`, fg: C.sectionHeader, bold: true }]);
|
|
593
|
-
}
|
|
594
|
-
|
|
595
|
-
private renderFileRow(entry: GitFileEntry, selected: boolean, width: number): Line {
|
|
596
|
-
const fg = entry.staged ? C.good : C.bad;
|
|
597
|
-
// Single-char status marker: + staged (ready to commit), M modified/unstaged.
|
|
598
|
-
const statusGlyph = entry.staged ? '+' : 'M';
|
|
599
|
-
const sessionMarker = entry.sessionChanged ? '● ' : ' ';
|
|
600
|
-
const path = truncateDisplay(entry.path, Math.max(0, width - 8));
|
|
601
|
-
return buildSelectablePanelLine(width, [
|
|
602
|
-
{ text: ` ${statusGlyph} `, fg, bg: selected ? C.selected : undefined, bold: true },
|
|
603
|
-
{ text: sessionMarker, fg: selected ? C.selectedFg : C.sessionChanged, bg: selected ? C.selected : undefined },
|
|
604
|
-
{ text: path, fg: selected ? C.selectedFg : fg, bg: selected ? C.selected : undefined, bold: selected },
|
|
605
|
-
], { selected, selectedBg: C.selected, fillFg: selected ? C.selectedFg : '', leadingMarker: '▸' });
|
|
606
|
-
}
|
|
607
|
-
|
|
608
|
-
private renderCommitRow(entry: CommitEntry, selected: boolean, width: number): Line {
|
|
609
|
-
const hashPart = ` ${entry.hash} `;
|
|
610
|
-
const msgBudget = Math.max(0, width - getDisplayWidth(hashPart) - 3);
|
|
611
|
-
const msgPart = truncateDisplay(entry.message, msgBudget);
|
|
612
|
-
return buildSelectablePanelLine(width, [
|
|
613
|
-
{ text: hashPart, fg: selected ? C.selectedFg : C.commitHash, bg: selected ? C.selected : undefined },
|
|
614
|
-
{ text: msgPart, fg: selected ? C.selectedFg : C.commit, bg: selected ? C.selected : undefined, bold: selected },
|
|
615
|
-
], { selected, selectedBg: C.selected, fillFg: selected ? C.selectedFg : '', leadingMarker: '▸' });
|
|
616
|
-
}
|
|
617
|
-
|
|
618
|
-
private renderList(width: number, height: number): Line[] {
|
|
619
|
-
const intro = 'Review branch status, staged and unstaged files, and recent commits. Open a file or commit row to inspect its diff.';
|
|
620
|
-
const footerHints = [
|
|
621
|
-
buildKeyboardHints(width, [
|
|
622
|
-
{ keys: '↑/↓', label: 'navigate' },
|
|
623
|
-
{ keys: 'Enter', label: 'open diff →' },
|
|
624
|
-
{ keys: 's/u', label: 'stage/unstage' },
|
|
625
|
-
{ keys: 'c', label: 'commit' },
|
|
626
|
-
{ keys: 'r', label: 'refresh' },
|
|
627
|
-
], DEFAULT_PANEL_PALETTE),
|
|
628
|
-
];
|
|
629
|
-
const emptyStateLines = buildEmptyState(width, ' No git rows', 'This repository has no staged or unstaged changes and no commits to display yet.', [{ command: 'r', summary: 'refresh working-tree status in this panel, or stage changes to populate this view' }], DEFAULT_PANEL_PALETTE); // was '/git status' (chat-only, never touches this panel)
|
|
630
|
-
const rows: Line[] = [];
|
|
631
|
-
for (let i = 0; i < this.items.length; i++) {
|
|
632
|
-
const item = this.items[i];
|
|
633
|
-
const selected = i === this.selectedIndex;
|
|
634
|
-
if (!item) continue;
|
|
635
|
-
|
|
636
|
-
switch (item.kind) {
|
|
637
|
-
case 'header':
|
|
638
|
-
rows.push(this.renderBranchLine(width));
|
|
639
|
-
rows.push(createEmptyLine(width)); // spacer
|
|
640
|
-
break;
|
|
641
|
-
case 'section':
|
|
642
|
-
rows.push(this.renderSectionHeader(item.label, width));
|
|
643
|
-
break;
|
|
644
|
-
case 'file':
|
|
645
|
-
rows.push(this.renderFileRow(item.entry, selected, width));
|
|
646
|
-
break;
|
|
647
|
-
case 'commit':
|
|
648
|
-
rows.push(this.renderCommitRow(item.entry, selected, width));
|
|
649
|
-
break;
|
|
650
|
-
case 'empty': {
|
|
651
|
-
rows.push(buildStyledPanelLine(width, [{ text: item.label, fg: C.sectionHeader, dim: true }]));
|
|
652
|
-
break;
|
|
653
|
-
}
|
|
654
|
-
}
|
|
655
|
-
}
|
|
656
|
-
|
|
657
|
-
const selectedRowIndex = this.getRowIndexForItem(this.selectedIndex);
|
|
658
|
-
if (selectedRowIndex >= 0) {
|
|
659
|
-
const isDirty = this.data.stagedFiles.length > 0 || this.data.unstagedFiles.length > 0;
|
|
660
|
-
const selectedItem = this.items.at(this.selectedIndex);
|
|
661
|
-
const selectedLines: Line[] = [];
|
|
662
|
-
if (selectedItem?.kind === 'file') {
|
|
663
|
-
selectedLines.push(buildPanelLine(width, [
|
|
664
|
-
[' File ', DEFAULT_PANEL_PALETTE.label],
|
|
665
|
-
[selectedItem.entry.path, DEFAULT_PANEL_PALETTE.value],
|
|
666
|
-
[' State ', DEFAULT_PANEL_PALETTE.label],
|
|
667
|
-
[selectedItem.entry.staged ? 'staged' : 'unstaged', selectedItem.entry.staged ? DEFAULT_PANEL_PALETTE.good : DEFAULT_PANEL_PALETTE.warn],
|
|
668
|
-
]));
|
|
669
|
-
} else if (selectedItem?.kind === 'commit') {
|
|
670
|
-
selectedLines.push(buildPanelLine(width, [
|
|
671
|
-
[' Commit ', DEFAULT_PANEL_PALETTE.label],
|
|
672
|
-
[selectedItem.entry.hash, DEFAULT_PANEL_PALETTE.info],
|
|
673
|
-
[' Author ', DEFAULT_PANEL_PALETTE.label],
|
|
674
|
-
[selectedItem.entry.author, DEFAULT_PANEL_PALETTE.value],
|
|
675
|
-
]));
|
|
676
|
-
selectedLines.push(buildPanelLine(width, [
|
|
677
|
-
[' Message ', DEFAULT_PANEL_PALETTE.label],
|
|
678
|
-
[selectedItem.entry.message, DEFAULT_PANEL_PALETTE.value],
|
|
679
|
-
]));
|
|
680
|
-
}
|
|
681
|
-
|
|
682
|
-
const summarySection = {
|
|
683
|
-
title: 'Summary',
|
|
684
|
-
lines: [
|
|
685
|
-
buildPanelLine(width, [
|
|
686
|
-
[' Branch ', DEFAULT_PANEL_PALETTE.label],
|
|
687
|
-
[this.data.branch, DEFAULT_PANEL_PALETTE.info],
|
|
688
|
-
[' Ahead ', DEFAULT_PANEL_PALETTE.label],
|
|
689
|
-
[String(this.data.ahead), this.data.ahead > 0 ? DEFAULT_PANEL_PALETTE.good : DEFAULT_PANEL_PALETTE.dim],
|
|
690
|
-
[' Behind ', DEFAULT_PANEL_PALETTE.label],
|
|
691
|
-
[String(this.data.behind), this.data.behind > 0 ? DEFAULT_PANEL_PALETTE.bad : DEFAULT_PANEL_PALETTE.dim],
|
|
692
|
-
[' Status ', DEFAULT_PANEL_PALETTE.label],
|
|
693
|
-
[isDirty ? 'dirty' : 'clean', isDirty ? DEFAULT_PANEL_PALETTE.warn : DEFAULT_PANEL_PALETTE.good],
|
|
694
|
-
]),
|
|
695
|
-
],
|
|
696
|
-
} as const;
|
|
697
|
-
const selectedSection = { title: 'Selected', lines: selectedLines } as const;
|
|
698
|
-
const workspaceSection = resolveScrollablePanelSection(width, height, {
|
|
699
|
-
intro,
|
|
700
|
-
footerLines: footerHints,
|
|
701
|
-
palette: DEFAULT_PANEL_PALETTE,
|
|
702
|
-
beforeSections: [summarySection],
|
|
703
|
-
section: {
|
|
704
|
-
title: 'Workspace',
|
|
705
|
-
scrollableLines: rows,
|
|
706
|
-
selectedIndex: selectedRowIndex,
|
|
707
|
-
scrollOffset: this.scrollOffset,
|
|
708
|
-
minRows: 8,
|
|
709
|
-
},
|
|
710
|
-
afterSections: [selectedSection],
|
|
711
|
-
});
|
|
712
|
-
this.scrollOffset = workspaceSection.scrollOffset;
|
|
713
|
-
|
|
714
|
-
return buildPanelWorkspace(width, height, {
|
|
715
|
-
title: ' Git',
|
|
716
|
-
intro,
|
|
717
|
-
sections: [
|
|
718
|
-
summarySection,
|
|
719
|
-
workspaceSection.section.lines.length > 0 ? workspaceSection.section : { title: 'Workspace', lines: emptyStateLines },
|
|
720
|
-
selectedSection,
|
|
721
|
-
],
|
|
722
|
-
footerLines: footerHints,
|
|
723
|
-
palette: DEFAULT_PANEL_PALETTE,
|
|
724
|
-
});
|
|
725
|
-
}
|
|
726
|
-
return buildPanelWorkspace(width, height, {
|
|
727
|
-
title: ' Git',
|
|
728
|
-
intro,
|
|
729
|
-
sections: [{ lines: emptyStateLines }],
|
|
730
|
-
palette: DEFAULT_PANEL_PALETTE,
|
|
731
|
-
});
|
|
732
|
-
}
|
|
733
|
-
|
|
734
|
-
/** Map an item index in `this.items` to its rendered row index (header items expand to 2 rows: branch + spacer). */
|
|
735
|
-
private getRowIndexForItem(itemIndex: number): number {
|
|
736
|
-
let row = 0;
|
|
737
|
-
for (let i = 0; i < itemIndex && i < this.items.length; i++) {
|
|
738
|
-
const it = this.items[i];
|
|
739
|
-
if (it?.kind === 'header') {
|
|
740
|
-
row += 2;
|
|
741
|
-
} else {
|
|
742
|
-
row += 1;
|
|
743
|
-
}
|
|
744
|
-
}
|
|
745
|
-
return row;
|
|
746
|
-
}
|
|
747
|
-
|
|
748
|
-
private renderDiff(width: number, height: number): Line[] {
|
|
749
|
-
const item = this.items.at(this.selectedIndex);
|
|
750
|
-
const diffLines = this.expandedDiff ?? [];
|
|
751
|
-
let added = 0;
|
|
752
|
-
let removed = 0;
|
|
753
|
-
for (const l of diffLines) {
|
|
754
|
-
if (l.startsWith('+') && !l.startsWith('+++')) added++;
|
|
755
|
-
else if (l.startsWith('-') && !l.startsWith('---')) removed++;
|
|
756
|
-
}
|
|
757
|
-
const title =
|
|
758
|
-
item?.kind === 'file'
|
|
759
|
-
? `Diff: ${item.entry.path} +${added} -${removed}`
|
|
760
|
-
: item?.kind === 'commit'
|
|
761
|
-
? `Commit ${item.entry.hash}: ${item.entry.message} +${added} -${removed}`
|
|
762
|
-
: 'Diff';
|
|
763
|
-
const renderedLines = diffLines.map((rawLine) => {
|
|
764
|
-
const dLine = createEmptyLine(width);
|
|
765
|
-
let fg: string;
|
|
766
|
-
if (rawLine.startsWith('+') && !rawLine.startsWith('+++')) {
|
|
767
|
-
fg = C.good;
|
|
768
|
-
} else if (rawLine.startsWith('-') && !rawLine.startsWith('---')) {
|
|
769
|
-
fg = C.bad;
|
|
770
|
-
} else if (rawLine.startsWith('@@') || rawLine.startsWith('diff') || rawLine.startsWith('index')) {
|
|
771
|
-
fg = C.diffMeta;
|
|
772
|
-
} else {
|
|
773
|
-
fg = C.diffNeutral;
|
|
774
|
-
}
|
|
775
|
-
this.paintText(dLine, rawLine, 0, width, fg);
|
|
776
|
-
return dLine;
|
|
777
|
-
});
|
|
778
|
-
const footerLines = [
|
|
779
|
-
buildKeyboardHints(width, [{ keys: '↑/↓', label: 'scroll' }, { keys: 'Esc/q', label: 'back to files' }], DEFAULT_PANEL_PALETTE),
|
|
780
|
-
];
|
|
781
|
-
const diffSection = resolveScrollablePanelSection(width, height, {
|
|
782
|
-
palette: DEFAULT_PANEL_PALETTE,
|
|
783
|
-
footerLines,
|
|
784
|
-
section: {
|
|
785
|
-
title: 'Patch',
|
|
786
|
-
scrollableLines: renderedLines,
|
|
787
|
-
scrollOffset: this.scrollOffset,
|
|
788
|
-
minRows: 1,
|
|
789
|
-
},
|
|
790
|
-
});
|
|
791
|
-
this.scrollOffset = diffSection.scrollOffset;
|
|
792
|
-
|
|
793
|
-
return buildPanelWorkspace(width, height, {
|
|
794
|
-
title: ` ${title}`,
|
|
795
|
-
sections: [diffSection.section],
|
|
796
|
-
footerLines,
|
|
797
|
-
palette: DEFAULT_PANEL_PALETTE,
|
|
798
|
-
});
|
|
799
|
-
}
|
|
800
|
-
}
|