diffstalker 0.2.1 → 0.2.3
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/.dependency-cruiser.cjs +67 -0
- package/.githooks/pre-commit +2 -0
- package/.githooks/pre-push +15 -0
- package/README.md +43 -35
- package/bun.lock +60 -4
- package/dist/App.js +495 -131
- package/dist/KeyBindings.js +134 -10
- package/dist/MouseHandlers.js +67 -20
- package/dist/core/ExplorerStateManager.js +37 -75
- package/dist/core/GitStateManager.js +252 -46
- package/dist/git/diff.js +99 -18
- package/dist/git/status.js +111 -54
- package/dist/git/test-helpers.js +67 -0
- package/dist/index.js +54 -43
- package/dist/ipc/CommandClient.js +6 -7
- package/dist/state/UIState.js +22 -0
- package/dist/types/remote.js +5 -0
- package/dist/ui/PaneRenderers.js +45 -15
- package/dist/ui/modals/BranchPicker.js +157 -0
- package/dist/ui/modals/CommitActionConfirm.js +66 -0
- package/dist/ui/modals/FileFinder.js +45 -75
- package/dist/ui/modals/HotkeysModal.js +35 -3
- package/dist/ui/modals/SoftResetConfirm.js +68 -0
- package/dist/ui/modals/StashListModal.js +98 -0
- package/dist/ui/modals/ThemePicker.js +1 -2
- package/dist/ui/widgets/CommitPanel.js +113 -7
- package/dist/ui/widgets/CompareListView.js +44 -23
- package/dist/ui/widgets/DiffView.js +216 -170
- package/dist/ui/widgets/ExplorerView.js +50 -54
- package/dist/ui/widgets/FileList.js +62 -95
- package/dist/ui/widgets/FlatFileList.js +65 -0
- package/dist/ui/widgets/Footer.js +25 -15
- package/dist/ui/widgets/Header.js +51 -9
- package/dist/ui/widgets/fileRowFormatters.js +73 -0
- package/dist/utils/ansiTruncate.js +0 -1
- package/dist/utils/displayRows.js +101 -21
- package/dist/utils/flatFileList.js +67 -0
- package/dist/utils/layoutCalculations.js +5 -3
- package/eslint.metrics.js +0 -1
- package/metrics/v0.2.2.json +229 -0
- package/metrics/v0.2.3.json +243 -0
- package/package.json +10 -3
package/dist/App.js
CHANGED
|
@@ -15,11 +15,17 @@ import { HotkeysModal } from './ui/modals/HotkeysModal.js';
|
|
|
15
15
|
import { BaseBranchPicker } from './ui/modals/BaseBranchPicker.js';
|
|
16
16
|
import { DiscardConfirm } from './ui/modals/DiscardConfirm.js';
|
|
17
17
|
import { FileFinder } from './ui/modals/FileFinder.js';
|
|
18
|
+
import { StashListModal } from './ui/modals/StashListModal.js';
|
|
19
|
+
import { BranchPicker } from './ui/modals/BranchPicker.js';
|
|
20
|
+
import { SoftResetConfirm } from './ui/modals/SoftResetConfirm.js';
|
|
21
|
+
import { CommitActionConfirm } from './ui/modals/CommitActionConfirm.js';
|
|
18
22
|
import { CommitFlowState } from './state/CommitFlowState.js';
|
|
19
23
|
import { UIState } from './state/UIState.js';
|
|
20
24
|
import { getManagerForRepo, removeManagerForRepo, } from './core/GitStateManager.js';
|
|
21
25
|
import { saveConfig } from './config.js';
|
|
22
26
|
import { getCategoryForIndex, getIndexForCategoryPosition, } from './utils/fileCategories.js';
|
|
27
|
+
import { buildFlatFileList, getFlatFileAtIndex, getFlatFileIndexByPath, } from './utils/flatFileList.js';
|
|
28
|
+
import { extractHunkPatch } from './git/diff.js';
|
|
23
29
|
/**
|
|
24
30
|
* Main application controller.
|
|
25
31
|
* Coordinates between GitStateManager, UIState, and blessed widgets.
|
|
@@ -41,10 +47,19 @@ export class App {
|
|
|
41
47
|
commitTextarea = null;
|
|
42
48
|
// Active modals
|
|
43
49
|
activeModal = null;
|
|
44
|
-
//
|
|
50
|
+
// Auto-clear timer for remote operation status
|
|
51
|
+
remoteClearTimer = null;
|
|
52
|
+
// Cached total rows and hunk info for scroll bounds (single source of truth from render)
|
|
45
53
|
bottomPaneTotalRows = 0;
|
|
54
|
+
bottomPaneHunkCount = 0;
|
|
55
|
+
bottomPaneHunkBoundaries = [];
|
|
46
56
|
// Selection anchor: remembers category + position before stage/unstage
|
|
47
57
|
pendingSelectionAnchor = null;
|
|
58
|
+
// Flat view mode state
|
|
59
|
+
cachedFlatFiles = [];
|
|
60
|
+
pendingFlatSelectionPath = null;
|
|
61
|
+
pendingHunkIndex = null;
|
|
62
|
+
combinedHunkMapping = [];
|
|
48
63
|
constructor(options) {
|
|
49
64
|
this.config = options.config;
|
|
50
65
|
this.commandServer = options.commandServer ?? null;
|
|
@@ -158,18 +173,33 @@ export class App {
|
|
|
158
173
|
toggleFollow: () => this.toggleFollow(),
|
|
159
174
|
showDiscardConfirm: (file) => this.showDiscardConfirm(file),
|
|
160
175
|
render: () => this.render(),
|
|
176
|
+
toggleCurrentHunk: () => this.toggleCurrentHunk(),
|
|
177
|
+
navigateNextHunk: () => this.navigateNextHunk(),
|
|
178
|
+
navigatePrevHunk: () => this.navigatePrevHunk(),
|
|
179
|
+
push: () => this.gitManager?.push(),
|
|
180
|
+
fetchRemote: () => this.gitManager?.fetchRemote(),
|
|
181
|
+
pullRebase: () => this.gitManager?.pullRebase(),
|
|
182
|
+
stash: () => this.gitManager?.stash(),
|
|
183
|
+
stashPop: () => this.gitManager?.stashPop(),
|
|
184
|
+
openStashListModal: () => this.openStashListModal(),
|
|
185
|
+
openBranchPicker: () => this.openBranchPicker(),
|
|
186
|
+
showSoftResetConfirm: () => this.showSoftResetConfirm(),
|
|
187
|
+
cherryPickSelected: () => this.cherryPickSelected(),
|
|
188
|
+
revertSelected: () => this.revertSelected(),
|
|
161
189
|
}, {
|
|
162
190
|
hasActiveModal: () => this.activeModal !== null,
|
|
163
191
|
getBottomTab: () => this.uiState.state.bottomTab,
|
|
164
192
|
getCurrentPane: () => this.uiState.state.currentPane,
|
|
165
193
|
isCommitInputFocused: () => this.commitFlowState.state.inputFocused,
|
|
194
|
+
isRemoteInProgress: () => this.gitManager?.remoteState.inProgress ?? false,
|
|
166
195
|
getStatusFiles: () => this.gitManager?.state.status?.files ?? [],
|
|
167
196
|
getSelectedIndex: () => this.uiState.state.selectedIndex,
|
|
168
197
|
uiState: this.uiState,
|
|
169
|
-
|
|
198
|
+
getExplorerManager: () => this.explorerManager,
|
|
170
199
|
commitFlowState: this.commitFlowState,
|
|
171
|
-
|
|
200
|
+
getGitManager: () => this.gitManager,
|
|
172
201
|
layout: this.layout,
|
|
202
|
+
getCachedFlatFiles: () => this.cachedFlatFiles,
|
|
173
203
|
});
|
|
174
204
|
}
|
|
175
205
|
setupMouseEventHandlers() {
|
|
@@ -178,30 +208,59 @@ export class App {
|
|
|
178
208
|
selectCompareItem: (selection) => this.selectCompareItem(selection),
|
|
179
209
|
selectFileByIndex: (index) => this.selectFileByIndex(index),
|
|
180
210
|
toggleFileByIndex: (index) => this.toggleFileByIndex(index),
|
|
211
|
+
enterExplorerDirectory: () => this.enterExplorerDirectory(),
|
|
181
212
|
toggleMouseMode: () => this.toggleMouseMode(),
|
|
182
213
|
toggleFollow: () => this.toggleFollow(),
|
|
214
|
+
selectHunkAtRow: (row) => this.selectHunkAtRow(row),
|
|
215
|
+
focusCommitInput: () => this.focusCommitInput(),
|
|
216
|
+
toggleAmend: () => {
|
|
217
|
+
this.commitFlowState.toggleAmend();
|
|
218
|
+
this.render();
|
|
219
|
+
},
|
|
183
220
|
render: () => this.render(),
|
|
184
221
|
}, {
|
|
185
222
|
uiState: this.uiState,
|
|
186
|
-
|
|
223
|
+
getExplorerManager: () => this.explorerManager,
|
|
187
224
|
getStatusFiles: () => this.gitManager?.state.status?.files ?? [],
|
|
188
225
|
getHistoryCommitCount: () => this.gitManager?.historyState.commits.length ?? 0,
|
|
189
226
|
getCompareCommits: () => this.gitManager?.compareState?.compareDiff?.commits ?? [],
|
|
190
227
|
getCompareFiles: () => this.gitManager?.compareState?.compareDiff?.files ?? [],
|
|
191
228
|
getBottomPaneTotalRows: () => this.bottomPaneTotalRows,
|
|
192
229
|
getScreenWidth: () => this.screen.width || 80,
|
|
230
|
+
getCachedFlatFiles: () => this.cachedFlatFiles,
|
|
193
231
|
});
|
|
194
232
|
}
|
|
233
|
+
/**
|
|
234
|
+
* Toggle staging for a flat file entry (stage if unstaged/partial, unstage if fully staged).
|
|
235
|
+
*/
|
|
236
|
+
async toggleFlatEntry(entry) {
|
|
237
|
+
this.pendingFlatSelectionPath = entry.path;
|
|
238
|
+
if (entry.stagingState === 'staged') {
|
|
239
|
+
if (entry.stagedEntry)
|
|
240
|
+
await this.gitManager?.unstage(entry.stagedEntry);
|
|
241
|
+
}
|
|
242
|
+
else {
|
|
243
|
+
if (entry.unstagedEntry)
|
|
244
|
+
await this.gitManager?.stage(entry.unstagedEntry);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
195
247
|
async toggleFileByIndex(index) {
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
248
|
+
if (this.uiState.state.flatViewMode) {
|
|
249
|
+
const flatEntry = getFlatFileAtIndex(this.cachedFlatFiles, index);
|
|
250
|
+
if (flatEntry)
|
|
251
|
+
await this.toggleFlatEntry(flatEntry);
|
|
252
|
+
}
|
|
253
|
+
else {
|
|
254
|
+
const files = this.gitManager?.state.status?.files ?? [];
|
|
255
|
+
const file = getFileAtIndex(files, index);
|
|
256
|
+
if (file) {
|
|
257
|
+
this.pendingSelectionAnchor = getCategoryForIndex(files, this.uiState.state.selectedIndex);
|
|
258
|
+
if (file.staged) {
|
|
259
|
+
await this.gitManager?.unstage(file);
|
|
260
|
+
}
|
|
261
|
+
else {
|
|
262
|
+
await this.gitManager?.stage(file);
|
|
263
|
+
}
|
|
205
264
|
}
|
|
206
265
|
}
|
|
207
266
|
}
|
|
@@ -212,6 +271,10 @@ export class App {
|
|
|
212
271
|
});
|
|
213
272
|
// Load data when switching tabs
|
|
214
273
|
this.uiState.on('tab-change', (tab) => {
|
|
274
|
+
// Reset hunk selection when leaving diff tab
|
|
275
|
+
if (tab !== 'diff') {
|
|
276
|
+
this.uiState.setSelectedHunkIndex(0);
|
|
277
|
+
}
|
|
215
278
|
if (tab === 'history') {
|
|
216
279
|
this.gitManager?.loadHistory();
|
|
217
280
|
}
|
|
@@ -224,6 +287,13 @@ export class App {
|
|
|
224
287
|
this.explorerManager?.loadDirectory('');
|
|
225
288
|
}
|
|
226
289
|
}
|
|
290
|
+
else if (tab === 'commit') {
|
|
291
|
+
this.gitManager?.loadStashList();
|
|
292
|
+
// Also load history if needed for HEAD commit display
|
|
293
|
+
if (!this.gitManager?.historyState.commits.length) {
|
|
294
|
+
this.gitManager?.loadHistory();
|
|
295
|
+
}
|
|
296
|
+
}
|
|
227
297
|
});
|
|
228
298
|
// Handle modal opening/closing
|
|
229
299
|
this.uiState.on('modal-change', (modal) => {
|
|
@@ -305,23 +375,11 @@ export class App {
|
|
|
305
375
|
this.gitManager = getManagerForRepo(this.repoPath);
|
|
306
376
|
// Listen to state changes
|
|
307
377
|
this.gitManager.on('state-change', () => {
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
this.pendingSelectionAnchor = null;
|
|
313
|
-
const newIndex = getIndexForCategoryPosition(files, anchor.category, anchor.categoryIndex);
|
|
314
|
-
this.uiState.setSelectedIndex(newIndex);
|
|
315
|
-
this.selectFileByIndex(newIndex);
|
|
316
|
-
}
|
|
317
|
-
else if (files.length > 0) {
|
|
318
|
-
// Default: clamp selected index to valid range
|
|
319
|
-
const maxIndex = files.length - 1;
|
|
320
|
-
if (this.uiState.state.selectedIndex > maxIndex) {
|
|
321
|
-
this.uiState.setSelectedIndex(maxIndex);
|
|
322
|
-
}
|
|
378
|
+
// Skip reconciliation while loading — the pending anchor must wait
|
|
379
|
+
// for the new status to arrive before being consumed
|
|
380
|
+
if (!this.gitManager?.state.isLoading) {
|
|
381
|
+
this.reconcileSelectionAfterStateChange();
|
|
323
382
|
}
|
|
324
|
-
// Update explorer git status when git state changes
|
|
325
383
|
this.updateExplorerGitStatus();
|
|
326
384
|
this.render();
|
|
327
385
|
});
|
|
@@ -341,12 +399,73 @@ export class App {
|
|
|
341
399
|
this.gitManager.on('compare-selection-change', () => {
|
|
342
400
|
this.render();
|
|
343
401
|
});
|
|
402
|
+
this.gitManager.on('remote-state-change', (remoteState) => {
|
|
403
|
+
// Auto-clear success after 3s, error after 5s
|
|
404
|
+
if (this.remoteClearTimer)
|
|
405
|
+
clearTimeout(this.remoteClearTimer);
|
|
406
|
+
if (remoteState.lastResult && !remoteState.inProgress) {
|
|
407
|
+
this.remoteClearTimer = setTimeout(() => {
|
|
408
|
+
this.gitManager?.clearRemoteState();
|
|
409
|
+
}, 3000);
|
|
410
|
+
}
|
|
411
|
+
else if (remoteState.error) {
|
|
412
|
+
this.remoteClearTimer = setTimeout(() => {
|
|
413
|
+
this.gitManager?.clearRemoteState();
|
|
414
|
+
}, 5000);
|
|
415
|
+
}
|
|
416
|
+
this.render();
|
|
417
|
+
});
|
|
344
418
|
// Start watching and do initial refresh
|
|
345
419
|
this.gitManager.startWatching();
|
|
346
420
|
this.gitManager.refresh();
|
|
347
421
|
// Initialize explorer manager
|
|
348
422
|
this.initExplorerManager();
|
|
349
423
|
}
|
|
424
|
+
/**
|
|
425
|
+
* After git state changes, reconcile the selected file index.
|
|
426
|
+
* Handles both flat mode (path-based anchoring) and categorized mode (category-based anchoring).
|
|
427
|
+
*/
|
|
428
|
+
reconcileSelectionAfterStateChange() {
|
|
429
|
+
const files = this.gitManager?.state.status?.files ?? [];
|
|
430
|
+
if (this.uiState.state.flatViewMode && this.pendingFlatSelectionPath) {
|
|
431
|
+
const flatFiles = buildFlatFileList(files, this.gitManager?.state.hunkCounts ?? null);
|
|
432
|
+
const targetPath = this.pendingFlatSelectionPath;
|
|
433
|
+
this.pendingFlatSelectionPath = null;
|
|
434
|
+
const newIndex = getFlatFileIndexByPath(flatFiles, targetPath);
|
|
435
|
+
if (newIndex >= 0) {
|
|
436
|
+
this.uiState.setSelectedIndex(newIndex);
|
|
437
|
+
this.selectFileByIndex(newIndex);
|
|
438
|
+
}
|
|
439
|
+
else if (flatFiles.length > 0) {
|
|
440
|
+
const clamped = Math.min(this.uiState.state.selectedIndex, flatFiles.length - 1);
|
|
441
|
+
this.uiState.setSelectedIndex(clamped);
|
|
442
|
+
this.selectFileByIndex(clamped);
|
|
443
|
+
}
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
if (this.pendingSelectionAnchor) {
|
|
447
|
+
const anchor = this.pendingSelectionAnchor;
|
|
448
|
+
this.pendingSelectionAnchor = null;
|
|
449
|
+
const newIndex = getIndexForCategoryPosition(files, anchor.category, anchor.categoryIndex);
|
|
450
|
+
this.uiState.setSelectedIndex(newIndex);
|
|
451
|
+
this.selectFileByIndex(newIndex);
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
// No pending anchor — just clamp to valid range
|
|
455
|
+
if (this.uiState.state.flatViewMode) {
|
|
456
|
+
const flatFiles = buildFlatFileList(files, this.gitManager?.state.hunkCounts ?? null);
|
|
457
|
+
const maxIndex = flatFiles.length - 1;
|
|
458
|
+
if (maxIndex >= 0 && this.uiState.state.selectedIndex > maxIndex) {
|
|
459
|
+
this.uiState.setSelectedIndex(maxIndex);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
else if (files.length > 0) {
|
|
463
|
+
const maxIndex = files.length - 1;
|
|
464
|
+
if (this.uiState.state.selectedIndex > maxIndex) {
|
|
465
|
+
this.uiState.setSelectedIndex(maxIndex);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
}
|
|
350
469
|
initExplorerManager() {
|
|
351
470
|
// Clean up existing manager
|
|
352
471
|
if (this.explorerManager) {
|
|
@@ -365,6 +484,8 @@ export class App {
|
|
|
365
484
|
});
|
|
366
485
|
// Load root directory
|
|
367
486
|
this.explorerManager.loadDirectory('');
|
|
487
|
+
// Pre-load file paths for file finder (runs in background)
|
|
488
|
+
this.explorerManager.loadFilePaths();
|
|
368
489
|
// Update git status after tree is loaded
|
|
369
490
|
this.updateExplorerGitStatus();
|
|
370
491
|
}
|
|
@@ -469,93 +590,98 @@ export class App {
|
|
|
469
590
|
};
|
|
470
591
|
}
|
|
471
592
|
// Navigation methods
|
|
472
|
-
|
|
593
|
+
/**
|
|
594
|
+
* Scroll the content pane (diff or explorer file content) by delta lines.
|
|
595
|
+
*/
|
|
596
|
+
scrollActiveDiffPane(delta) {
|
|
473
597
|
const state = this.uiState.state;
|
|
474
|
-
if (state.bottomTab === '
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
}
|
|
478
|
-
else if (state.currentPane === 'diff') {
|
|
479
|
-
this.uiState.setDiffScrollOffset(Math.max(0, state.diffScrollOffset - 3));
|
|
480
|
-
}
|
|
481
|
-
return;
|
|
598
|
+
if (state.bottomTab === 'explorer') {
|
|
599
|
+
const newOffset = Math.max(0, state.explorerFileScrollOffset + delta);
|
|
600
|
+
this.uiState.setExplorerFileScrollOffset(newOffset);
|
|
482
601
|
}
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
}
|
|
487
|
-
else if (state.currentPane === 'diff') {
|
|
488
|
-
this.uiState.setDiffScrollOffset(Math.max(0, state.diffScrollOffset - 3));
|
|
489
|
-
}
|
|
490
|
-
return;
|
|
602
|
+
else {
|
|
603
|
+
const newOffset = Math.max(0, state.diffScrollOffset + delta);
|
|
604
|
+
this.uiState.setDiffScrollOffset(newOffset);
|
|
491
605
|
}
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
606
|
+
}
|
|
607
|
+
/**
|
|
608
|
+
* Navigate the file list by one item and keep selection visible.
|
|
609
|
+
*/
|
|
610
|
+
navigateFileList(direction) {
|
|
611
|
+
const state = this.uiState.state;
|
|
612
|
+
const files = this.gitManager?.state.status?.files ?? [];
|
|
613
|
+
// Determine max index based on view mode
|
|
614
|
+
const maxIndex = state.flatViewMode ? this.cachedFlatFiles.length - 1 : files.length - 1;
|
|
615
|
+
if (maxIndex < 0)
|
|
499
616
|
return;
|
|
617
|
+
const newIndex = direction === -1
|
|
618
|
+
? Math.max(0, state.selectedIndex - 1)
|
|
619
|
+
: Math.min(maxIndex, state.selectedIndex + 1);
|
|
620
|
+
this.uiState.setSelectedIndex(newIndex);
|
|
621
|
+
this.selectFileByIndex(newIndex);
|
|
622
|
+
// In flat mode row === index + 1 (header row); in categorized mode account for headers/spacers
|
|
623
|
+
const row = state.flatViewMode ? newIndex + 1 : getRowFromFileIndex(newIndex, files);
|
|
624
|
+
this.scrollToKeepRowVisible(row, direction, state.fileListScrollOffset);
|
|
625
|
+
}
|
|
626
|
+
/**
|
|
627
|
+
* Scroll the file list to keep a given row visible.
|
|
628
|
+
*/
|
|
629
|
+
scrollToKeepRowVisible(row, direction, currentOffset) {
|
|
630
|
+
if (direction === -1 && row < currentOffset) {
|
|
631
|
+
this.uiState.setFileListScrollOffset(row);
|
|
500
632
|
}
|
|
501
|
-
if (
|
|
502
|
-
const
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
this.selectFileByIndex(newIndex);
|
|
506
|
-
// Keep selection visible - scroll up if needed
|
|
507
|
-
const row = getRowFromFileIndex(newIndex, files);
|
|
508
|
-
if (row < state.fileListScrollOffset) {
|
|
509
|
-
this.uiState.setFileListScrollOffset(row);
|
|
633
|
+
else if (direction === 1) {
|
|
634
|
+
const visibleEnd = currentOffset + this.layout.dimensions.topPaneHeight - 1;
|
|
635
|
+
if (row >= visibleEnd) {
|
|
636
|
+
this.uiState.setFileListScrollOffset(currentOffset + (row - visibleEnd + 1));
|
|
510
637
|
}
|
|
511
638
|
}
|
|
512
|
-
else if (state.currentPane === 'diff') {
|
|
513
|
-
this.uiState.setDiffScrollOffset(Math.max(0, state.diffScrollOffset - 3));
|
|
514
|
-
}
|
|
515
639
|
}
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
640
|
+
/**
|
|
641
|
+
* Navigate the active list pane by one item in the given direction.
|
|
642
|
+
*/
|
|
643
|
+
navigateActiveList(direction) {
|
|
644
|
+
const tab = this.uiState.state.bottomTab;
|
|
645
|
+
if (tab === 'history') {
|
|
646
|
+
if (direction === -1)
|
|
647
|
+
this.navigateHistoryUp();
|
|
648
|
+
else
|
|
521
649
|
this.navigateHistoryDown();
|
|
522
|
-
}
|
|
523
|
-
else if (state.currentPane === 'diff') {
|
|
524
|
-
this.uiState.setDiffScrollOffset(state.diffScrollOffset + 3);
|
|
525
|
-
}
|
|
526
|
-
return;
|
|
527
650
|
}
|
|
528
|
-
if (
|
|
529
|
-
if (
|
|
651
|
+
else if (tab === 'compare') {
|
|
652
|
+
if (direction === -1)
|
|
653
|
+
this.navigateCompareUp();
|
|
654
|
+
else
|
|
530
655
|
this.navigateCompareDown();
|
|
531
|
-
}
|
|
532
|
-
else if (state.currentPane === 'diff') {
|
|
533
|
-
this.uiState.setDiffScrollOffset(state.diffScrollOffset + 3);
|
|
534
|
-
}
|
|
535
|
-
return;
|
|
536
656
|
}
|
|
537
|
-
if (
|
|
538
|
-
if (
|
|
657
|
+
else if (tab === 'explorer') {
|
|
658
|
+
if (direction === -1)
|
|
659
|
+
this.navigateExplorerUp();
|
|
660
|
+
else
|
|
539
661
|
this.navigateExplorerDown();
|
|
540
|
-
}
|
|
541
|
-
else if (state.currentPane === 'diff') {
|
|
542
|
-
this.uiState.setExplorerFileScrollOffset(state.explorerFileScrollOffset + 3);
|
|
543
|
-
}
|
|
544
|
-
return;
|
|
545
662
|
}
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
this.uiState.setSelectedIndex(newIndex);
|
|
549
|
-
this.selectFileByIndex(newIndex);
|
|
550
|
-
// Keep selection visible - scroll down if needed
|
|
551
|
-
const row = getRowFromFileIndex(newIndex, files);
|
|
552
|
-
const visibleEnd = state.fileListScrollOffset + this.layout.dimensions.topPaneHeight - 1;
|
|
553
|
-
if (row >= visibleEnd) {
|
|
554
|
-
this.uiState.setFileListScrollOffset(state.fileListScrollOffset + (row - visibleEnd + 1));
|
|
555
|
-
}
|
|
663
|
+
else {
|
|
664
|
+
this.navigateFileList(direction);
|
|
556
665
|
}
|
|
557
|
-
|
|
558
|
-
|
|
666
|
+
}
|
|
667
|
+
navigateUp() {
|
|
668
|
+
const state = this.uiState.state;
|
|
669
|
+
const isListPane = state.currentPane !== 'diff';
|
|
670
|
+
if (isListPane) {
|
|
671
|
+
this.navigateActiveList(-1);
|
|
672
|
+
}
|
|
673
|
+
else {
|
|
674
|
+
this.scrollActiveDiffPane(-3);
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
navigateDown() {
|
|
678
|
+
const state = this.uiState.state;
|
|
679
|
+
const isListPane = state.currentPane !== 'diff';
|
|
680
|
+
if (isListPane) {
|
|
681
|
+
this.navigateActiveList(1);
|
|
682
|
+
}
|
|
683
|
+
else {
|
|
684
|
+
this.scrollActiveDiffPane(3);
|
|
559
685
|
}
|
|
560
686
|
}
|
|
561
687
|
navigateHistoryUp() {
|
|
@@ -691,12 +817,26 @@ export class App {
|
|
|
691
817
|
this.uiState.setExplorerSelectedIndex(this.explorerManager?.state.selectedIndex ?? 0);
|
|
692
818
|
}
|
|
693
819
|
selectFileByIndex(index) {
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
820
|
+
if (this.uiState.state.flatViewMode) {
|
|
821
|
+
const flatEntry = getFlatFileAtIndex(this.cachedFlatFiles, index);
|
|
822
|
+
if (flatEntry) {
|
|
823
|
+
// Prefer unstaged entry (shows unstaged diff for partial files), fallback to staged
|
|
824
|
+
const file = flatEntry.unstagedEntry ?? flatEntry.stagedEntry;
|
|
825
|
+
if (file) {
|
|
826
|
+
this.uiState.setDiffScrollOffset(0);
|
|
827
|
+
this.uiState.setSelectedHunkIndex(0);
|
|
828
|
+
this.gitManager?.selectFile(file);
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
else {
|
|
833
|
+
const files = this.gitManager?.state.status?.files ?? [];
|
|
834
|
+
const file = getFileAtIndex(files, index);
|
|
835
|
+
if (file) {
|
|
836
|
+
this.uiState.setDiffScrollOffset(0);
|
|
837
|
+
this.uiState.setSelectedHunkIndex(0);
|
|
838
|
+
this.gitManager?.selectFile(file);
|
|
839
|
+
}
|
|
700
840
|
}
|
|
701
841
|
}
|
|
702
842
|
/**
|
|
@@ -726,32 +866,64 @@ export class App {
|
|
|
726
866
|
async stageSelected() {
|
|
727
867
|
const files = this.gitManager?.state.status?.files ?? [];
|
|
728
868
|
const index = this.uiState.state.selectedIndex;
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
869
|
+
if (this.uiState.state.flatViewMode) {
|
|
870
|
+
const flatEntry = getFlatFileAtIndex(this.cachedFlatFiles, index);
|
|
871
|
+
if (!flatEntry)
|
|
872
|
+
return;
|
|
873
|
+
// Stage: operate on the unstaged entry if available
|
|
874
|
+
const file = flatEntry.unstagedEntry;
|
|
875
|
+
if (file) {
|
|
876
|
+
this.pendingFlatSelectionPath = flatEntry.path;
|
|
877
|
+
await this.gitManager?.stage(file);
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
else {
|
|
881
|
+
const selectedFile = getFileAtIndex(files, index);
|
|
882
|
+
if (selectedFile && !selectedFile.staged) {
|
|
883
|
+
this.pendingSelectionAnchor = getCategoryForIndex(files, index);
|
|
884
|
+
await this.gitManager?.stage(selectedFile);
|
|
885
|
+
}
|
|
733
886
|
}
|
|
734
887
|
}
|
|
735
888
|
async unstageSelected() {
|
|
736
889
|
const files = this.gitManager?.state.status?.files ?? [];
|
|
737
890
|
const index = this.uiState.state.selectedIndex;
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
891
|
+
if (this.uiState.state.flatViewMode) {
|
|
892
|
+
const flatEntry = getFlatFileAtIndex(this.cachedFlatFiles, index);
|
|
893
|
+
if (!flatEntry)
|
|
894
|
+
return;
|
|
895
|
+
const file = flatEntry.stagedEntry;
|
|
896
|
+
if (file) {
|
|
897
|
+
this.pendingFlatSelectionPath = flatEntry.path;
|
|
898
|
+
await this.gitManager?.unstage(file);
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
else {
|
|
902
|
+
const selectedFile = getFileAtIndex(files, index);
|
|
903
|
+
if (selectedFile?.staged) {
|
|
904
|
+
this.pendingSelectionAnchor = getCategoryForIndex(files, index);
|
|
905
|
+
await this.gitManager?.unstage(selectedFile);
|
|
906
|
+
}
|
|
742
907
|
}
|
|
743
908
|
}
|
|
744
909
|
async toggleSelected() {
|
|
745
|
-
const files = this.gitManager?.state.status?.files ?? [];
|
|
746
910
|
const index = this.uiState.state.selectedIndex;
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
911
|
+
if (this.uiState.state.flatViewMode) {
|
|
912
|
+
const flatEntry = getFlatFileAtIndex(this.cachedFlatFiles, index);
|
|
913
|
+
if (flatEntry)
|
|
914
|
+
await this.toggleFlatEntry(flatEntry);
|
|
915
|
+
}
|
|
916
|
+
else {
|
|
917
|
+
const files = this.gitManager?.state.status?.files ?? [];
|
|
918
|
+
const selectedFile = getFileAtIndex(files, index);
|
|
919
|
+
if (selectedFile) {
|
|
920
|
+
this.pendingSelectionAnchor = getCategoryForIndex(files, index);
|
|
921
|
+
if (selectedFile.staged) {
|
|
922
|
+
await this.gitManager?.unstage(selectedFile);
|
|
923
|
+
}
|
|
924
|
+
else {
|
|
925
|
+
await this.gitManager?.stage(selectedFile);
|
|
926
|
+
}
|
|
755
927
|
}
|
|
756
928
|
}
|
|
757
929
|
}
|
|
@@ -770,18 +942,125 @@ export class App {
|
|
|
770
942
|
});
|
|
771
943
|
this.activeModal.focus();
|
|
772
944
|
}
|
|
945
|
+
// Hunk navigation and staging
|
|
946
|
+
navigateNextHunk() {
|
|
947
|
+
const current = this.uiState.state.selectedHunkIndex;
|
|
948
|
+
if (this.bottomPaneHunkCount > 0 && current < this.bottomPaneHunkCount - 1) {
|
|
949
|
+
this.uiState.setSelectedHunkIndex(current + 1);
|
|
950
|
+
this.scrollHunkIntoView(current + 1);
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
navigatePrevHunk() {
|
|
954
|
+
const current = this.uiState.state.selectedHunkIndex;
|
|
955
|
+
if (current > 0) {
|
|
956
|
+
this.uiState.setSelectedHunkIndex(current - 1);
|
|
957
|
+
this.scrollHunkIntoView(current - 1);
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
scrollHunkIntoView(hunkIndex) {
|
|
961
|
+
const boundary = this.bottomPaneHunkBoundaries[hunkIndex];
|
|
962
|
+
if (!boundary)
|
|
963
|
+
return;
|
|
964
|
+
const scrollOffset = this.uiState.state.diffScrollOffset;
|
|
965
|
+
const visibleHeight = this.layout.dimensions.bottomPaneHeight;
|
|
966
|
+
// If hunk header is outside the visible area, scroll so it's at top
|
|
967
|
+
if (boundary.startRow < scrollOffset || boundary.startRow >= scrollOffset + visibleHeight) {
|
|
968
|
+
this.uiState.setDiffScrollOffset(boundary.startRow);
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
selectHunkAtRow(visualRow) {
|
|
972
|
+
if (this.uiState.state.bottomTab !== 'diff')
|
|
973
|
+
return;
|
|
974
|
+
if (this.bottomPaneHunkBoundaries.length === 0)
|
|
975
|
+
return;
|
|
976
|
+
// Focus the diff pane so the hunk gutter appears
|
|
977
|
+
this.uiState.setPane('diff');
|
|
978
|
+
const absoluteRow = this.uiState.state.diffScrollOffset + visualRow;
|
|
979
|
+
for (let i = 0; i < this.bottomPaneHunkBoundaries.length; i++) {
|
|
980
|
+
const b = this.bottomPaneHunkBoundaries[i];
|
|
981
|
+
if (absoluteRow >= b.startRow && absoluteRow < b.endRow) {
|
|
982
|
+
this.uiState.setSelectedHunkIndex(i);
|
|
983
|
+
return;
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
async toggleCurrentHunk() {
|
|
988
|
+
const selectedFile = this.gitManager?.state.selectedFile;
|
|
989
|
+
if (!selectedFile || selectedFile.status === 'untracked')
|
|
990
|
+
return;
|
|
991
|
+
if (this.uiState.state.flatViewMode) {
|
|
992
|
+
await this.toggleCurrentHunkFlat();
|
|
993
|
+
}
|
|
994
|
+
else {
|
|
995
|
+
await this.toggleCurrentHunkCategorized(selectedFile);
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
async toggleCurrentHunkFlat() {
|
|
999
|
+
const mapping = this.combinedHunkMapping[this.uiState.state.selectedHunkIndex];
|
|
1000
|
+
if (!mapping)
|
|
1001
|
+
return;
|
|
1002
|
+
const combined = this.gitManager?.state.combinedFileDiffs;
|
|
1003
|
+
if (!combined)
|
|
1004
|
+
return;
|
|
1005
|
+
const rawDiff = mapping.source === 'unstaged' ? combined.unstaged.raw : combined.staged.raw;
|
|
1006
|
+
const patch = extractHunkPatch(rawDiff, mapping.hunkIndex);
|
|
1007
|
+
if (!patch)
|
|
1008
|
+
return;
|
|
1009
|
+
// Preserve hunk index across refresh — file stays selected via path-only fallback
|
|
1010
|
+
this.pendingHunkIndex = this.uiState.state.selectedHunkIndex;
|
|
1011
|
+
if (mapping.source === 'staged') {
|
|
1012
|
+
await this.gitManager?.unstageHunk(patch);
|
|
1013
|
+
}
|
|
1014
|
+
else {
|
|
1015
|
+
await this.gitManager?.stageHunk(patch);
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
async toggleCurrentHunkCategorized(selectedFile) {
|
|
1019
|
+
const rawDiff = this.gitManager?.state.diff?.raw;
|
|
1020
|
+
if (!rawDiff)
|
|
1021
|
+
return;
|
|
1022
|
+
const patch = extractHunkPatch(rawDiff, this.uiState.state.selectedHunkIndex);
|
|
1023
|
+
if (!patch)
|
|
1024
|
+
return;
|
|
1025
|
+
const files = this.gitManager?.state.status?.files ?? [];
|
|
1026
|
+
this.pendingSelectionAnchor = getCategoryForIndex(files, this.uiState.state.selectedIndex);
|
|
1027
|
+
if (selectedFile.staged) {
|
|
1028
|
+
await this.gitManager?.unstageHunk(patch);
|
|
1029
|
+
}
|
|
1030
|
+
else {
|
|
1031
|
+
await this.gitManager?.stageHunk(patch);
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
773
1034
|
async openFileFinder() {
|
|
774
|
-
|
|
1035
|
+
let allPaths = this.explorerManager?.getCachedFilePaths() ?? [];
|
|
1036
|
+
if (allPaths.length === 0) {
|
|
1037
|
+
// First open or cache not yet loaded — wait for it
|
|
1038
|
+
await this.explorerManager?.loadFilePaths();
|
|
1039
|
+
allPaths = this.explorerManager?.getCachedFilePaths() ?? [];
|
|
1040
|
+
}
|
|
775
1041
|
if (allPaths.length === 0)
|
|
776
1042
|
return;
|
|
777
1043
|
this.activeModal = new FileFinder(this.screen, allPaths, async (selectedPath) => {
|
|
778
1044
|
this.activeModal = null;
|
|
1045
|
+
// Switch to explorer tab if not already there
|
|
1046
|
+
if (this.uiState.state.bottomTab !== 'explorer') {
|
|
1047
|
+
this.uiState.setTab('explorer');
|
|
1048
|
+
}
|
|
779
1049
|
// Navigate to the selected file in explorer
|
|
780
1050
|
const success = await this.explorerManager?.navigateToPath(selectedPath);
|
|
781
1051
|
if (success) {
|
|
782
|
-
//
|
|
783
|
-
this.
|
|
1052
|
+
// Sync selected index from explorer manager
|
|
1053
|
+
const selectedIndex = this.explorerManager?.state.selectedIndex ?? 0;
|
|
1054
|
+
this.uiState.setExplorerSelectedIndex(selectedIndex);
|
|
784
1055
|
this.uiState.setExplorerFileScrollOffset(0);
|
|
1056
|
+
// Scroll to make selected file visible
|
|
1057
|
+
const visibleHeight = this.layout.dimensions.topPaneHeight;
|
|
1058
|
+
if (selectedIndex >= visibleHeight) {
|
|
1059
|
+
this.uiState.setExplorerScrollOffset(selectedIndex - Math.floor(visibleHeight / 2));
|
|
1060
|
+
}
|
|
1061
|
+
else {
|
|
1062
|
+
this.uiState.setExplorerScrollOffset(0);
|
|
1063
|
+
}
|
|
785
1064
|
}
|
|
786
1065
|
this.render();
|
|
787
1066
|
}, () => {
|
|
@@ -790,6 +1069,66 @@ export class App {
|
|
|
790
1069
|
});
|
|
791
1070
|
this.activeModal.focus();
|
|
792
1071
|
}
|
|
1072
|
+
openStashListModal() {
|
|
1073
|
+
const entries = this.gitManager?.state.stashList ?? [];
|
|
1074
|
+
this.activeModal = new StashListModal(this.screen, entries, (index) => {
|
|
1075
|
+
this.activeModal = null;
|
|
1076
|
+
this.gitManager?.stashPop(index);
|
|
1077
|
+
}, () => {
|
|
1078
|
+
this.activeModal = null;
|
|
1079
|
+
});
|
|
1080
|
+
this.activeModal.focus();
|
|
1081
|
+
}
|
|
1082
|
+
openBranchPicker() {
|
|
1083
|
+
this.gitManager?.getLocalBranches().then((branches) => {
|
|
1084
|
+
this.activeModal = new BranchPicker(this.screen, branches, (name) => {
|
|
1085
|
+
this.activeModal = null;
|
|
1086
|
+
this.gitManager?.switchBranch(name);
|
|
1087
|
+
}, (name) => {
|
|
1088
|
+
this.activeModal = null;
|
|
1089
|
+
this.gitManager?.createBranch(name);
|
|
1090
|
+
}, () => {
|
|
1091
|
+
this.activeModal = null;
|
|
1092
|
+
});
|
|
1093
|
+
this.activeModal.focus();
|
|
1094
|
+
});
|
|
1095
|
+
}
|
|
1096
|
+
showSoftResetConfirm() {
|
|
1097
|
+
const headCommit = this.gitManager?.historyState.commits[0];
|
|
1098
|
+
if (!headCommit)
|
|
1099
|
+
return;
|
|
1100
|
+
this.activeModal = new SoftResetConfirm(this.screen, headCommit, () => {
|
|
1101
|
+
this.activeModal = null;
|
|
1102
|
+
this.gitManager?.softReset();
|
|
1103
|
+
}, () => {
|
|
1104
|
+
this.activeModal = null;
|
|
1105
|
+
});
|
|
1106
|
+
this.activeModal.focus();
|
|
1107
|
+
}
|
|
1108
|
+
cherryPickSelected() {
|
|
1109
|
+
const commit = this.gitManager?.historyState.selectedCommit;
|
|
1110
|
+
if (!commit)
|
|
1111
|
+
return;
|
|
1112
|
+
this.activeModal = new CommitActionConfirm(this.screen, 'Cherry-pick', commit, () => {
|
|
1113
|
+
this.activeModal = null;
|
|
1114
|
+
this.gitManager?.cherryPick(commit.hash);
|
|
1115
|
+
}, () => {
|
|
1116
|
+
this.activeModal = null;
|
|
1117
|
+
});
|
|
1118
|
+
this.activeModal.focus();
|
|
1119
|
+
}
|
|
1120
|
+
revertSelected() {
|
|
1121
|
+
const commit = this.gitManager?.historyState.selectedCommit;
|
|
1122
|
+
if (!commit)
|
|
1123
|
+
return;
|
|
1124
|
+
this.activeModal = new CommitActionConfirm(this.screen, 'Revert', commit, () => {
|
|
1125
|
+
this.activeModal = null;
|
|
1126
|
+
this.gitManager?.revertCommit(commit.hash);
|
|
1127
|
+
}, () => {
|
|
1128
|
+
this.activeModal = null;
|
|
1129
|
+
});
|
|
1130
|
+
this.activeModal.focus();
|
|
1131
|
+
}
|
|
793
1132
|
async commit(message) {
|
|
794
1133
|
await this.gitManager?.commit(message);
|
|
795
1134
|
}
|
|
@@ -800,6 +1139,7 @@ export class App {
|
|
|
800
1139
|
const willEnable = !this.uiState.state.mouseEnabled;
|
|
801
1140
|
this.uiState.toggleMouse();
|
|
802
1141
|
// Access program for terminal mouse control (not on screen's TS types)
|
|
1142
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
803
1143
|
const program = this.screen.program;
|
|
804
1144
|
if (willEnable) {
|
|
805
1145
|
program.enableMouse();
|
|
@@ -842,19 +1182,31 @@ export class App {
|
|
|
842
1182
|
this.updateHeader();
|
|
843
1183
|
this.updateTopPane();
|
|
844
1184
|
this.updateBottomPane();
|
|
1185
|
+
// Restore hunk index after diff refresh (e.g. after hunk toggle in flat mode)
|
|
1186
|
+
if (this.pendingHunkIndex !== null && this.bottomPaneHunkCount > 0) {
|
|
1187
|
+
const restored = Math.min(this.pendingHunkIndex, this.bottomPaneHunkCount - 1);
|
|
1188
|
+
this.pendingHunkIndex = null;
|
|
1189
|
+
this.uiState.setSelectedHunkIndex(restored);
|
|
1190
|
+
this.updateBottomPane(); // Re-render with correct hunk selection
|
|
1191
|
+
}
|
|
845
1192
|
this.updateFooter();
|
|
846
1193
|
this.screen.render();
|
|
847
1194
|
}
|
|
848
1195
|
updateHeader() {
|
|
849
1196
|
const gitState = this.gitManager?.state;
|
|
850
1197
|
const width = this.screen.width || 80;
|
|
851
|
-
const content = formatHeader(this.repoPath, gitState?.status?.branch ?? null, gitState?.isLoading ?? false, gitState?.error ?? null, width);
|
|
1198
|
+
const content = formatHeader(this.repoPath, gitState?.status?.branch ?? null, gitState?.isLoading ?? false, gitState?.error ?? null, width, this.gitManager?.remoteState ?? null);
|
|
852
1199
|
this.layout.headerBox.setContent(content);
|
|
853
1200
|
}
|
|
854
1201
|
updateTopPane() {
|
|
855
1202
|
const state = this.uiState.state;
|
|
856
1203
|
const width = this.screen.width || 80;
|
|
857
|
-
const
|
|
1204
|
+
const files = this.gitManager?.state.status?.files ?? [];
|
|
1205
|
+
// Build and cache flat file list when in flat mode
|
|
1206
|
+
if (state.flatViewMode) {
|
|
1207
|
+
this.cachedFlatFiles = buildFlatFileList(files, this.gitManager?.state.hunkCounts ?? null);
|
|
1208
|
+
}
|
|
1209
|
+
const content = renderTopPane(state, files, this.gitManager?.historyState?.commits ?? [], this.gitManager?.compareState?.compareDiff ?? null, this.compareSelection, this.explorerManager?.state, width, this.layout.dimensions.topPaneHeight, this.gitManager?.state.hunkCounts, state.flatViewMode ? this.cachedFlatFiles : undefined);
|
|
858
1210
|
this.layout.topPane.setContent(content);
|
|
859
1211
|
}
|
|
860
1212
|
updateBottomPane() {
|
|
@@ -864,8 +1216,17 @@ export class App {
|
|
|
864
1216
|
const stagedCount = files.filter((f) => f.staged).length;
|
|
865
1217
|
// Update staged count for commit validation
|
|
866
1218
|
this.commitFlowState.setStagedCount(stagedCount);
|
|
867
|
-
|
|
1219
|
+
// Pass selectedHunkIndex and staged status only when diff pane is focused on diff tab
|
|
1220
|
+
const diffPaneFocused = state.bottomTab === 'diff' && state.currentPane === 'diff';
|
|
1221
|
+
const hunkIndex = diffPaneFocused ? state.selectedHunkIndex : undefined;
|
|
1222
|
+
const isFileStaged = diffPaneFocused ? this.gitManager?.state.selectedFile?.staged : undefined;
|
|
1223
|
+
const { content, totalRows, hunkCount, hunkBoundaries, hunkMapping } = renderBottomPane(state, this.gitManager?.state.diff ?? null, this.gitManager?.historyState, this.gitManager?.compareSelectionState, this.explorerManager?.state?.selectedFile ?? null, this.commitFlowState.state, stagedCount, this.currentTheme, width, this.layout.dimensions.bottomPaneHeight, hunkIndex, isFileStaged, state.flatViewMode ? this.gitManager?.state.combinedFileDiffs : undefined, this.gitManager?.state.status?.branch ?? null, this.gitManager?.remoteState ?? null, this.gitManager?.state.stashList, this.gitManager?.historyState.commits[0] ?? null);
|
|
868
1224
|
this.bottomPaneTotalRows = totalRows;
|
|
1225
|
+
this.bottomPaneHunkCount = hunkCount;
|
|
1226
|
+
this.bottomPaneHunkBoundaries = hunkBoundaries;
|
|
1227
|
+
this.combinedHunkMapping = hunkMapping ?? [];
|
|
1228
|
+
// Silently clamp hunk index to actual count (handles async refresh after hunk staging)
|
|
1229
|
+
this.uiState.clampSelectedHunkIndex(hunkCount);
|
|
869
1230
|
this.layout.bottomPane.setContent(content);
|
|
870
1231
|
// Manage commit textarea visibility
|
|
871
1232
|
if (this.commitTextarea) {
|
|
@@ -880,7 +1241,7 @@ export class App {
|
|
|
880
1241
|
updateFooter() {
|
|
881
1242
|
const state = this.uiState.state;
|
|
882
1243
|
const width = this.screen.width || 80;
|
|
883
|
-
const content = formatFooter(state.bottomTab, state.mouseEnabled, state.autoTabEnabled, state.wrapMode, this.followMode?.isEnabled ?? false, this.explorerManager?.showOnlyChanges ?? false, width);
|
|
1244
|
+
const content = formatFooter(state.bottomTab, state.mouseEnabled, state.autoTabEnabled, state.wrapMode, this.followMode?.isEnabled ?? false, this.explorerManager?.showOnlyChanges ?? false, width, state.currentPane);
|
|
884
1245
|
this.layout.footerBox.setContent(content);
|
|
885
1246
|
}
|
|
886
1247
|
/**
|
|
@@ -900,6 +1261,9 @@ export class App {
|
|
|
900
1261
|
if (this.commandServer) {
|
|
901
1262
|
this.commandServer.stop();
|
|
902
1263
|
}
|
|
1264
|
+
if (this.remoteClearTimer) {
|
|
1265
|
+
clearTimeout(this.remoteClearTimer);
|
|
1266
|
+
}
|
|
903
1267
|
// Destroy screen (this will clean up terminal)
|
|
904
1268
|
this.screen.destroy();
|
|
905
1269
|
}
|