@pellux/goodvibes-tui 0.27.0 → 0.29.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 +124 -0
- package/README.md +1 -1
- package/package.json +1 -1
- package/src/core/context-usage.ts +53 -0
- package/src/core/conversation.ts +4 -6
- package/src/core/session-recovery.ts +2 -0
- package/src/core/turn-event-wiring.ts +1 -0
- package/src/input/commands/cost-runtime.ts +49 -0
- package/src/input/commands/eval.ts +4 -3
- package/src/input/commands/operator-runtime.ts +5 -1
- package/src/input/commands.ts +2 -0
- package/src/input/handler-feed-routes.ts +60 -26
- package/src/input/handler-feed.ts +17 -8
- package/src/input/handler-picker-routes.ts +18 -13
- package/src/input/handler-shortcuts.ts +51 -0
- package/src/input/handler-ui-state.ts +4 -0
- package/src/input/handler.ts +43 -6
- package/src/input/keybindings.ts +53 -8
- package/src/input/model-picker.ts +5 -0
- package/src/input/panel-integration-actions.ts +117 -1
- package/src/main.ts +5 -2
- package/src/panels/agent-inspector-panel.ts +139 -45
- package/src/panels/agent-inspector-shared.ts +147 -3
- package/src/panels/approval-panel.ts +204 -102
- package/src/panels/automation-control-panel.ts +370 -103
- package/src/panels/base-panel.ts +26 -2
- package/src/panels/builtin/agent.ts +28 -37
- package/src/panels/builtin/development.ts +40 -32
- package/src/panels/builtin/knowledge.ts +15 -6
- package/src/panels/builtin/operations.ts +178 -117
- package/src/panels/builtin/session.ts +35 -9
- package/src/panels/builtin/shared.ts +98 -6
- package/src/panels/cockpit-panel.ts +253 -75
- package/src/panels/cockpit-read-model.ts +2 -1
- package/src/panels/communication-panel.ts +159 -56
- package/src/panels/confirm-state.ts +8 -1
- package/src/panels/control-plane-panel.ts +354 -95
- package/src/panels/cost-tracker-panel.ts +243 -57
- package/src/panels/debug-panel.ts +347 -159
- package/src/panels/diff-panel.ts +257 -82
- package/src/panels/docs-panel.ts +166 -64
- package/src/panels/eval-panel.ts +336 -109
- package/src/panels/expandable-list-panel.ts +189 -0
- package/src/panels/file-explorer-panel.ts +238 -159
- package/src/panels/file-preview-panel.ts +141 -59
- package/src/panels/git-panel.ts +360 -191
- package/src/panels/hooks-panel.ts +181 -19
- package/src/panels/incident-review-panel.ts +252 -43
- package/src/panels/index.ts +0 -4
- package/src/panels/intelligence-panel.ts +310 -103
- package/src/panels/knowledge-graph-panel.ts +480 -58
- package/src/panels/local-auth-panel.ts +251 -33
- package/src/panels/marketplace-panel.ts +219 -33
- package/src/panels/memory-panel.ts +83 -81
- package/src/panels/ops-control-panel.ts +211 -32
- package/src/panels/ops-strategy-panel.ts +131 -45
- package/src/panels/orchestration-panel.ts +259 -67
- package/src/panels/panel-list-panel.ts +184 -136
- package/src/panels/panel-manager.ts +120 -13
- package/src/panels/plan-dashboard-panel.ts +507 -74
- package/src/panels/plugins-panel.ts +227 -34
- package/src/panels/policy-panel.ts +264 -55
- package/src/panels/polish-core.ts +162 -0
- package/src/panels/polish-tables.ts +258 -0
- package/src/panels/polish.ts +67 -149
- package/src/panels/project-planning-answer-actions.ts +134 -0
- package/src/panels/project-planning-panel.ts +84 -113
- package/src/panels/provider-health-panel.ts +438 -480
- package/src/panels/provider-health-routes.ts +203 -0
- package/src/panels/provider-health-tracker.ts +194 -6
- package/src/panels/provider-health-views.ts +560 -0
- package/src/panels/qr-panel.ts +136 -38
- package/src/panels/remote-panel.ts +123 -38
- package/src/panels/routes-panel.ts +87 -24
- package/src/panels/sandbox-panel.ts +232 -65
- package/src/panels/scrollable-list-panel.ts +143 -145
- package/src/panels/security-panel.ts +204 -52
- package/src/panels/services-panel.ts +174 -76
- package/src/panels/session-browser-panel.ts +101 -6
- package/src/panels/session-maintenance.ts +4 -122
- package/src/panels/settings-sync-panel.ts +346 -68
- package/src/panels/skills-panel.ts +157 -89
- package/src/panels/subscription-panel.ts +105 -34
- package/src/panels/symbol-outline-panel.ts +287 -154
- package/src/panels/system-messages-panel.ts +172 -38
- package/src/panels/tasks-panel.ts +348 -134
- package/src/panels/thinking-panel.ts +66 -32
- package/src/panels/token-budget-panel.ts +233 -46
- package/src/panels/tool-inspector-panel.ts +170 -54
- package/src/panels/types.ts +65 -4
- package/src/panels/work-plan-panel.ts +393 -39
- package/src/panels/worktree-panel.ts +239 -61
- package/src/panels/wrfc-panel-format.ts +133 -0
- package/src/panels/wrfc-panel.ts +146 -150
- package/src/renderer/compaction-preview.ts +2 -1
- package/src/renderer/compositor.ts +46 -43
- package/src/renderer/conversation-overlays.ts +25 -11
- package/src/renderer/footer-tips.ts +41 -0
- package/src/renderer/fullscreen-primitives.ts +22 -16
- package/src/renderer/help-overlay.ts +91 -14
- package/src/renderer/hint-grammar.ts +52 -0
- package/src/renderer/layout.ts +7 -7
- package/src/renderer/modal-factory.ts +23 -15
- package/src/renderer/model-picker-overlay.ts +30 -11
- package/src/renderer/model-workspace.ts +2 -3
- package/src/renderer/overlay-box.ts +16 -10
- package/src/renderer/panel-composite.ts +7 -20
- package/src/renderer/panel-workspace-bar.ts +14 -8
- package/src/renderer/process-indicator.ts +3 -1
- package/src/renderer/progress.ts +8 -6
- package/src/renderer/search-overlay.ts +27 -6
- package/src/renderer/session-picker-modal.ts +6 -4
- package/src/renderer/settings-modal.ts +70 -10
- package/src/renderer/shell-surface.ts +41 -15
- package/src/renderer/status-glyphs.ts +11 -9
- package/src/renderer/tab-strip.ts +148 -34
- package/src/renderer/theme.ts +60 -3
- package/src/renderer/ui-factory.ts +45 -36
- package/src/renderer/ui-primitives.ts +23 -2
- package/src/runtime/bootstrap-shell.ts +35 -18
- package/src/runtime/diagnostics/panels/index.ts +0 -3
- package/src/runtime/ui/model-picker/health-enrichment.ts +3 -0
- package/src/runtime/ui/model-picker/types.ts +4 -0
- package/src/shell/ui-openers.ts +14 -22
- package/src/utils/format-duration.ts +55 -0
- package/src/utils/format-number.ts +71 -0
- package/src/utils/splash-lines.ts +44 -3
- package/src/version.ts +1 -1
- package/src/work-plans/work-plan-store.ts +13 -0
- package/src/panels/agent-logs-panel.ts +0 -666
- package/src/panels/agent-logs-shared.ts +0 -129
- package/src/panels/context-visualizer-panel.ts +0 -214
- package/src/panels/forensics-panel.ts +0 -364
- package/src/panels/panel-picker.ts +0 -106
- package/src/panels/provider-account-snapshot.ts +0 -259
- package/src/panels/provider-accounts-panel.ts +0 -218
- package/src/panels/provider-stats-panel.ts +0 -366
- package/src/panels/schedule-panel.ts +0 -342
- package/src/panels/watchers-panel.ts +0 -193
- package/src/renderer/panel-picker-overlay.ts +0 -202
- package/src/renderer/panel-tab-bar.ts +0 -69
- package/src/runtime/diagnostics/panels/panel-resources.ts +0 -118
- package/src/runtime/diagnostics/panels/policy.ts +0 -177
package/src/panels/git-panel.ts
CHANGED
|
@@ -1,25 +1,37 @@
|
|
|
1
1
|
import { BasePanel } from './base-panel.ts';
|
|
2
2
|
import { createEmptyLine, createStyledCell, type Line } from '../types/grid.ts';
|
|
3
|
+
import { truncateDisplay, getDisplayWidth } from '../utils/terminal-width.ts';
|
|
3
4
|
import { GitService } from '@pellux/goodvibes-sdk/platform/git';
|
|
4
5
|
import { logger } from '@pellux/goodvibes-sdk/platform/utils';
|
|
5
6
|
import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
|
|
7
|
+
import { UI_TONES } from '../renderer/ui-primitives.ts';
|
|
6
8
|
import {
|
|
7
9
|
buildEmptyState,
|
|
10
|
+
buildKeyboardHints,
|
|
8
11
|
buildPanelLine,
|
|
9
12
|
buildPanelWorkspace,
|
|
13
|
+
buildSearchInputLine,
|
|
10
14
|
resolveScrollablePanelSection,
|
|
15
|
+
buildSelectablePanelLine,
|
|
11
16
|
buildStyledPanelLine,
|
|
12
17
|
DEFAULT_PANEL_PALETTE,
|
|
13
18
|
extendPalette,
|
|
14
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';
|
|
15
27
|
|
|
16
|
-
// ---------------------------------------------------------------------------
|
|
17
28
|
// Types
|
|
18
|
-
// ---------------------------------------------------------------------------
|
|
19
29
|
|
|
20
30
|
interface GitFileEntry {
|
|
21
31
|
path: string;
|
|
22
32
|
staged: boolean;
|
|
33
|
+
/** True when this file was touched during the current session (best-effort). */
|
|
34
|
+
sessionChanged?: boolean;
|
|
23
35
|
}
|
|
24
36
|
|
|
25
37
|
interface CommitEntry {
|
|
@@ -43,19 +55,19 @@ type ViewItem =
|
|
|
43
55
|
| { kind: 'section'; label: string }
|
|
44
56
|
| { kind: 'file'; entry: GitFileEntry }
|
|
45
57
|
| { kind: 'commit'; entry: CommitEntry }
|
|
46
|
-
| { kind: 'empty'; label: string }
|
|
47
|
-
|
|
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 };
|
|
48
64
|
|
|
49
|
-
// ---------------------------------------------------------------------------
|
|
50
65
|
// Constants
|
|
51
|
-
// ---------------------------------------------------------------------------
|
|
52
66
|
|
|
53
67
|
/** Minimum number of diff lines kept visible when clamping scroll offset. */
|
|
54
68
|
const MIN_VISIBLE_DIFF_LINES = 5;
|
|
55
69
|
|
|
56
|
-
// ---------------------------------------------------------------------------
|
|
57
70
|
// Colors
|
|
58
|
-
// ---------------------------------------------------------------------------
|
|
59
71
|
|
|
60
72
|
const C = extendPalette(DEFAULT_PANEL_PALETTE, {
|
|
61
73
|
branch: '#00d7ff',
|
|
@@ -67,14 +79,18 @@ const C = extendPalette(DEFAULT_PANEL_PALETTE, {
|
|
|
67
79
|
selectedFg: '#ffffff',
|
|
68
80
|
diffMeta: '#5f87ff',
|
|
69
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,
|
|
70
85
|
});
|
|
71
86
|
|
|
72
|
-
// ---------------------------------------------------------------------------
|
|
73
87
|
// GitPanel
|
|
74
|
-
// ---------------------------------------------------------------------------
|
|
75
88
|
|
|
76
89
|
export class GitPanel extends BasePanel {
|
|
77
90
|
private readonly workingDirectory: string;
|
|
91
|
+
private readonly git: GitService;
|
|
92
|
+
private readonly getChangedFiles?: () => readonly string[];
|
|
93
|
+
|
|
78
94
|
private data: GitData = {
|
|
79
95
|
branch: '...',
|
|
80
96
|
ahead: 0,
|
|
@@ -90,7 +106,7 @@ export class GitPanel extends BasePanel {
|
|
|
90
106
|
/** Selected row index within `items`. */
|
|
91
107
|
private selectedIndex = 0;
|
|
92
108
|
|
|
93
|
-
/** When truthy, shows the diff for the selected file. */
|
|
109
|
+
/** When truthy, shows the diff for the selected file or commit. */
|
|
94
110
|
private expandedDiff: string[] | null = null;
|
|
95
111
|
|
|
96
112
|
/** Scroll offset for both main view and diff view. */
|
|
@@ -100,14 +116,34 @@ export class GitPanel extends BasePanel {
|
|
|
100
116
|
private loading = true;
|
|
101
117
|
private error: string | null = null;
|
|
102
118
|
|
|
103
|
-
|
|
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
|
+
) {
|
|
104
133
|
super('git', 'Git', 'G', 'development');
|
|
105
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();
|
|
106
144
|
}
|
|
107
145
|
|
|
108
|
-
// ---------------------------------------------------------------------------
|
|
109
146
|
// Lifecycle
|
|
110
|
-
// ---------------------------------------------------------------------------
|
|
111
147
|
|
|
112
148
|
override onActivate(): void {
|
|
113
149
|
super.onActivate();
|
|
@@ -129,30 +165,35 @@ export class GitPanel extends BasePanel {
|
|
|
129
165
|
super.onDestroy();
|
|
130
166
|
}
|
|
131
167
|
|
|
132
|
-
// ---------------------------------------------------------------------------
|
|
133
168
|
// Data fetching
|
|
134
|
-
// ---------------------------------------------------------------------------
|
|
135
169
|
|
|
136
|
-
private async refresh(
|
|
170
|
+
private async refresh(): Promise<void> {
|
|
137
171
|
try {
|
|
138
|
-
const git = new GitService(this.workingDirectory);
|
|
139
172
|
const [statusResult, branchResult, logEntries] = await Promise.all([
|
|
140
|
-
git.status(),
|
|
141
|
-
git.branch(),
|
|
142
|
-
git.log(10),
|
|
173
|
+
this.git.status(),
|
|
174
|
+
this.git.branch(),
|
|
175
|
+
this.git.log(10),
|
|
143
176
|
]);
|
|
144
177
|
|
|
145
|
-
const
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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)
|
|
182
|
+
// rather than simple-git's `modified`/`staged` convenience arrays: those
|
|
183
|
+
// arrays both include a file that is staged with no further edits (index
|
|
184
|
+
// 'M', working_dir ' '), which would otherwise show it as both staged
|
|
185
|
+
// AND unstaged and make 's'/'u' land on the wrong (phantom) row.
|
|
186
|
+
const stagedFiles: GitFileEntry[] = [];
|
|
187
|
+
const unstagedFiles: GitFileEntry[] = [];
|
|
188
|
+
for (const f of statusResult.files) {
|
|
189
|
+
const sessionChanged = markChanged(f.path);
|
|
190
|
+
if (f.index === '?' && f.working_dir === '?') {
|
|
191
|
+
unstagedFiles.push({ path: f.path, staged: false, sessionChanged }); // untracked
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
if (f.index !== ' ') stagedFiles.push({ path: f.path, staged: true, sessionChanged });
|
|
195
|
+
if (f.working_dir !== ' ') unstagedFiles.push({ path: f.path, staged: false, sessionChanged });
|
|
196
|
+
}
|
|
156
197
|
|
|
157
198
|
this.data = {
|
|
158
199
|
branch: branchResult.current || 'HEAD',
|
|
@@ -170,86 +211,98 @@ export class GitPanel extends BasePanel {
|
|
|
170
211
|
|
|
171
212
|
this.loading = false;
|
|
172
213
|
this.error = null;
|
|
214
|
+
this.notGitRepo = false;
|
|
173
215
|
this.rebuildItems();
|
|
174
216
|
// Do not clear expandedDiff during auto-refresh — only clear on explicit user action
|
|
175
|
-
this.
|
|
217
|
+
this._markDirtyAndRender();
|
|
176
218
|
} catch (err) {
|
|
177
219
|
const msg = summarizeError(err);
|
|
178
|
-
// If the failure is because this directory isn't a git repo, auto-initialise
|
|
179
|
-
// and retry once so the panel becomes functional immediately.
|
|
180
220
|
if (/not a git\b/i.test(msg)) {
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
if (!isRetry) {
|
|
186
|
-
// Retry refresh now that the repo exists (once only)
|
|
187
|
-
void this.refresh(true);
|
|
188
|
-
return;
|
|
189
|
-
}
|
|
190
|
-
this.error = 'Not a git repository. Auto-init succeeded but refresh failed.';
|
|
191
|
-
} else {
|
|
192
|
-
this.error = `Not a git repository. Auto-init failed: ${initResult.error ?? 'unknown error'}`;
|
|
193
|
-
}
|
|
221
|
+
// I4: no more auto `git init` side effect — surface the state and let
|
|
222
|
+
// the user explicitly confirm initialisation with 'i'.
|
|
223
|
+
this.notGitRepo = true;
|
|
224
|
+
this.error = 'Not a git repository here. Press i to initialize one (explicit confirm).';
|
|
194
225
|
} else {
|
|
226
|
+
this.notGitRepo = false;
|
|
195
227
|
this.error = msg;
|
|
196
228
|
}
|
|
197
229
|
this.loading = false;
|
|
198
230
|
logger.debug('GitPanel: refresh failed', { error: this.error });
|
|
199
|
-
this.
|
|
231
|
+
this._markDirtyAndRender();
|
|
200
232
|
}
|
|
201
233
|
}
|
|
202
234
|
|
|
235
|
+
/** Push a labeled section (with count) plus its rows, or an empty-row placeholder. */
|
|
236
|
+
private pushSection<T>(items: ViewItem[], label: string, entries: readonly T[], emptyLabel: string, toItem: (entry: T) => ViewItem): void {
|
|
237
|
+
items.push({ kind: 'section', label: `${label} (${entries.length})` });
|
|
238
|
+
if (entries.length === 0) items.push({ kind: 'empty', label: emptyLabel });
|
|
239
|
+
else for (const entry of entries) items.push(toItem(entry));
|
|
240
|
+
}
|
|
241
|
+
|
|
203
242
|
/** Rebuild the flat navigable item list from current data. */
|
|
204
243
|
private rebuildItems(): void {
|
|
205
|
-
const items: ViewItem[] = [];
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
items.
|
|
244
|
+
const items: ViewItem[] = [{ kind: 'header' }];
|
|
245
|
+
this.pushSection(items, 'Staged', this.data.stagedFiles, ' (no staged files)', (entry) => ({ kind: 'file', entry }));
|
|
246
|
+
this.pushSection(items, 'Unstaged', this.data.unstagedFiles, ' (no unstaged files)', (entry) => ({ kind: 'file', entry }));
|
|
247
|
+
this.pushSection(items, 'Recent Commits', this.data.recentCommits, ' (no commits)', (entry) => ({ kind: 'commit', entry }));
|
|
248
|
+
this.items = items;
|
|
209
249
|
|
|
210
|
-
//
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
items.push({ kind: 'empty', label: ' (no staged files)' });
|
|
214
|
-
} else {
|
|
215
|
-
for (const entry of this.data.stagedFiles) {
|
|
216
|
-
items.push({ kind: 'file', entry });
|
|
217
|
-
}
|
|
250
|
+
// Keep selection in bounds and off header/section/empty filler rows (I5).
|
|
251
|
+
if (this.selectedIndex >= this.items.length) {
|
|
252
|
+
this.selectedIndex = Math.max(0, this.items.length - 1);
|
|
218
253
|
}
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
if (this.data.unstagedFiles.length === 0) {
|
|
223
|
-
items.push({ kind: 'empty', label: ' (no unstaged files)' });
|
|
224
|
-
} else {
|
|
225
|
-
for (const entry of this.data.unstagedFiles) {
|
|
226
|
-
items.push({ kind: 'file', entry });
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
// Recent commits
|
|
231
|
-
items.push({ kind: 'section', label: `Recent Commits (${this.data.recentCommits.length})` });
|
|
232
|
-
if (this.data.recentCommits.length === 0) {
|
|
233
|
-
items.push({ kind: 'empty', label: ' (no commits)' });
|
|
234
|
-
} else {
|
|
235
|
-
for (const entry of this.data.recentCommits) {
|
|
236
|
-
items.push({ kind: 'commit', entry });
|
|
237
|
-
}
|
|
254
|
+
if (!this.isSelectable(this.items.at(this.selectedIndex))) {
|
|
255
|
+
const firstSelectable = this.items.findIndex((it) => this.isSelectable(it));
|
|
256
|
+
if (firstSelectable >= 0) this.selectedIndex = firstSelectable;
|
|
238
257
|
}
|
|
258
|
+
}
|
|
239
259
|
|
|
240
|
-
|
|
260
|
+
/** True for rows the cursor is allowed to rest on (file or commit rows). */
|
|
261
|
+
private isSelectable(item: ViewItem | undefined): boolean {
|
|
262
|
+
return item?.kind === 'file' || item?.kind === 'commit';
|
|
263
|
+
}
|
|
241
264
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
265
|
+
/** Move the selection to the next/previous selectable row, skipping filler rows. */
|
|
266
|
+
private moveSelection(direction: 1 | -1): void {
|
|
267
|
+
let idx = this.selectedIndex;
|
|
268
|
+
for (let i = 0; i < this.items.length; i++) {
|
|
269
|
+
idx += direction;
|
|
270
|
+
if (idx < 0 || idx >= this.items.length) return;
|
|
271
|
+
if (this.isSelectable(this.items[idx])) {
|
|
272
|
+
this.selectedIndex = idx;
|
|
273
|
+
this.markDirty();
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
245
276
|
}
|
|
246
277
|
}
|
|
247
278
|
|
|
248
|
-
// ---------------------------------------------------------------------------
|
|
249
279
|
// Input handling
|
|
250
|
-
// ---------------------------------------------------------------------------
|
|
251
280
|
|
|
252
281
|
handleInput(key: string): boolean {
|
|
282
|
+
// Confirm (init repo / commit) takes priority over everything else.
|
|
283
|
+
const confirmResult = handleConfirmInput(this.confirm, key);
|
|
284
|
+
if (confirmResult === 'confirmed') {
|
|
285
|
+
const subject = this.confirm!.subject;
|
|
286
|
+
this.confirm = null;
|
|
287
|
+
if (subject.kind === 'init') {
|
|
288
|
+
void this.performInit();
|
|
289
|
+
} else {
|
|
290
|
+
void this.performCommit(subject.message);
|
|
291
|
+
}
|
|
292
|
+
this.markDirty();
|
|
293
|
+
return true;
|
|
294
|
+
}
|
|
295
|
+
if (confirmResult === 'cancelled') {
|
|
296
|
+
this.confirm = null;
|
|
297
|
+
this.markDirty();
|
|
298
|
+
return true;
|
|
299
|
+
}
|
|
300
|
+
if (confirmResult === 'absorbed') return true;
|
|
301
|
+
|
|
302
|
+
if (this.commitMessage !== null) {
|
|
303
|
+
return this.handleCommitMessageInput(key);
|
|
304
|
+
}
|
|
305
|
+
|
|
253
306
|
if (this.expandedDiff !== null) {
|
|
254
307
|
return this.handleDiffInput(key);
|
|
255
308
|
}
|
|
@@ -260,18 +313,12 @@ export class GitPanel extends BasePanel {
|
|
|
260
313
|
switch (key) {
|
|
261
314
|
case 'up':
|
|
262
315
|
case 'k': {
|
|
263
|
-
|
|
264
|
-
this.selectedIndex--;
|
|
265
|
-
this.markDirty();
|
|
266
|
-
}
|
|
316
|
+
this.moveSelection(-1);
|
|
267
317
|
return true;
|
|
268
318
|
}
|
|
269
319
|
case 'down':
|
|
270
320
|
case 'j': {
|
|
271
|
-
|
|
272
|
-
this.selectedIndex++;
|
|
273
|
-
this.markDirty();
|
|
274
|
-
}
|
|
321
|
+
this.moveSelection(1);
|
|
275
322
|
return true;
|
|
276
323
|
}
|
|
277
324
|
case 'return': {
|
|
@@ -282,11 +329,59 @@ export class GitPanel extends BasePanel {
|
|
|
282
329
|
void this.refresh();
|
|
283
330
|
return true;
|
|
284
331
|
}
|
|
332
|
+
case 's': {
|
|
333
|
+
void this.setStaged(true);
|
|
334
|
+
return true;
|
|
335
|
+
}
|
|
336
|
+
case 'u': {
|
|
337
|
+
void this.setStaged(false);
|
|
338
|
+
return true;
|
|
339
|
+
}
|
|
340
|
+
case 'c': {
|
|
341
|
+
this.commitMessage = '';
|
|
342
|
+
this.markDirty();
|
|
343
|
+
return true;
|
|
344
|
+
}
|
|
345
|
+
case 'i': {
|
|
346
|
+
// Only meaningful when no repo is loaded — otherwise leave the key
|
|
347
|
+
// unconsumed instead of absorbing it as a silent no-op.
|
|
348
|
+
if (!this.notGitRepo) return false;
|
|
349
|
+
this.confirm = { subject: { kind: 'init' }, label: this.workingDirectory, verb: 'Init' };
|
|
350
|
+
this.markDirty();
|
|
351
|
+
return true;
|
|
352
|
+
}
|
|
285
353
|
default:
|
|
286
354
|
return false;
|
|
287
355
|
}
|
|
288
356
|
}
|
|
289
357
|
|
|
358
|
+
private handleCommitMessageInput(key: string): boolean {
|
|
359
|
+
if (isPanelSearchCancel(key)) {
|
|
360
|
+
this.commitMessage = null;
|
|
361
|
+
this.markDirty();
|
|
362
|
+
return true;
|
|
363
|
+
}
|
|
364
|
+
if (isPanelSearchCommit(key)) {
|
|
365
|
+
const message = (this.commitMessage ?? '').trim();
|
|
366
|
+
if (!message) return true; // absorb — require a non-empty message before confirming
|
|
367
|
+
this.confirm = { subject: { kind: 'commit', message }, label: message, verb: 'Commit' };
|
|
368
|
+
this.commitMessage = null;
|
|
369
|
+
this.markDirty();
|
|
370
|
+
return true;
|
|
371
|
+
}
|
|
372
|
+
if (isPanelSearchBackspace(key)) {
|
|
373
|
+
this.commitMessage = (this.commitMessage ?? '').slice(0, -1);
|
|
374
|
+
this.markDirty();
|
|
375
|
+
return true;
|
|
376
|
+
}
|
|
377
|
+
if (isPanelSearchPrintable(key)) {
|
|
378
|
+
this.commitMessage = (this.commitMessage ?? '') + key;
|
|
379
|
+
this.markDirty();
|
|
380
|
+
return true;
|
|
381
|
+
}
|
|
382
|
+
return true; // absorb every other key while composing
|
|
383
|
+
}
|
|
384
|
+
|
|
290
385
|
private handleDiffInput(key: string): boolean {
|
|
291
386
|
switch (key) {
|
|
292
387
|
case 'up':
|
|
@@ -316,38 +411,106 @@ export class GitPanel extends BasePanel {
|
|
|
316
411
|
}
|
|
317
412
|
}
|
|
318
413
|
|
|
414
|
+
/** Commit the given lines as the new expanded diff and reset scroll/dirty state. */
|
|
415
|
+
private finishDiff(lines: string[]): void {
|
|
416
|
+
this.expandedDiff = lines;
|
|
417
|
+
this.scrollOffset = 0;
|
|
418
|
+
this.markDirty();
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// I3: withLoading guarantees the spinner is cleared even if the fetch throws.
|
|
319
422
|
private async openDiff(): Promise<void> {
|
|
320
|
-
const item = this.items
|
|
321
|
-
if (!item
|
|
423
|
+
const item = this.items.at(this.selectedIndex);
|
|
424
|
+
if (!item) return;
|
|
425
|
+
|
|
426
|
+
if (item.kind === 'file') {
|
|
427
|
+
try {
|
|
428
|
+
const raw = await this.withLoading('Loading diff…', async () => this.git.diffFile(item.entry.path, item.entry.staged));
|
|
429
|
+
this.finishDiff(raw ? raw.split('\n') : ['(no diff available)']);
|
|
430
|
+
} catch (err) {
|
|
431
|
+
this.finishDiff([`Error: ${summarizeError(err)}`]);
|
|
432
|
+
}
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
322
435
|
|
|
323
|
-
|
|
436
|
+
if (item.kind === 'commit') {
|
|
437
|
+
// Root commits have no parent (`<hash>^` is invalid) — the catch below
|
|
438
|
+
// surfaces that instead of pretending a diff exists.
|
|
439
|
+
const before = `${item.entry.hash}^`;
|
|
440
|
+
const after = item.entry.hash;
|
|
441
|
+
try {
|
|
442
|
+
const [patch, stat] = await this.withLoading('Loading commit…', async () => Promise.all([
|
|
443
|
+
this.git.diffBetween(before, after),
|
|
444
|
+
this.git.diffStat(before, after),
|
|
445
|
+
]));
|
|
446
|
+
const statLines = stat.split('\n').filter((l) => l.trim().length > 0);
|
|
447
|
+
const patchLines = patch ? patch.split('\n') : [];
|
|
448
|
+
this.finishDiff(statLines.length > 0 ? [...statLines, '', ...patchLines] : (patchLines.length > 0 ? patchLines : ['(no diff available)']));
|
|
449
|
+
} catch (err) {
|
|
450
|
+
this.finishDiff([`Error: ${summarizeError(err)}`]);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// -- Mutating actions: stage / unstage / commit / init ----------------------
|
|
456
|
+
|
|
457
|
+
/** Stage (target=true) or unstage (target=false) the selected file row. */
|
|
458
|
+
private async setStaged(target: boolean): Promise<void> {
|
|
459
|
+
const item = this.items.at(this.selectedIndex);
|
|
460
|
+
if (!item || item.kind !== 'file' || item.entry.staged === target) return;
|
|
324
461
|
try {
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
});
|
|
329
|
-
this.expandedDiff = raw ? raw.split('\n') : ['(no diff available)'];
|
|
330
|
-
this.scrollOffset = 0;
|
|
331
|
-
this.markDirty();
|
|
462
|
+
if (target) await this.git.add(item.entry.path);
|
|
463
|
+
else await this.git.reset(item.entry.path);
|
|
464
|
+
await this.refresh();
|
|
332
465
|
} catch (err) {
|
|
333
|
-
this.
|
|
334
|
-
this.
|
|
335
|
-
|
|
466
|
+
this.error = `${target ? 'Stage' : 'Unstage'} failed: ${summarizeError(err)}`;
|
|
467
|
+
this._markDirtyAndRender();
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
private async performCommit(message: string): Promise<void> {
|
|
472
|
+
try {
|
|
473
|
+
await this.withLoading('Committing…', async () => this.git.commit(message));
|
|
474
|
+
await this.refresh();
|
|
475
|
+
} catch (err) {
|
|
476
|
+
this.error = `Commit failed: ${summarizeError(err)}`;
|
|
477
|
+
this._markDirtyAndRender();
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
private async performInit(): Promise<void> {
|
|
482
|
+
const result = GitService.initRepo(this.workingDirectory);
|
|
483
|
+
if (result.success) {
|
|
484
|
+
logger.debug('GitPanel: repo initialised via explicit confirm', { cwd: this.workingDirectory });
|
|
485
|
+
this.notGitRepo = false;
|
|
486
|
+
this.error = null;
|
|
487
|
+
await this.refresh();
|
|
488
|
+
} else {
|
|
489
|
+
this.error = `Git init failed: ${result.error ?? 'unknown error'}`;
|
|
490
|
+
this._markDirtyAndRender();
|
|
336
491
|
}
|
|
337
492
|
}
|
|
338
493
|
|
|
339
|
-
// ---------------------------------------------------------------------------
|
|
340
494
|
// Rendering
|
|
341
|
-
// ---------------------------------------------------------------------------
|
|
342
495
|
|
|
343
496
|
override render(width: number, height: number): Line[] {
|
|
497
|
+
if (this.confirm) {
|
|
498
|
+
return buildPanelWorkspace(width, height, {
|
|
499
|
+
title: ' Git',
|
|
500
|
+
sections: [{ title: 'Confirmation', lines: renderConfirmLines(width, this.confirm) }],
|
|
501
|
+
palette: DEFAULT_PANEL_PALETTE,
|
|
502
|
+
});
|
|
503
|
+
}
|
|
504
|
+
if (this.commitMessage !== null) {
|
|
505
|
+
return this.renderCommitCompose(width, height);
|
|
506
|
+
}
|
|
344
507
|
if (this.loading) {
|
|
345
508
|
return this.renderMessage(width, height, 'Loading git status...', C.branch);
|
|
346
509
|
}
|
|
347
510
|
if (this.error) {
|
|
348
|
-
return this.renderMessage(width, height,
|
|
511
|
+
return this.renderMessage(width, height, this.error, this.notGitRepo ? C.warn : C.bad);
|
|
349
512
|
}
|
|
350
|
-
// I3: spinner during openDiff() async fetch
|
|
513
|
+
// I3: spinner during openDiff()/performCommit() async fetch
|
|
351
514
|
if (this.loadingState === 'loading') {
|
|
352
515
|
return this.renderMessage(width, height, 'Loading diff...', C.branch);
|
|
353
516
|
}
|
|
@@ -365,6 +528,25 @@ export class GitPanel extends BasePanel {
|
|
|
365
528
|
return lines;
|
|
366
529
|
}
|
|
367
530
|
|
|
531
|
+
private renderCommitCompose(width: number, height: number): Line[] {
|
|
532
|
+
const inputLine = buildSearchInputLine(
|
|
533
|
+
width,
|
|
534
|
+
'Commit message: ',
|
|
535
|
+
`${this.commitMessage ?? ''}_`,
|
|
536
|
+
DEFAULT_PANEL_PALETTE,
|
|
537
|
+
{ active: true, bg: DEFAULT_PANEL_PALETTE.inputBg, valueColor: DEFAULT_PANEL_PALETTE.info },
|
|
538
|
+
);
|
|
539
|
+
const footerLines = [
|
|
540
|
+
buildKeyboardHints(width, [{ keys: 'Enter', label: 'review & confirm' }, { keys: 'Esc', label: 'cancel' }], DEFAULT_PANEL_PALETTE),
|
|
541
|
+
];
|
|
542
|
+
return buildPanelWorkspace(width, height, {
|
|
543
|
+
title: ' Git — Commit',
|
|
544
|
+
sections: [{ title: 'Message', lines: [inputLine] }],
|
|
545
|
+
footerLines,
|
|
546
|
+
palette: DEFAULT_PANEL_PALETTE,
|
|
547
|
+
});
|
|
548
|
+
}
|
|
549
|
+
|
|
368
550
|
/** Paint a single text string into a new Line at x=startX. */
|
|
369
551
|
private paintText(
|
|
370
552
|
line: Line,
|
|
@@ -383,80 +565,65 @@ export class GitPanel extends BasePanel {
|
|
|
383
565
|
}
|
|
384
566
|
|
|
385
567
|
private renderBranchLine(width: number): Line {
|
|
386
|
-
const line = createEmptyLine(width);
|
|
387
|
-
let x = 0;
|
|
388
|
-
|
|
389
|
-
const branchIcon = ' git: ';
|
|
390
|
-
x = this.paintText(line, branchIcon, x, width, C.sectionHeader);
|
|
391
|
-
x = this.paintText(line, this.data.branch, x, width, C.branch, { bold: true });
|
|
392
|
-
|
|
393
|
-
if (this.data.ahead > 0) {
|
|
394
|
-
x = this.paintText(line, ` +${this.data.ahead}`, x, width, C.good);
|
|
395
|
-
}
|
|
396
|
-
if (this.data.behind > 0) {
|
|
397
|
-
x = this.paintText(line, ` -${this.data.behind}`, x, width, C.bad);
|
|
398
|
-
}
|
|
399
|
-
|
|
400
568
|
const isDirty = this.data.stagedFiles.length > 0 || this.data.unstagedFiles.length > 0;
|
|
401
|
-
const
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
569
|
+
const segments: Array<{ text: string; fg: string; bold?: boolean }> = [
|
|
570
|
+
{ text: ' ⎇ ', fg: C.sectionHeader },
|
|
571
|
+
{ text: this.data.branch, fg: C.branch, bold: true },
|
|
572
|
+
];
|
|
573
|
+
if (this.data.ahead > 0) segments.push({ text: ` ↑${this.data.ahead}`, fg: C.good });
|
|
574
|
+
if (this.data.behind > 0) segments.push({ text: ` ↓${this.data.behind}`, fg: C.bad });
|
|
575
|
+
if (this.data.ahead === 0 && this.data.behind === 0) {
|
|
576
|
+
segments.push({ text: ' ≡ up to date', fg: C.dim });
|
|
577
|
+
}
|
|
578
|
+
segments.push({
|
|
579
|
+
text: isDirty ? ' ● dirty' : ' ✓ clean',
|
|
580
|
+
fg: isDirty ? C.warn : C.good,
|
|
581
|
+
bold: true,
|
|
582
|
+
});
|
|
583
|
+
segments.push({ text: ` ${this.data.stagedFiles.length} staged`, fg: this.data.stagedFiles.length > 0 ? C.good : C.dim });
|
|
584
|
+
segments.push({ text: ` · ${this.data.unstagedFiles.length} unstaged`, fg: this.data.unstagedFiles.length > 0 ? C.warn : C.dim });
|
|
585
|
+
return buildStyledPanelLine(width, segments);
|
|
406
586
|
}
|
|
407
587
|
|
|
408
588
|
private renderSectionHeader(label: string, width: number): Line {
|
|
409
|
-
return buildStyledPanelLine(width, [{ text:
|
|
589
|
+
return buildStyledPanelLine(width, [{ text: ` ◆ ${label}`, fg: C.sectionHeader, bold: true }]);
|
|
410
590
|
}
|
|
411
591
|
|
|
412
592
|
private renderFileRow(entry: GitFileEntry, selected: boolean, width: number): Line {
|
|
413
|
-
const line = createEmptyLine(width);
|
|
414
593
|
const fg = entry.staged ? C.good : C.bad;
|
|
415
|
-
|
|
416
|
-
const
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
for (const ch of label) {
|
|
425
|
-
if (x >= width) break;
|
|
426
|
-
line[x++] = createStyledCell(ch, { fg, bg: C.selected, bold: true });
|
|
427
|
-
}
|
|
428
|
-
} else {
|
|
429
|
-
this.paintText(line, label, 0, width, fg);
|
|
430
|
-
}
|
|
431
|
-
return line;
|
|
594
|
+
// Single-char status marker: + staged (ready to commit), M modified/unstaged.
|
|
595
|
+
const statusGlyph = entry.staged ? '+' : 'M';
|
|
596
|
+
const sessionMarker = entry.sessionChanged ? '● ' : ' ';
|
|
597
|
+
const path = truncateDisplay(entry.path, Math.max(0, width - 8));
|
|
598
|
+
return buildSelectablePanelLine(width, [
|
|
599
|
+
{ text: ` ${statusGlyph} `, fg, bg: selected ? C.selected : undefined, bold: true },
|
|
600
|
+
{ text: sessionMarker, fg: selected ? C.selectedFg : C.sessionChanged, bg: selected ? C.selected : undefined },
|
|
601
|
+
{ text: path, fg: selected ? C.selectedFg : fg, bg: selected ? C.selected : undefined, bold: selected },
|
|
602
|
+
], { selected, selectedBg: C.selected, fillFg: selected ? C.selectedFg : '', leadingMarker: '▸' });
|
|
432
603
|
}
|
|
433
604
|
|
|
434
605
|
private renderCommitRow(entry: CommitEntry, selected: boolean, width: number): Line {
|
|
435
|
-
const
|
|
436
|
-
const
|
|
437
|
-
const msgPart = entry.message
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
}
|
|
443
|
-
let x = 0;
|
|
444
|
-
for (const ch of hashPart) {
|
|
445
|
-
if (x >= width) break;
|
|
446
|
-
line[x++] = createStyledCell(ch, { fg: C.commitHash, bg: C.selected });
|
|
447
|
-
}
|
|
448
|
-
for (const ch of msgPart) {
|
|
449
|
-
if (x >= width) break;
|
|
450
|
-
line[x++] = createStyledCell(ch, { fg: C.selectedFg, bg: C.selected });
|
|
451
|
-
}
|
|
452
|
-
} else {
|
|
453
|
-
let x = this.paintText(line, hashPart, 0, width, C.commitHash);
|
|
454
|
-
this.paintText(line, msgPart, x, width, C.commit);
|
|
455
|
-
}
|
|
456
|
-
return line;
|
|
606
|
+
const hashPart = ` ${entry.hash} `;
|
|
607
|
+
const msgBudget = Math.max(0, width - getDisplayWidth(hashPart) - 3);
|
|
608
|
+
const msgPart = truncateDisplay(entry.message, msgBudget);
|
|
609
|
+
return buildSelectablePanelLine(width, [
|
|
610
|
+
{ text: hashPart, fg: selected ? C.selectedFg : C.commitHash, bg: selected ? C.selected : undefined },
|
|
611
|
+
{ text: msgPart, fg: selected ? C.selectedFg : C.commit, bg: selected ? C.selected : undefined, bold: selected },
|
|
612
|
+
], { selected, selectedBg: C.selected, fillFg: selected ? C.selectedFg : '', leadingMarker: '▸' });
|
|
457
613
|
}
|
|
458
614
|
|
|
459
615
|
private renderList(width: number, height: number): Line[] {
|
|
616
|
+
const intro = 'Review branch status, staged and unstaged files, and recent commits. Open a file or commit row to inspect its diff.';
|
|
617
|
+
const footerHints = [
|
|
618
|
+
buildKeyboardHints(width, [
|
|
619
|
+
{ keys: '↑/↓', label: 'navigate' },
|
|
620
|
+
{ keys: 'Enter', label: 'open diff →' },
|
|
621
|
+
{ keys: 's/u', label: 'stage/unstage' },
|
|
622
|
+
{ keys: 'c', label: 'commit' },
|
|
623
|
+
{ keys: 'r', label: 'refresh' },
|
|
624
|
+
], DEFAULT_PANEL_PALETTE),
|
|
625
|
+
];
|
|
626
|
+
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); // WO-160: was '/git status' (chat-only, never touches this panel)
|
|
460
627
|
const rows: Line[] = [];
|
|
461
628
|
for (let i = 0; i < this.items.length; i++) {
|
|
462
629
|
const item = this.items[i];
|
|
@@ -487,7 +654,7 @@ export class GitPanel extends BasePanel {
|
|
|
487
654
|
const selectedRowIndex = this.getRowIndexForItem(this.selectedIndex);
|
|
488
655
|
if (selectedRowIndex >= 0) {
|
|
489
656
|
const isDirty = this.data.stagedFiles.length > 0 || this.data.unstagedFiles.length > 0;
|
|
490
|
-
const selectedItem = this.items
|
|
657
|
+
const selectedItem = this.items.at(this.selectedIndex);
|
|
491
658
|
const selectedLines: Line[] = [];
|
|
492
659
|
if (selectedItem?.kind === 'file') {
|
|
493
660
|
selectedLines.push(buildPanelLine(width, [
|
|
@@ -526,10 +693,8 @@ export class GitPanel extends BasePanel {
|
|
|
526
693
|
} as const;
|
|
527
694
|
const selectedSection = { title: 'Selected', lines: selectedLines } as const;
|
|
528
695
|
const workspaceSection = resolveScrollablePanelSection(width, height, {
|
|
529
|
-
intro
|
|
530
|
-
footerLines:
|
|
531
|
-
buildPanelLine(width, [[' Up/Down', DEFAULT_PANEL_PALETTE.info], [' navigate', DEFAULT_PANEL_PALETTE.dim], [' Enter', DEFAULT_PANEL_PALETTE.info], [' diff', DEFAULT_PANEL_PALETTE.dim], [' r', DEFAULT_PANEL_PALETTE.info], [' refresh', DEFAULT_PANEL_PALETTE.dim]]),
|
|
532
|
-
],
|
|
696
|
+
intro,
|
|
697
|
+
footerLines: footerHints,
|
|
533
698
|
palette: DEFAULT_PANEL_PALETTE,
|
|
534
699
|
beforeSections: [summarySection],
|
|
535
700
|
section: {
|
|
@@ -545,26 +710,20 @@ export class GitPanel extends BasePanel {
|
|
|
545
710
|
|
|
546
711
|
return buildPanelWorkspace(width, height, {
|
|
547
712
|
title: ' Git',
|
|
548
|
-
intro
|
|
713
|
+
intro,
|
|
549
714
|
sections: [
|
|
550
715
|
summarySection,
|
|
551
|
-
workspaceSection.section.lines.length > 0 ? workspaceSection.section : { title: 'Workspace', lines:
|
|
716
|
+
workspaceSection.section.lines.length > 0 ? workspaceSection.section : { title: 'Workspace', lines: emptyStateLines },
|
|
552
717
|
selectedSection,
|
|
553
718
|
],
|
|
554
|
-
footerLines:
|
|
555
|
-
buildPanelLine(width, [[' Up/Down', DEFAULT_PANEL_PALETTE.info], [' navigate', DEFAULT_PANEL_PALETTE.dim], [' Enter', DEFAULT_PANEL_PALETTE.info], [' diff', DEFAULT_PANEL_PALETTE.dim], [' r', DEFAULT_PANEL_PALETTE.info], [' refresh', DEFAULT_PANEL_PALETTE.dim]]),
|
|
556
|
-
],
|
|
719
|
+
footerLines: footerHints,
|
|
557
720
|
palette: DEFAULT_PANEL_PALETTE,
|
|
558
721
|
});
|
|
559
722
|
}
|
|
560
723
|
return buildPanelWorkspace(width, height, {
|
|
561
724
|
title: ' Git',
|
|
562
|
-
intro
|
|
563
|
-
sections: [
|
|
564
|
-
{
|
|
565
|
-
lines: buildEmptyState(width, ' No git rows', 'This repository has no files or commits to display yet.', [], DEFAULT_PANEL_PALETTE),
|
|
566
|
-
},
|
|
567
|
-
],
|
|
725
|
+
intro,
|
|
726
|
+
sections: [{ lines: emptyStateLines }],
|
|
568
727
|
palette: DEFAULT_PANEL_PALETTE,
|
|
569
728
|
});
|
|
570
729
|
}
|
|
@@ -587,10 +746,20 @@ export class GitPanel extends BasePanel {
|
|
|
587
746
|
}
|
|
588
747
|
|
|
589
748
|
private renderDiff(width: number, height: number): Line[] {
|
|
590
|
-
const item = this.items
|
|
591
|
-
const title =
|
|
592
|
-
item?.kind === 'file' ? `Diff: ${item.entry.path}` : 'Diff';
|
|
749
|
+
const item = this.items.at(this.selectedIndex);
|
|
593
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';
|
|
594
763
|
const renderedLines = diffLines.map((rawLine) => {
|
|
595
764
|
const dLine = createEmptyLine(width);
|
|
596
765
|
let fg: string;
|
|
@@ -607,7 +776,7 @@ export class GitPanel extends BasePanel {
|
|
|
607
776
|
return dLine;
|
|
608
777
|
});
|
|
609
778
|
const footerLines = [
|
|
610
|
-
|
|
779
|
+
buildKeyboardHints(width, [{ keys: '↑/↓', label: 'scroll' }, { keys: 'Esc/q', label: 'back to files' }], DEFAULT_PANEL_PALETTE),
|
|
611
780
|
];
|
|
612
781
|
const diffSection = resolveScrollablePanelSection(width, height, {
|
|
613
782
|
palette: DEFAULT_PANEL_PALETTE,
|