diffstalker 0.2.2 → 0.2.4
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 +2 -2
- package/dist/App.js +299 -664
- package/dist/KeyBindings.js +125 -39
- package/dist/ModalController.js +166 -0
- package/dist/MouseHandlers.js +43 -25
- package/dist/NavigationController.js +290 -0
- package/dist/StagingOperations.js +199 -0
- package/dist/config.js +39 -0
- package/dist/core/CompareManager.js +134 -0
- package/dist/core/ExplorerStateManager.js +27 -40
- package/dist/core/GitStateManager.js +28 -630
- package/dist/core/HistoryManager.js +72 -0
- package/dist/core/RemoteOperationManager.js +109 -0
- package/dist/core/WorkingTreeManager.js +412 -0
- package/dist/git/status.js +95 -0
- package/dist/index.js +59 -54
- package/dist/state/FocusRing.js +40 -0
- package/dist/state/UIState.js +82 -48
- package/dist/types/remote.js +5 -0
- package/dist/ui/PaneRenderers.js +11 -4
- package/dist/ui/modals/BaseBranchPicker.js +4 -7
- package/dist/ui/modals/CommitActionConfirm.js +66 -0
- package/dist/ui/modals/DiscardConfirm.js +4 -7
- package/dist/ui/modals/FileFinder.js +33 -27
- package/dist/ui/modals/HotkeysModal.js +32 -13
- package/dist/ui/modals/Modal.js +1 -0
- package/dist/ui/modals/RepoPicker.js +109 -0
- package/dist/ui/modals/ThemePicker.js +4 -7
- package/dist/ui/widgets/CommitPanel.js +52 -14
- package/dist/ui/widgets/CompareListView.js +1 -11
- package/dist/ui/widgets/DiffView.js +2 -27
- package/dist/ui/widgets/ExplorerContent.js +1 -4
- package/dist/ui/widgets/ExplorerView.js +1 -11
- package/dist/ui/widgets/FileList.js +2 -8
- package/dist/ui/widgets/Footer.js +1 -0
- package/dist/ui/widgets/Header.js +37 -3
- package/dist/utils/ansi.js +38 -0
- package/dist/utils/ansiTruncate.js +1 -5
- package/dist/utils/displayRows.js +72 -59
- package/dist/utils/fileCategories.js +7 -0
- package/dist/utils/fileResolution.js +23 -0
- package/dist/utils/languageDetection.js +3 -2
- package/dist/utils/logger.js +32 -0
- package/metrics/v0.2.3.json +243 -0
- package/metrics/v0.2.4.json +236 -0
- package/package.json +5 -2
- package/dist/utils/layoutCalculations.js +0 -100
package/dist/App.js
CHANGED
|
@@ -3,25 +3,21 @@ import { LayoutManager } from './ui/Layout.js';
|
|
|
3
3
|
import { setupKeyBindings } from './KeyBindings.js';
|
|
4
4
|
import { renderTopPane, renderBottomPane } from './ui/PaneRenderers.js';
|
|
5
5
|
import { setupMouseHandlers } from './MouseHandlers.js';
|
|
6
|
+
import { NavigationController } from './NavigationController.js';
|
|
7
|
+
import { StagingOperations } from './StagingOperations.js';
|
|
8
|
+
import { ModalController } from './ModalController.js';
|
|
6
9
|
import { FollowMode } from './FollowMode.js';
|
|
7
10
|
import { formatHeader } from './ui/widgets/Header.js';
|
|
8
11
|
import { formatFooter } from './ui/widgets/Footer.js';
|
|
9
|
-
import { getFileAtIndex, getRowFromFileIndex } from './ui/widgets/FileList.js';
|
|
10
|
-
import { getCommitAtIndex } from './ui/widgets/HistoryView.js';
|
|
11
|
-
import { getNextCompareSelection, getRowFromCompareSelection, } from './ui/widgets/CompareListView.js';
|
|
12
12
|
import { ExplorerStateManager, } from './core/ExplorerStateManager.js';
|
|
13
|
-
import { ThemePicker } from './ui/modals/ThemePicker.js';
|
|
14
|
-
import { HotkeysModal } from './ui/modals/HotkeysModal.js';
|
|
15
|
-
import { BaseBranchPicker } from './ui/modals/BaseBranchPicker.js';
|
|
16
|
-
import { DiscardConfirm } from './ui/modals/DiscardConfirm.js';
|
|
17
|
-
import { FileFinder } from './ui/modals/FileFinder.js';
|
|
18
13
|
import { CommitFlowState } from './state/CommitFlowState.js';
|
|
19
14
|
import { UIState } from './state/UIState.js';
|
|
20
15
|
import { getManagerForRepo, removeManagerForRepo, } from './core/GitStateManager.js';
|
|
21
|
-
import { saveConfig } from './config.js';
|
|
22
|
-
import {
|
|
16
|
+
import { saveConfig, addRecentRepo } from './config.js';
|
|
17
|
+
import { getIndexForCategoryPosition } from './utils/fileCategories.js';
|
|
23
18
|
import { buildFlatFileList, getFlatFileAtIndex, getFlatFileIndexByPath, } from './utils/flatFileList.js';
|
|
24
|
-
import {
|
|
19
|
+
import { getFileAtIndex } from './ui/widgets/FileList.js';
|
|
20
|
+
import { resolveFileAtIndex as resolveFile, getFileListMaxIndex as getMaxIndex, } from './utils/fileResolution.js';
|
|
25
21
|
/**
|
|
26
22
|
* Main application controller.
|
|
27
23
|
* Coordinates between GitStateManager, UIState, and blessed widgets.
|
|
@@ -35,33 +31,39 @@ export class App {
|
|
|
35
31
|
explorerManager = null;
|
|
36
32
|
config;
|
|
37
33
|
commandServer;
|
|
34
|
+
navigation;
|
|
35
|
+
staging;
|
|
36
|
+
modals;
|
|
38
37
|
// Current state
|
|
39
38
|
repoPath;
|
|
40
39
|
currentTheme;
|
|
40
|
+
recentRepos;
|
|
41
41
|
// Commit flow state
|
|
42
42
|
commitFlowState;
|
|
43
43
|
commitTextarea = null;
|
|
44
|
-
//
|
|
45
|
-
|
|
44
|
+
// Auto-clear timer for remote operation status
|
|
45
|
+
remoteClearTimer = null;
|
|
46
46
|
// Cached total rows and hunk info for scroll bounds (single source of truth from render)
|
|
47
47
|
bottomPaneTotalRows = 0;
|
|
48
48
|
bottomPaneHunkCount = 0;
|
|
49
49
|
bottomPaneHunkBoundaries = [];
|
|
50
|
-
//
|
|
51
|
-
|
|
50
|
+
// Auto-tab transition tracking
|
|
51
|
+
prevFileCount = 0;
|
|
52
52
|
// Flat view mode state
|
|
53
53
|
cachedFlatFiles = [];
|
|
54
|
-
pendingFlatSelectionPath = null;
|
|
55
|
-
pendingHunkIndex = null;
|
|
56
54
|
combinedHunkMapping = [];
|
|
57
55
|
constructor(options) {
|
|
58
56
|
this.config = options.config;
|
|
59
57
|
this.commandServer = options.commandServer ?? null;
|
|
60
58
|
this.repoPath = options.initialPath ?? process.cwd();
|
|
61
59
|
this.currentTheme = options.config.theme;
|
|
60
|
+
this.recentRepos = options.config.recentRepos ?? [];
|
|
62
61
|
// Initialize UI state with config values
|
|
63
62
|
this.uiState = new UIState({
|
|
64
63
|
splitRatio: options.config.splitRatio ?? 0.4,
|
|
64
|
+
autoTabEnabled: options.config.autoTabEnabled ?? false,
|
|
65
|
+
wrapMode: options.config.wrapMode ?? false,
|
|
66
|
+
mouseEnabled: options.config.mouseEnabled ?? true,
|
|
65
67
|
});
|
|
66
68
|
// Create blessed screen
|
|
67
69
|
this.screen = blessed.screen({
|
|
@@ -89,9 +91,9 @@ export class App {
|
|
|
89
91
|
});
|
|
90
92
|
// Initialize commit flow state
|
|
91
93
|
this.commitFlowState = new CommitFlowState({
|
|
92
|
-
getHeadMessage: () => this.gitManager?.getHeadCommitMessage() ?? Promise.resolve(''),
|
|
94
|
+
getHeadMessage: () => this.gitManager?.history.getHeadCommitMessage() ?? Promise.resolve(''),
|
|
93
95
|
onCommit: async (message, amend) => {
|
|
94
|
-
await this.gitManager?.commit(message, amend);
|
|
96
|
+
await this.gitManager?.workingTree.commit(message, amend);
|
|
95
97
|
},
|
|
96
98
|
onSuccess: () => {
|
|
97
99
|
this.uiState.setTab('diff');
|
|
@@ -124,6 +126,50 @@ export class App {
|
|
|
124
126
|
this.commitFlowState.setMessage(value);
|
|
125
127
|
});
|
|
126
128
|
});
|
|
129
|
+
// Setup navigation controller
|
|
130
|
+
this.navigation = new NavigationController({
|
|
131
|
+
uiState: this.uiState,
|
|
132
|
+
getGitManager: () => this.gitManager,
|
|
133
|
+
getExplorerManager: () => this.explorerManager,
|
|
134
|
+
getTopPaneHeight: () => this.layout.dimensions.topPaneHeight,
|
|
135
|
+
getBottomPaneHeight: () => this.layout.dimensions.bottomPaneHeight,
|
|
136
|
+
getCachedFlatFiles: () => this.cachedFlatFiles,
|
|
137
|
+
getHunkCount: () => this.bottomPaneHunkCount,
|
|
138
|
+
getHunkBoundaries: () => this.bottomPaneHunkBoundaries,
|
|
139
|
+
getRepoPath: () => this.repoPath,
|
|
140
|
+
onError: (message) => this.showError(message),
|
|
141
|
+
resolveFileAtIndex: (index) => resolveFile(index, this.uiState.state.flatViewMode, this.cachedFlatFiles, this.gitManager?.workingTree.state.status?.files ?? []),
|
|
142
|
+
getFileListMaxIndex: () => getMaxIndex(this.uiState.state.flatViewMode, this.cachedFlatFiles, this.gitManager?.workingTree.state.status?.files ?? []),
|
|
143
|
+
});
|
|
144
|
+
// Setup modal controller
|
|
145
|
+
this.modals = new ModalController({
|
|
146
|
+
screen: this.screen,
|
|
147
|
+
uiState: this.uiState,
|
|
148
|
+
getGitManager: () => this.gitManager,
|
|
149
|
+
getExplorerManager: () => this.explorerManager,
|
|
150
|
+
getTopPaneHeight: () => this.layout.dimensions.topPaneHeight,
|
|
151
|
+
getCurrentTheme: () => this.currentTheme,
|
|
152
|
+
setCurrentTheme: (theme) => {
|
|
153
|
+
this.currentTheme = theme;
|
|
154
|
+
},
|
|
155
|
+
getRepoPath: () => this.repoPath,
|
|
156
|
+
getRecentRepos: () => this.recentRepos,
|
|
157
|
+
onRepoSwitch: (repoPath) => this.switchToRepo(repoPath),
|
|
158
|
+
render: () => this.render(),
|
|
159
|
+
});
|
|
160
|
+
// Setup staging operations
|
|
161
|
+
this.staging = new StagingOperations({
|
|
162
|
+
uiState: this.uiState,
|
|
163
|
+
getGitManager: () => this.gitManager,
|
|
164
|
+
getCachedFlatFiles: () => this.cachedFlatFiles,
|
|
165
|
+
getCombinedHunkMapping: () => this.combinedHunkMapping,
|
|
166
|
+
resolveFileAtIndex: (index) => resolveFile(index, this.uiState.state.flatViewMode, this.cachedFlatFiles, this.gitManager?.workingTree.state.status?.files ?? []),
|
|
167
|
+
});
|
|
168
|
+
// If mouse was persisted as disabled, disable it now
|
|
169
|
+
if (!this.uiState.state.mouseEnabled) {
|
|
170
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
171
|
+
this.screen.program.disableMouse();
|
|
172
|
+
}
|
|
127
173
|
// Setup keyboard handlers
|
|
128
174
|
this.setupKeyboardHandlers();
|
|
129
175
|
// Setup mouse handlers
|
|
@@ -147,102 +193,95 @@ export class App {
|
|
|
147
193
|
// Initial render
|
|
148
194
|
this.render();
|
|
149
195
|
}
|
|
196
|
+
/**
|
|
197
|
+
* Display an error in the UI by emitting a state change with the error set.
|
|
198
|
+
*/
|
|
199
|
+
showError(message) {
|
|
200
|
+
if (!this.gitManager)
|
|
201
|
+
return;
|
|
202
|
+
const wt = this.gitManager.workingTree;
|
|
203
|
+
wt.emit('state-change', { ...wt.state, error: message });
|
|
204
|
+
}
|
|
150
205
|
setupKeyboardHandlers() {
|
|
151
206
|
setupKeyBindings(this.screen, {
|
|
152
207
|
exit: () => this.exit(),
|
|
153
|
-
navigateDown: () => this.navigateDown(),
|
|
154
|
-
navigateUp: () => this.navigateUp(),
|
|
155
|
-
stageSelected: () => this.stageSelected(),
|
|
156
|
-
unstageSelected: () => this.unstageSelected(),
|
|
157
|
-
stageAll: () => this.stageAll(),
|
|
158
|
-
unstageAll: () => this.unstageAll(),
|
|
159
|
-
toggleSelected: () => this.toggleSelected(),
|
|
160
|
-
enterExplorerDirectory: () => this.enterExplorerDirectory(),
|
|
161
|
-
goExplorerUp: () => this.goExplorerUp(),
|
|
162
|
-
openFileFinder: () => this.openFileFinder(),
|
|
208
|
+
navigateDown: () => this.navigation.navigateDown(),
|
|
209
|
+
navigateUp: () => this.navigation.navigateUp(),
|
|
210
|
+
stageSelected: () => this.staging.stageSelected(),
|
|
211
|
+
unstageSelected: () => this.staging.unstageSelected(),
|
|
212
|
+
stageAll: () => this.staging.stageAll(),
|
|
213
|
+
unstageAll: () => this.staging.unstageAll(),
|
|
214
|
+
toggleSelected: () => this.staging.toggleSelected(),
|
|
215
|
+
enterExplorerDirectory: () => this.navigation.enterExplorerDirectory(),
|
|
216
|
+
goExplorerUp: () => this.navigation.goExplorerUp(),
|
|
217
|
+
openFileFinder: () => this.modals.openFileFinder(),
|
|
163
218
|
focusCommitInput: () => this.focusCommitInput(),
|
|
164
219
|
unfocusCommitInput: () => this.unfocusCommitInput(),
|
|
165
|
-
|
|
220
|
+
openRepoPicker: () => this.modals.openRepoPicker(),
|
|
221
|
+
openThemePicker: () => this.modals.openThemePicker(),
|
|
222
|
+
openHotkeysModal: () => this.modals.openHotkeysModal(),
|
|
223
|
+
openBaseBranchPicker: () => this.modals.openBaseBranchPicker(),
|
|
224
|
+
closeActiveModal: () => this.modals.closeActiveModal(),
|
|
166
225
|
toggleMouseMode: () => this.toggleMouseMode(),
|
|
167
226
|
toggleFollow: () => this.toggleFollow(),
|
|
168
|
-
|
|
227
|
+
openDiscardConfirm: (file) => this.modals.openDiscardConfirm(file),
|
|
169
228
|
render: () => this.render(),
|
|
170
|
-
toggleCurrentHunk: () => this.toggleCurrentHunk(),
|
|
171
|
-
navigateNextHunk: () => this.navigateNextHunk(),
|
|
172
|
-
navigatePrevHunk: () => this.navigatePrevHunk(),
|
|
229
|
+
toggleCurrentHunk: () => this.staging.toggleCurrentHunk(),
|
|
230
|
+
navigateNextHunk: () => this.navigation.navigateNextHunk(),
|
|
231
|
+
navigatePrevHunk: () => this.navigation.navigatePrevHunk(),
|
|
232
|
+
openCherryPickConfirm: () => this.modals.openCherryPickConfirm(),
|
|
233
|
+
openRevertConfirm: () => this.modals.openRevertConfirm(),
|
|
173
234
|
}, {
|
|
174
|
-
hasActiveModal: () => this.
|
|
235
|
+
hasActiveModal: () => this.modals.hasActiveModal(),
|
|
236
|
+
getActiveModalType: () => this.modals.getActiveModalType(),
|
|
175
237
|
getBottomTab: () => this.uiState.state.bottomTab,
|
|
176
238
|
getCurrentPane: () => this.uiState.state.currentPane,
|
|
239
|
+
getFocusedZone: () => this.uiState.state.focusedZone,
|
|
177
240
|
isCommitInputFocused: () => this.commitFlowState.state.inputFocused,
|
|
178
|
-
getStatusFiles: () => this.gitManager?.state.status?.files ?? [],
|
|
241
|
+
getStatusFiles: () => this.gitManager?.workingTree.state.status?.files ?? [],
|
|
179
242
|
getSelectedIndex: () => this.uiState.state.selectedIndex,
|
|
180
243
|
uiState: this.uiState,
|
|
181
244
|
getExplorerManager: () => this.explorerManager,
|
|
182
245
|
commitFlowState: this.commitFlowState,
|
|
183
246
|
getGitManager: () => this.gitManager,
|
|
184
247
|
layout: this.layout,
|
|
185
|
-
|
|
248
|
+
resolveFileAtIndex: (index) => resolveFile(index, this.uiState.state.flatViewMode, this.cachedFlatFiles, this.gitManager?.workingTree.state.status?.files ?? []),
|
|
186
249
|
});
|
|
187
250
|
}
|
|
188
251
|
setupMouseEventHandlers() {
|
|
189
252
|
setupMouseHandlers(this.layout, {
|
|
190
|
-
selectHistoryCommitByIndex: (index) => this.selectHistoryCommitByIndex(index),
|
|
191
|
-
selectCompareItem: (selection) => this.selectCompareItem(selection),
|
|
192
|
-
selectFileByIndex: (index) => this.selectFileByIndex(index),
|
|
193
|
-
toggleFileByIndex: (index) => this.toggleFileByIndex(index),
|
|
194
|
-
enterExplorerDirectory: () => this.enterExplorerDirectory(),
|
|
253
|
+
selectHistoryCommitByIndex: (index) => this.navigation.selectHistoryCommitByIndex(index),
|
|
254
|
+
selectCompareItem: (selection) => this.navigation.selectCompareItem(selection),
|
|
255
|
+
selectFileByIndex: (index) => this.navigation.selectFileByIndex(index),
|
|
256
|
+
toggleFileByIndex: (index) => this.staging.toggleFileByIndex(index),
|
|
257
|
+
enterExplorerDirectory: () => this.navigation.enterExplorerDirectory(),
|
|
195
258
|
toggleMouseMode: () => this.toggleMouseMode(),
|
|
196
259
|
toggleFollow: () => this.toggleFollow(),
|
|
197
|
-
selectHunkAtRow: (row) => this.selectHunkAtRow(row),
|
|
260
|
+
selectHunkAtRow: (row) => this.navigation.selectHunkAtRow(row),
|
|
261
|
+
focusCommitInput: () => this.focusCommitInput(),
|
|
262
|
+
openHotkeysModal: () => this.modals.openHotkeysModal(),
|
|
198
263
|
render: () => this.render(),
|
|
199
264
|
}, {
|
|
200
265
|
uiState: this.uiState,
|
|
201
266
|
getExplorerManager: () => this.explorerManager,
|
|
202
|
-
getStatusFiles: () => this.gitManager?.state.status?.files ?? [],
|
|
203
|
-
getHistoryCommitCount: () => this.gitManager?.historyState.commits.length ?? 0,
|
|
204
|
-
getCompareCommits: () => this.gitManager?.compareState?.compareDiff?.commits ?? [],
|
|
205
|
-
getCompareFiles: () => this.gitManager?.compareState?.compareDiff?.files ?? [],
|
|
267
|
+
getStatusFiles: () => this.gitManager?.workingTree.state.status?.files ?? [],
|
|
268
|
+
getHistoryCommitCount: () => this.gitManager?.history.historyState.commits.length ?? 0,
|
|
269
|
+
getCompareCommits: () => this.gitManager?.compare.compareState?.compareDiff?.commits ?? [],
|
|
270
|
+
getCompareFiles: () => this.gitManager?.compare.compareState?.compareDiff?.files ?? [],
|
|
206
271
|
getBottomPaneTotalRows: () => this.bottomPaneTotalRows,
|
|
207
272
|
getScreenWidth: () => this.screen.width || 80,
|
|
208
273
|
getCachedFlatFiles: () => this.cachedFlatFiles,
|
|
209
274
|
});
|
|
210
275
|
}
|
|
211
|
-
/**
|
|
212
|
-
* Toggle staging for a flat file entry (stage if unstaged/partial, unstage if fully staged).
|
|
213
|
-
*/
|
|
214
|
-
async toggleFlatEntry(entry) {
|
|
215
|
-
this.pendingFlatSelectionPath = entry.path;
|
|
216
|
-
if (entry.stagingState === 'staged') {
|
|
217
|
-
if (entry.stagedEntry)
|
|
218
|
-
await this.gitManager?.unstage(entry.stagedEntry);
|
|
219
|
-
}
|
|
220
|
-
else {
|
|
221
|
-
if (entry.unstagedEntry)
|
|
222
|
-
await this.gitManager?.stage(entry.unstagedEntry);
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
async toggleFileByIndex(index) {
|
|
226
|
-
if (this.uiState.state.flatViewMode) {
|
|
227
|
-
const flatEntry = getFlatFileAtIndex(this.cachedFlatFiles, index);
|
|
228
|
-
if (flatEntry)
|
|
229
|
-
await this.toggleFlatEntry(flatEntry);
|
|
230
|
-
}
|
|
231
|
-
else {
|
|
232
|
-
const files = this.gitManager?.state.status?.files ?? [];
|
|
233
|
-
const file = getFileAtIndex(files, index);
|
|
234
|
-
if (file) {
|
|
235
|
-
this.pendingSelectionAnchor = getCategoryForIndex(files, this.uiState.state.selectedIndex);
|
|
236
|
-
if (file.staged) {
|
|
237
|
-
await this.gitManager?.unstage(file);
|
|
238
|
-
}
|
|
239
|
-
else {
|
|
240
|
-
await this.gitManager?.stage(file);
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
276
|
setupStateListeners() {
|
|
277
|
+
// Apply auto-tab logic when toggled on
|
|
278
|
+
let prevAutoTab = this.uiState.state.autoTabEnabled;
|
|
279
|
+
this.uiState.on('change', (state) => {
|
|
280
|
+
if (state.autoTabEnabled && !prevAutoTab) {
|
|
281
|
+
this.applyAutoTab();
|
|
282
|
+
}
|
|
283
|
+
prevAutoTab = state.autoTabEnabled;
|
|
284
|
+
});
|
|
246
285
|
// Update footer when UI state changes
|
|
247
286
|
this.uiState.on('change', () => {
|
|
248
287
|
this.render();
|
|
@@ -254,10 +293,10 @@ export class App {
|
|
|
254
293
|
this.uiState.setSelectedHunkIndex(0);
|
|
255
294
|
}
|
|
256
295
|
if (tab === 'history') {
|
|
257
|
-
this.
|
|
296
|
+
this.loadHistory();
|
|
258
297
|
}
|
|
259
298
|
else if (tab === 'compare') {
|
|
260
|
-
this.gitManager?.refreshCompareDiff(this.uiState.state.includeUncommitted);
|
|
299
|
+
this.gitManager?.compare.refreshCompareDiff(this.uiState.state.includeUncommitted);
|
|
261
300
|
}
|
|
262
301
|
else if (tab === 'explorer') {
|
|
263
302
|
// Explorer is already loaded on init, but refresh if needed
|
|
@@ -266,60 +305,23 @@ export class App {
|
|
|
266
305
|
}
|
|
267
306
|
}
|
|
268
307
|
});
|
|
269
|
-
//
|
|
270
|
-
this.uiState.on('modal-change', (modal) => {
|
|
271
|
-
// Close any existing modal
|
|
272
|
-
if (this.activeModal) {
|
|
273
|
-
this.activeModal = null;
|
|
274
|
-
}
|
|
275
|
-
// Open new modal if requested
|
|
276
|
-
if (modal === 'theme') {
|
|
277
|
-
this.activeModal = new ThemePicker(this.screen, this.currentTheme, (theme) => {
|
|
278
|
-
this.currentTheme = theme;
|
|
279
|
-
saveConfig({ theme });
|
|
280
|
-
this.activeModal = null;
|
|
281
|
-
this.uiState.closeModal();
|
|
282
|
-
this.render();
|
|
283
|
-
}, () => {
|
|
284
|
-
this.activeModal = null;
|
|
285
|
-
this.uiState.closeModal();
|
|
286
|
-
});
|
|
287
|
-
this.activeModal.focus();
|
|
288
|
-
}
|
|
289
|
-
else if (modal === 'hotkeys') {
|
|
290
|
-
this.activeModal = new HotkeysModal(this.screen, () => {
|
|
291
|
-
this.activeModal = null;
|
|
292
|
-
this.uiState.closeModal();
|
|
293
|
-
});
|
|
294
|
-
this.activeModal.focus();
|
|
295
|
-
}
|
|
296
|
-
else if (modal === 'baseBranch') {
|
|
297
|
-
// Load candidate branches and show picker
|
|
298
|
-
this.gitManager?.getCandidateBaseBranches().then((branches) => {
|
|
299
|
-
const currentBranch = this.gitManager?.compareState.compareBaseBranch ?? null;
|
|
300
|
-
this.activeModal = new BaseBranchPicker(this.screen, branches, currentBranch, (branch) => {
|
|
301
|
-
this.activeModal = null;
|
|
302
|
-
this.uiState.closeModal();
|
|
303
|
-
// Set base branch and refresh compare view
|
|
304
|
-
const includeUncommitted = this.uiState.state.includeUncommitted;
|
|
305
|
-
this.gitManager?.setCompareBaseBranch(branch, includeUncommitted);
|
|
306
|
-
}, () => {
|
|
307
|
-
this.activeModal = null;
|
|
308
|
-
this.uiState.closeModal();
|
|
309
|
-
});
|
|
310
|
-
this.activeModal.focus();
|
|
311
|
-
});
|
|
312
|
-
}
|
|
313
|
-
});
|
|
314
|
-
// Save split ratio to config when it changes
|
|
308
|
+
// Persist UI state to config when toggles or split ratio change
|
|
315
309
|
let saveTimer = null;
|
|
316
310
|
this.uiState.on('change', (state) => {
|
|
317
311
|
if (saveTimer)
|
|
318
312
|
clearTimeout(saveTimer);
|
|
319
313
|
saveTimer = setTimeout(() => {
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
314
|
+
const updates = {};
|
|
315
|
+
if (state.splitRatio !== this.config.splitRatio)
|
|
316
|
+
updates.splitRatio = state.splitRatio;
|
|
317
|
+
if (state.autoTabEnabled !== this.config.autoTabEnabled)
|
|
318
|
+
updates.autoTabEnabled = state.autoTabEnabled;
|
|
319
|
+
if (state.wrapMode !== this.config.wrapMode)
|
|
320
|
+
updates.wrapMode = state.wrapMode;
|
|
321
|
+
if (state.mouseEnabled !== this.config.mouseEnabled)
|
|
322
|
+
updates.mouseEnabled = state.mouseEnabled;
|
|
323
|
+
if (Object.keys(updates).length > 0)
|
|
324
|
+
saveConfig(updates);
|
|
323
325
|
}, 500);
|
|
324
326
|
});
|
|
325
327
|
}
|
|
@@ -332,92 +334,160 @@ export class App {
|
|
|
332
334
|
this.render();
|
|
333
335
|
}
|
|
334
336
|
handleFollowFileNavigate(rawContent) {
|
|
335
|
-
this.navigateToFile(rawContent);
|
|
337
|
+
this.navigation.navigateToFile(rawContent);
|
|
338
|
+
this.render();
|
|
339
|
+
}
|
|
340
|
+
recordCurrentRepo() {
|
|
341
|
+
const max = this.config.maxRecentRepos ?? 10;
|
|
342
|
+
const normalized = this.repoPath.replace(/\/$/, '');
|
|
343
|
+
this.recentRepos = [
|
|
344
|
+
normalized,
|
|
345
|
+
...this.recentRepos.map((r) => r.replace(/\/$/, '')).filter((r) => r !== normalized),
|
|
346
|
+
].slice(0, max);
|
|
347
|
+
addRecentRepo(this.repoPath, max);
|
|
348
|
+
}
|
|
349
|
+
switchToRepo(newPath) {
|
|
350
|
+
if (newPath === this.repoPath)
|
|
351
|
+
return;
|
|
352
|
+
if (this.followMode?.isEnabled)
|
|
353
|
+
this.followMode.stop();
|
|
354
|
+
const oldRepoPath = this.repoPath;
|
|
355
|
+
this.repoPath = newPath;
|
|
356
|
+
this.initGitManager(oldRepoPath);
|
|
357
|
+
this.resetRepoSpecificState();
|
|
358
|
+
this.loadCurrentTabData();
|
|
336
359
|
this.render();
|
|
337
360
|
}
|
|
338
361
|
initGitManager(oldRepoPath) {
|
|
339
|
-
// Clean up existing manager
|
|
362
|
+
// Clean up existing manager's event listeners
|
|
340
363
|
if (this.gitManager) {
|
|
341
|
-
this.gitManager.removeAllListeners();
|
|
364
|
+
this.gitManager.workingTree.removeAllListeners();
|
|
365
|
+
this.gitManager.history.removeAllListeners();
|
|
366
|
+
this.gitManager.compare.removeAllListeners();
|
|
367
|
+
this.gitManager.remote.removeAllListeners();
|
|
342
368
|
// Use oldRepoPath if provided (when switching repos), otherwise use current path
|
|
343
369
|
removeManagerForRepo(oldRepoPath ?? this.repoPath);
|
|
344
370
|
}
|
|
345
371
|
// Get or create manager for this repo
|
|
346
372
|
this.gitManager = getManagerForRepo(this.repoPath);
|
|
347
|
-
// Listen to state changes
|
|
348
|
-
this.gitManager.on('state-change', () => {
|
|
373
|
+
// Listen to working tree state changes
|
|
374
|
+
this.gitManager.workingTree.on('state-change', () => {
|
|
349
375
|
// Skip reconciliation while loading — the pending anchor must wait
|
|
350
376
|
// for the new status to arrive before being consumed
|
|
351
|
-
if (!this.gitManager?.state.isLoading) {
|
|
377
|
+
if (!this.gitManager?.workingTree.state.isLoading) {
|
|
352
378
|
this.reconcileSelectionAfterStateChange();
|
|
379
|
+
this.applyAutoTab();
|
|
353
380
|
}
|
|
354
381
|
this.updateExplorerGitStatus();
|
|
355
382
|
this.render();
|
|
356
383
|
});
|
|
357
|
-
|
|
384
|
+
// Listen to history state changes
|
|
385
|
+
this.gitManager.history.on('history-state-change', (historyState) => {
|
|
358
386
|
// Auto-select first commit when history loads
|
|
359
387
|
if (historyState.commits.length > 0 && !historyState.selectedCommit) {
|
|
360
388
|
const state = this.uiState.state;
|
|
361
389
|
if (state.bottomTab === 'history') {
|
|
362
|
-
this.selectHistoryCommitByIndex(state.historySelectedIndex);
|
|
390
|
+
this.navigation.selectHistoryCommitByIndex(state.historySelectedIndex);
|
|
363
391
|
}
|
|
364
392
|
}
|
|
365
393
|
this.render();
|
|
366
394
|
});
|
|
367
|
-
|
|
395
|
+
// Listen to compare state changes
|
|
396
|
+
this.gitManager.compare.on('compare-state-change', () => {
|
|
397
|
+
this.render();
|
|
398
|
+
});
|
|
399
|
+
this.gitManager.compare.on('compare-selection-change', () => {
|
|
368
400
|
this.render();
|
|
369
401
|
});
|
|
370
|
-
|
|
402
|
+
// Listen to remote operation state changes
|
|
403
|
+
this.gitManager.remote.on('remote-state-change', (remoteState) => {
|
|
404
|
+
// Auto-clear success after 3s, error after 5s
|
|
405
|
+
if (this.remoteClearTimer)
|
|
406
|
+
clearTimeout(this.remoteClearTimer);
|
|
407
|
+
if (remoteState.lastResult && !remoteState.inProgress) {
|
|
408
|
+
this.remoteClearTimer = setTimeout(() => {
|
|
409
|
+
this.gitManager?.remote.clearRemoteState();
|
|
410
|
+
}, 3000);
|
|
411
|
+
}
|
|
412
|
+
else if (remoteState.error) {
|
|
413
|
+
this.remoteClearTimer = setTimeout(() => {
|
|
414
|
+
this.gitManager?.remote.clearRemoteState();
|
|
415
|
+
}, 5000);
|
|
416
|
+
}
|
|
371
417
|
this.render();
|
|
372
418
|
});
|
|
373
419
|
// Start watching and do initial refresh
|
|
374
|
-
this.gitManager.startWatching();
|
|
375
|
-
this.gitManager.refresh();
|
|
420
|
+
this.gitManager.workingTree.startWatching();
|
|
421
|
+
this.gitManager.workingTree.refresh();
|
|
376
422
|
// Initialize explorer manager
|
|
377
423
|
this.initExplorerManager();
|
|
424
|
+
// Record this repo in recent repos list
|
|
425
|
+
this.recordCurrentRepo();
|
|
426
|
+
}
|
|
427
|
+
/**
|
|
428
|
+
* Load history with error handling (moved from facade).
|
|
429
|
+
*/
|
|
430
|
+
loadHistory(count = 100) {
|
|
431
|
+
this.gitManager?.history.loadHistory(count).catch((err) => {
|
|
432
|
+
this.showError(`Failed to load history: ${err instanceof Error ? err.message : String(err)}`);
|
|
433
|
+
});
|
|
378
434
|
}
|
|
379
435
|
/**
|
|
380
436
|
* After git state changes, reconcile the selected file index.
|
|
381
437
|
* Handles both flat mode (path-based anchoring) and categorized mode (category-based anchoring).
|
|
382
438
|
*/
|
|
383
439
|
reconcileSelectionAfterStateChange() {
|
|
384
|
-
const files = this.gitManager?.state.status?.files ?? [];
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
const
|
|
388
|
-
|
|
389
|
-
const newIndex = getFlatFileIndexByPath(flatFiles, targetPath);
|
|
440
|
+
const files = this.gitManager?.workingTree.state.status?.files ?? [];
|
|
441
|
+
const pendingFlatPath = this.staging.consumePendingFlatSelectionPath();
|
|
442
|
+
if (this.uiState.state.flatViewMode && pendingFlatPath) {
|
|
443
|
+
const flatFiles = buildFlatFileList(files, this.gitManager?.workingTree.state.hunkCounts ?? null);
|
|
444
|
+
const newIndex = getFlatFileIndexByPath(flatFiles, pendingFlatPath);
|
|
390
445
|
if (newIndex >= 0) {
|
|
391
446
|
this.uiState.setSelectedIndex(newIndex);
|
|
392
|
-
this.selectFileByIndex(newIndex);
|
|
447
|
+
this.navigation.selectFileByIndex(newIndex);
|
|
393
448
|
}
|
|
394
449
|
else if (flatFiles.length > 0) {
|
|
395
450
|
const clamped = Math.min(this.uiState.state.selectedIndex, flatFiles.length - 1);
|
|
396
451
|
this.uiState.setSelectedIndex(clamped);
|
|
397
|
-
this.selectFileByIndex(clamped);
|
|
452
|
+
this.navigation.selectFileByIndex(clamped);
|
|
398
453
|
}
|
|
399
454
|
return;
|
|
400
455
|
}
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
this.pendingSelectionAnchor = null;
|
|
456
|
+
const anchor = this.staging.consumePendingSelectionAnchor();
|
|
457
|
+
if (anchor) {
|
|
404
458
|
const newIndex = getIndexForCategoryPosition(files, anchor.category, anchor.categoryIndex);
|
|
405
459
|
this.uiState.setSelectedIndex(newIndex);
|
|
406
|
-
this.selectFileByIndex(newIndex);
|
|
460
|
+
this.navigation.selectFileByIndex(newIndex);
|
|
407
461
|
return;
|
|
408
462
|
}
|
|
409
|
-
// No pending anchor —
|
|
463
|
+
// No pending anchor — clamp to valid range and sync diff if file changed
|
|
464
|
+
const currentSelected = this.gitManager?.workingTree.state.selectedFile ?? null;
|
|
410
465
|
if (this.uiState.state.flatViewMode) {
|
|
411
|
-
const flatFiles = buildFlatFileList(files, this.gitManager?.state.hunkCounts ?? null);
|
|
466
|
+
const flatFiles = buildFlatFileList(files, this.gitManager?.workingTree.state.hunkCounts ?? null);
|
|
412
467
|
const maxIndex = flatFiles.length - 1;
|
|
413
|
-
|
|
414
|
-
|
|
468
|
+
let idx = this.uiState.state.selectedIndex;
|
|
469
|
+
if (maxIndex >= 0 && idx > maxIndex) {
|
|
470
|
+
idx = maxIndex;
|
|
471
|
+
this.uiState.setSelectedIndex(idx);
|
|
472
|
+
}
|
|
473
|
+
const flatEntry = getFlatFileAtIndex(flatFiles, idx);
|
|
474
|
+
const fileAtIdx = flatEntry?.unstagedEntry ?? flatEntry?.stagedEntry ?? null;
|
|
475
|
+
if (fileAtIdx &&
|
|
476
|
+
(fileAtIdx.path !== currentSelected?.path || fileAtIdx.staged !== currentSelected?.staged)) {
|
|
477
|
+
this.navigation.selectFileByIndex(idx);
|
|
415
478
|
}
|
|
416
479
|
}
|
|
417
480
|
else if (files.length > 0) {
|
|
418
481
|
const maxIndex = files.length - 1;
|
|
419
|
-
|
|
420
|
-
|
|
482
|
+
let idx = this.uiState.state.selectedIndex;
|
|
483
|
+
if (idx > maxIndex) {
|
|
484
|
+
idx = maxIndex;
|
|
485
|
+
this.uiState.setSelectedIndex(idx);
|
|
486
|
+
}
|
|
487
|
+
const fileAtIdx = getFileAtIndex(files, idx);
|
|
488
|
+
if (fileAtIdx &&
|
|
489
|
+
(fileAtIdx.path !== currentSelected?.path || fileAtIdx.staged !== currentSelected?.staged)) {
|
|
490
|
+
this.navigation.selectFileByIndex(idx);
|
|
421
491
|
}
|
|
422
492
|
}
|
|
423
493
|
}
|
|
@@ -439,6 +509,8 @@ export class App {
|
|
|
439
509
|
});
|
|
440
510
|
// Load root directory
|
|
441
511
|
this.explorerManager.loadDirectory('');
|
|
512
|
+
// Pre-load file paths for file finder (runs in background)
|
|
513
|
+
this.explorerManager.loadFilePaths();
|
|
442
514
|
// Update git status after tree is loaded
|
|
443
515
|
this.updateExplorerGitStatus();
|
|
444
516
|
}
|
|
@@ -448,7 +520,7 @@ export class App {
|
|
|
448
520
|
updateExplorerGitStatus() {
|
|
449
521
|
if (!this.explorerManager || !this.gitManager)
|
|
450
522
|
return;
|
|
451
|
-
const files = this.gitManager.state.status?.files ?? [];
|
|
523
|
+
const files = this.gitManager.workingTree.state.status?.files ?? [];
|
|
452
524
|
const statusMap = {
|
|
453
525
|
files: new Map(),
|
|
454
526
|
directories: new Set(),
|
|
@@ -472,8 +544,8 @@ export class App {
|
|
|
472
544
|
* Called when switching to a new repo via file watcher.
|
|
473
545
|
*/
|
|
474
546
|
resetRepoSpecificState() {
|
|
475
|
-
// Reset compare selection (
|
|
476
|
-
this.compareSelection = null;
|
|
547
|
+
// Reset compare selection (owned by NavigationController)
|
|
548
|
+
this.navigation.compareSelection = null;
|
|
477
549
|
// Reset UI state scroll offsets and selections
|
|
478
550
|
this.uiState.resetForNewRepo();
|
|
479
551
|
}
|
|
@@ -484,26 +556,26 @@ export class App {
|
|
|
484
556
|
loadCurrentTabData() {
|
|
485
557
|
const tab = this.uiState.state.bottomTab;
|
|
486
558
|
if (tab === 'history') {
|
|
487
|
-
this.
|
|
559
|
+
this.loadHistory();
|
|
488
560
|
}
|
|
489
561
|
else if (tab === 'compare') {
|
|
490
|
-
this.gitManager?.refreshCompareDiff(this.uiState.state.includeUncommitted);
|
|
562
|
+
this.gitManager?.compare.refreshCompareDiff(this.uiState.state.includeUncommitted);
|
|
491
563
|
}
|
|
492
|
-
// Diff tab data is loaded by gitManager.refresh() in initGitManager
|
|
564
|
+
// Diff tab data is loaded by gitManager.workingTree.refresh() in initGitManager
|
|
493
565
|
// Explorer data is loaded by initExplorerManager()
|
|
494
566
|
}
|
|
495
567
|
setupCommandHandler() {
|
|
496
568
|
if (!this.commandServer)
|
|
497
569
|
return;
|
|
498
570
|
const handler = {
|
|
499
|
-
navigateUp: () => this.navigateUp(),
|
|
500
|
-
navigateDown: () => this.navigateDown(),
|
|
571
|
+
navigateUp: () => this.navigation.navigateUp(),
|
|
572
|
+
navigateDown: () => this.navigation.navigateDown(),
|
|
501
573
|
switchTab: (tab) => this.uiState.setTab(tab),
|
|
502
574
|
togglePane: () => this.uiState.togglePane(),
|
|
503
|
-
stage: async () => this.stageSelected(),
|
|
504
|
-
unstage: async () => this.unstageSelected(),
|
|
505
|
-
stageAll: async () => this.stageAll(),
|
|
506
|
-
unstageAll: async () => this.unstageAll(),
|
|
575
|
+
stage: async () => this.staging.stageSelected(),
|
|
576
|
+
unstage: async () => this.staging.unstageSelected(),
|
|
577
|
+
stageAll: async () => this.staging.stageAll(),
|
|
578
|
+
unstageAll: async () => this.staging.unstageAll(),
|
|
507
579
|
commit: async (message) => this.commit(message),
|
|
508
580
|
refresh: async () => this.refresh(),
|
|
509
581
|
getState: () => this.getAppState(),
|
|
@@ -514,8 +586,8 @@ export class App {
|
|
|
514
586
|
}
|
|
515
587
|
getAppState() {
|
|
516
588
|
const state = this.uiState.state;
|
|
517
|
-
const gitState = this.gitManager?.state;
|
|
518
|
-
const historyState = this.gitManager?.historyState;
|
|
589
|
+
const gitState = this.gitManager?.workingTree.state;
|
|
590
|
+
const historyState = this.gitManager?.history.historyState;
|
|
519
591
|
const files = gitState?.status?.files ?? [];
|
|
520
592
|
const commits = historyState?.commits ?? [];
|
|
521
593
|
return {
|
|
@@ -542,486 +614,11 @@ export class App {
|
|
|
542
614
|
autoTabEnabled: state.autoTabEnabled,
|
|
543
615
|
};
|
|
544
616
|
}
|
|
545
|
-
// Navigation methods
|
|
546
|
-
/**
|
|
547
|
-
* Scroll the content pane (diff or explorer file content) by delta lines.
|
|
548
|
-
*/
|
|
549
|
-
scrollActiveDiffPane(delta) {
|
|
550
|
-
const state = this.uiState.state;
|
|
551
|
-
if (state.bottomTab === 'explorer') {
|
|
552
|
-
const newOffset = Math.max(0, state.explorerFileScrollOffset + delta);
|
|
553
|
-
this.uiState.setExplorerFileScrollOffset(newOffset);
|
|
554
|
-
}
|
|
555
|
-
else {
|
|
556
|
-
const newOffset = Math.max(0, state.diffScrollOffset + delta);
|
|
557
|
-
this.uiState.setDiffScrollOffset(newOffset);
|
|
558
|
-
}
|
|
559
|
-
}
|
|
560
|
-
/**
|
|
561
|
-
* Navigate the file list by one item and keep selection visible.
|
|
562
|
-
*/
|
|
563
|
-
navigateFileList(direction) {
|
|
564
|
-
const state = this.uiState.state;
|
|
565
|
-
const files = this.gitManager?.state.status?.files ?? [];
|
|
566
|
-
// Determine max index based on view mode
|
|
567
|
-
const maxIndex = state.flatViewMode ? this.cachedFlatFiles.length - 1 : files.length - 1;
|
|
568
|
-
if (maxIndex < 0)
|
|
569
|
-
return;
|
|
570
|
-
const newIndex = direction === -1
|
|
571
|
-
? Math.max(0, state.selectedIndex - 1)
|
|
572
|
-
: Math.min(maxIndex, state.selectedIndex + 1);
|
|
573
|
-
this.uiState.setSelectedIndex(newIndex);
|
|
574
|
-
this.selectFileByIndex(newIndex);
|
|
575
|
-
// In flat mode row === index + 1 (header row); in categorized mode account for headers/spacers
|
|
576
|
-
const row = state.flatViewMode ? newIndex + 1 : getRowFromFileIndex(newIndex, files);
|
|
577
|
-
this.scrollToKeepRowVisible(row, direction, state.fileListScrollOffset);
|
|
578
|
-
}
|
|
579
|
-
/**
|
|
580
|
-
* Scroll the file list to keep a given row visible.
|
|
581
|
-
*/
|
|
582
|
-
scrollToKeepRowVisible(row, direction, currentOffset) {
|
|
583
|
-
if (direction === -1 && row < currentOffset) {
|
|
584
|
-
this.uiState.setFileListScrollOffset(row);
|
|
585
|
-
}
|
|
586
|
-
else if (direction === 1) {
|
|
587
|
-
const visibleEnd = currentOffset + this.layout.dimensions.topPaneHeight - 1;
|
|
588
|
-
if (row >= visibleEnd) {
|
|
589
|
-
this.uiState.setFileListScrollOffset(currentOffset + (row - visibleEnd + 1));
|
|
590
|
-
}
|
|
591
|
-
}
|
|
592
|
-
}
|
|
593
|
-
/**
|
|
594
|
-
* Navigate the active list pane by one item in the given direction.
|
|
595
|
-
*/
|
|
596
|
-
navigateActiveList(direction) {
|
|
597
|
-
const tab = this.uiState.state.bottomTab;
|
|
598
|
-
if (tab === 'history') {
|
|
599
|
-
if (direction === -1)
|
|
600
|
-
this.navigateHistoryUp();
|
|
601
|
-
else
|
|
602
|
-
this.navigateHistoryDown();
|
|
603
|
-
}
|
|
604
|
-
else if (tab === 'compare') {
|
|
605
|
-
if (direction === -1)
|
|
606
|
-
this.navigateCompareUp();
|
|
607
|
-
else
|
|
608
|
-
this.navigateCompareDown();
|
|
609
|
-
}
|
|
610
|
-
else if (tab === 'explorer') {
|
|
611
|
-
if (direction === -1)
|
|
612
|
-
this.navigateExplorerUp();
|
|
613
|
-
else
|
|
614
|
-
this.navigateExplorerDown();
|
|
615
|
-
}
|
|
616
|
-
else {
|
|
617
|
-
this.navigateFileList(direction);
|
|
618
|
-
}
|
|
619
|
-
}
|
|
620
|
-
navigateUp() {
|
|
621
|
-
const state = this.uiState.state;
|
|
622
|
-
const isListPane = state.currentPane !== 'diff';
|
|
623
|
-
if (isListPane) {
|
|
624
|
-
this.navigateActiveList(-1);
|
|
625
|
-
}
|
|
626
|
-
else {
|
|
627
|
-
this.scrollActiveDiffPane(-3);
|
|
628
|
-
}
|
|
629
|
-
}
|
|
630
|
-
navigateDown() {
|
|
631
|
-
const state = this.uiState.state;
|
|
632
|
-
const isListPane = state.currentPane !== 'diff';
|
|
633
|
-
if (isListPane) {
|
|
634
|
-
this.navigateActiveList(1);
|
|
635
|
-
}
|
|
636
|
-
else {
|
|
637
|
-
this.scrollActiveDiffPane(3);
|
|
638
|
-
}
|
|
639
|
-
}
|
|
640
|
-
navigateHistoryUp() {
|
|
641
|
-
const state = this.uiState.state;
|
|
642
|
-
const newIndex = Math.max(0, state.historySelectedIndex - 1);
|
|
643
|
-
if (newIndex !== state.historySelectedIndex) {
|
|
644
|
-
this.uiState.setHistorySelectedIndex(newIndex);
|
|
645
|
-
// Keep selection visible
|
|
646
|
-
if (newIndex < state.historyScrollOffset) {
|
|
647
|
-
this.uiState.setHistoryScrollOffset(newIndex);
|
|
648
|
-
}
|
|
649
|
-
this.selectHistoryCommitByIndex(newIndex);
|
|
650
|
-
}
|
|
651
|
-
}
|
|
652
|
-
navigateHistoryDown() {
|
|
653
|
-
const state = this.uiState.state;
|
|
654
|
-
const commits = this.gitManager?.historyState.commits ?? [];
|
|
655
|
-
const newIndex = Math.min(commits.length - 1, state.historySelectedIndex + 1);
|
|
656
|
-
if (newIndex !== state.historySelectedIndex) {
|
|
657
|
-
this.uiState.setHistorySelectedIndex(newIndex);
|
|
658
|
-
// Keep selection visible
|
|
659
|
-
const visibleEnd = state.historyScrollOffset + this.layout.dimensions.topPaneHeight - 1;
|
|
660
|
-
if (newIndex >= visibleEnd) {
|
|
661
|
-
this.uiState.setHistoryScrollOffset(state.historyScrollOffset + 1);
|
|
662
|
-
}
|
|
663
|
-
this.selectHistoryCommitByIndex(newIndex);
|
|
664
|
-
}
|
|
665
|
-
}
|
|
666
|
-
selectHistoryCommitByIndex(index) {
|
|
667
|
-
const commits = this.gitManager?.historyState.commits ?? [];
|
|
668
|
-
const commit = getCommitAtIndex(commits, index);
|
|
669
|
-
if (commit) {
|
|
670
|
-
this.uiState.setDiffScrollOffset(0);
|
|
671
|
-
this.gitManager?.selectHistoryCommit(commit);
|
|
672
|
-
}
|
|
673
|
-
}
|
|
674
|
-
// Compare navigation
|
|
675
|
-
compareSelection = null;
|
|
676
|
-
navigateCompareUp() {
|
|
677
|
-
const compareState = this.gitManager?.compareState;
|
|
678
|
-
const commits = compareState?.compareDiff?.commits ?? [];
|
|
679
|
-
const files = compareState?.compareDiff?.files ?? [];
|
|
680
|
-
if (commits.length === 0 && files.length === 0)
|
|
681
|
-
return;
|
|
682
|
-
const next = getNextCompareSelection(this.compareSelection, commits, files, 'up');
|
|
683
|
-
if (next &&
|
|
684
|
-
(next.type !== this.compareSelection?.type || next.index !== this.compareSelection?.index)) {
|
|
685
|
-
this.selectCompareItem(next);
|
|
686
|
-
// Keep selection visible - scroll up if needed
|
|
687
|
-
const state = this.uiState.state;
|
|
688
|
-
const row = getRowFromCompareSelection(next, commits, files);
|
|
689
|
-
if (row < state.compareScrollOffset) {
|
|
690
|
-
this.uiState.setCompareScrollOffset(row);
|
|
691
|
-
}
|
|
692
|
-
}
|
|
693
|
-
}
|
|
694
|
-
navigateCompareDown() {
|
|
695
|
-
const compareState = this.gitManager?.compareState;
|
|
696
|
-
const commits = compareState?.compareDiff?.commits ?? [];
|
|
697
|
-
const files = compareState?.compareDiff?.files ?? [];
|
|
698
|
-
if (commits.length === 0 && files.length === 0)
|
|
699
|
-
return;
|
|
700
|
-
// Auto-select first item if nothing selected
|
|
701
|
-
if (!this.compareSelection) {
|
|
702
|
-
// Select first commit if available, otherwise first file
|
|
703
|
-
if (commits.length > 0) {
|
|
704
|
-
this.selectCompareItem({ type: 'commit', index: 0 });
|
|
705
|
-
}
|
|
706
|
-
else if (files.length > 0) {
|
|
707
|
-
this.selectCompareItem({ type: 'file', index: 0 });
|
|
708
|
-
}
|
|
709
|
-
return;
|
|
710
|
-
}
|
|
711
|
-
const next = getNextCompareSelection(this.compareSelection, commits, files, 'down');
|
|
712
|
-
if (next &&
|
|
713
|
-
(next.type !== this.compareSelection?.type || next.index !== this.compareSelection?.index)) {
|
|
714
|
-
this.selectCompareItem(next);
|
|
715
|
-
// Keep selection visible - scroll down if needed
|
|
716
|
-
const state = this.uiState.state;
|
|
717
|
-
const row = getRowFromCompareSelection(next, commits, files);
|
|
718
|
-
const visibleEnd = state.compareScrollOffset + this.layout.dimensions.topPaneHeight - 1;
|
|
719
|
-
if (row >= visibleEnd) {
|
|
720
|
-
this.uiState.setCompareScrollOffset(state.compareScrollOffset + (row - visibleEnd + 1));
|
|
721
|
-
}
|
|
722
|
-
}
|
|
723
|
-
}
|
|
724
|
-
selectCompareItem(selection) {
|
|
725
|
-
this.compareSelection = selection;
|
|
726
|
-
this.uiState.setDiffScrollOffset(0);
|
|
727
|
-
if (selection.type === 'commit') {
|
|
728
|
-
this.gitManager?.selectCompareCommit(selection.index);
|
|
729
|
-
}
|
|
730
|
-
else {
|
|
731
|
-
this.gitManager?.selectCompareFile(selection.index);
|
|
732
|
-
}
|
|
733
|
-
}
|
|
734
|
-
// Explorer navigation
|
|
735
|
-
navigateExplorerUp() {
|
|
736
|
-
const state = this.uiState.state;
|
|
737
|
-
const rows = this.explorerManager?.state.displayRows ?? [];
|
|
738
|
-
if (rows.length === 0)
|
|
739
|
-
return;
|
|
740
|
-
const newScrollOffset = this.explorerManager?.navigateUp(state.explorerScrollOffset);
|
|
741
|
-
if (newScrollOffset !== null && newScrollOffset !== undefined) {
|
|
742
|
-
this.uiState.setExplorerScrollOffset(newScrollOffset);
|
|
743
|
-
}
|
|
744
|
-
this.uiState.setExplorerSelectedIndex(this.explorerManager?.state.selectedIndex ?? 0);
|
|
745
|
-
}
|
|
746
|
-
navigateExplorerDown() {
|
|
747
|
-
const state = this.uiState.state;
|
|
748
|
-
const rows = this.explorerManager?.state.displayRows ?? [];
|
|
749
|
-
if (rows.length === 0)
|
|
750
|
-
return;
|
|
751
|
-
const visibleHeight = this.layout.dimensions.topPaneHeight;
|
|
752
|
-
const newScrollOffset = this.explorerManager?.navigateDown(state.explorerScrollOffset, visibleHeight);
|
|
753
|
-
if (newScrollOffset !== null && newScrollOffset !== undefined) {
|
|
754
|
-
this.uiState.setExplorerScrollOffset(newScrollOffset);
|
|
755
|
-
}
|
|
756
|
-
this.uiState.setExplorerSelectedIndex(this.explorerManager?.state.selectedIndex ?? 0);
|
|
757
|
-
}
|
|
758
|
-
async enterExplorerDirectory() {
|
|
759
|
-
await this.explorerManager?.enterDirectory();
|
|
760
|
-
// Reset file content scroll when expanding/collapsing
|
|
761
|
-
this.uiState.setExplorerFileScrollOffset(0);
|
|
762
|
-
// Sync selected index from explorer manager (it maintains selection by path)
|
|
763
|
-
this.uiState.setExplorerSelectedIndex(this.explorerManager?.state.selectedIndex ?? 0);
|
|
764
|
-
}
|
|
765
|
-
async goExplorerUp() {
|
|
766
|
-
await this.explorerManager?.goUp();
|
|
767
|
-
// Reset file content scroll when collapsing
|
|
768
|
-
this.uiState.setExplorerFileScrollOffset(0);
|
|
769
|
-
// Sync selected index from explorer manager
|
|
770
|
-
this.uiState.setExplorerSelectedIndex(this.explorerManager?.state.selectedIndex ?? 0);
|
|
771
|
-
}
|
|
772
|
-
selectFileByIndex(index) {
|
|
773
|
-
if (this.uiState.state.flatViewMode) {
|
|
774
|
-
const flatEntry = getFlatFileAtIndex(this.cachedFlatFiles, index);
|
|
775
|
-
if (flatEntry) {
|
|
776
|
-
// Prefer unstaged entry (shows unstaged diff for partial files), fallback to staged
|
|
777
|
-
const file = flatEntry.unstagedEntry ?? flatEntry.stagedEntry;
|
|
778
|
-
if (file) {
|
|
779
|
-
this.uiState.setDiffScrollOffset(0);
|
|
780
|
-
this.uiState.setSelectedHunkIndex(0);
|
|
781
|
-
this.gitManager?.selectFile(file);
|
|
782
|
-
}
|
|
783
|
-
}
|
|
784
|
-
}
|
|
785
|
-
else {
|
|
786
|
-
const files = this.gitManager?.state.status?.files ?? [];
|
|
787
|
-
const file = getFileAtIndex(files, index);
|
|
788
|
-
if (file) {
|
|
789
|
-
this.uiState.setDiffScrollOffset(0);
|
|
790
|
-
this.uiState.setSelectedHunkIndex(0);
|
|
791
|
-
this.gitManager?.selectFile(file);
|
|
792
|
-
}
|
|
793
|
-
}
|
|
794
|
-
}
|
|
795
|
-
/**
|
|
796
|
-
* Navigate to a file given its absolute path.
|
|
797
|
-
* Extracts the relative path and finds the file in the current file list.
|
|
798
|
-
*/
|
|
799
|
-
navigateToFile(absolutePath) {
|
|
800
|
-
if (!absolutePath || !this.repoPath)
|
|
801
|
-
return;
|
|
802
|
-
// Check if the path is within the current repo
|
|
803
|
-
const repoPrefix = this.repoPath.endsWith('/') ? this.repoPath : this.repoPath + '/';
|
|
804
|
-
if (!absolutePath.startsWith(repoPrefix))
|
|
805
|
-
return;
|
|
806
|
-
// Extract relative path
|
|
807
|
-
const relativePath = absolutePath.slice(repoPrefix.length);
|
|
808
|
-
if (!relativePath)
|
|
809
|
-
return;
|
|
810
|
-
// Find the file in the list
|
|
811
|
-
const files = this.gitManager?.state.status?.files ?? [];
|
|
812
|
-
const fileIndex = files.findIndex((f) => f.path === relativePath);
|
|
813
|
-
if (fileIndex >= 0) {
|
|
814
|
-
this.uiState.setSelectedIndex(fileIndex);
|
|
815
|
-
this.selectFileByIndex(fileIndex);
|
|
816
|
-
}
|
|
817
|
-
}
|
|
818
|
-
// Git operations
|
|
819
|
-
async stageSelected() {
|
|
820
|
-
const files = this.gitManager?.state.status?.files ?? [];
|
|
821
|
-
const index = this.uiState.state.selectedIndex;
|
|
822
|
-
if (this.uiState.state.flatViewMode) {
|
|
823
|
-
const flatEntry = getFlatFileAtIndex(this.cachedFlatFiles, index);
|
|
824
|
-
if (!flatEntry)
|
|
825
|
-
return;
|
|
826
|
-
// Stage: operate on the unstaged entry if available
|
|
827
|
-
const file = flatEntry.unstagedEntry;
|
|
828
|
-
if (file) {
|
|
829
|
-
this.pendingFlatSelectionPath = flatEntry.path;
|
|
830
|
-
await this.gitManager?.stage(file);
|
|
831
|
-
}
|
|
832
|
-
}
|
|
833
|
-
else {
|
|
834
|
-
const selectedFile = getFileAtIndex(files, index);
|
|
835
|
-
if (selectedFile && !selectedFile.staged) {
|
|
836
|
-
this.pendingSelectionAnchor = getCategoryForIndex(files, index);
|
|
837
|
-
await this.gitManager?.stage(selectedFile);
|
|
838
|
-
}
|
|
839
|
-
}
|
|
840
|
-
}
|
|
841
|
-
async unstageSelected() {
|
|
842
|
-
const files = this.gitManager?.state.status?.files ?? [];
|
|
843
|
-
const index = this.uiState.state.selectedIndex;
|
|
844
|
-
if (this.uiState.state.flatViewMode) {
|
|
845
|
-
const flatEntry = getFlatFileAtIndex(this.cachedFlatFiles, index);
|
|
846
|
-
if (!flatEntry)
|
|
847
|
-
return;
|
|
848
|
-
const file = flatEntry.stagedEntry;
|
|
849
|
-
if (file) {
|
|
850
|
-
this.pendingFlatSelectionPath = flatEntry.path;
|
|
851
|
-
await this.gitManager?.unstage(file);
|
|
852
|
-
}
|
|
853
|
-
}
|
|
854
|
-
else {
|
|
855
|
-
const selectedFile = getFileAtIndex(files, index);
|
|
856
|
-
if (selectedFile?.staged) {
|
|
857
|
-
this.pendingSelectionAnchor = getCategoryForIndex(files, index);
|
|
858
|
-
await this.gitManager?.unstage(selectedFile);
|
|
859
|
-
}
|
|
860
|
-
}
|
|
861
|
-
}
|
|
862
|
-
async toggleSelected() {
|
|
863
|
-
const index = this.uiState.state.selectedIndex;
|
|
864
|
-
if (this.uiState.state.flatViewMode) {
|
|
865
|
-
const flatEntry = getFlatFileAtIndex(this.cachedFlatFiles, index);
|
|
866
|
-
if (flatEntry)
|
|
867
|
-
await this.toggleFlatEntry(flatEntry);
|
|
868
|
-
}
|
|
869
|
-
else {
|
|
870
|
-
const files = this.gitManager?.state.status?.files ?? [];
|
|
871
|
-
const selectedFile = getFileAtIndex(files, index);
|
|
872
|
-
if (selectedFile) {
|
|
873
|
-
this.pendingSelectionAnchor = getCategoryForIndex(files, index);
|
|
874
|
-
if (selectedFile.staged) {
|
|
875
|
-
await this.gitManager?.unstage(selectedFile);
|
|
876
|
-
}
|
|
877
|
-
else {
|
|
878
|
-
await this.gitManager?.stage(selectedFile);
|
|
879
|
-
}
|
|
880
|
-
}
|
|
881
|
-
}
|
|
882
|
-
}
|
|
883
|
-
async stageAll() {
|
|
884
|
-
await this.gitManager?.stageAll();
|
|
885
|
-
}
|
|
886
|
-
async unstageAll() {
|
|
887
|
-
await this.gitManager?.unstageAll();
|
|
888
|
-
}
|
|
889
|
-
showDiscardConfirm(file) {
|
|
890
|
-
this.activeModal = new DiscardConfirm(this.screen, file.path, async () => {
|
|
891
|
-
this.activeModal = null;
|
|
892
|
-
await this.gitManager?.discard(file);
|
|
893
|
-
}, () => {
|
|
894
|
-
this.activeModal = null;
|
|
895
|
-
});
|
|
896
|
-
this.activeModal.focus();
|
|
897
|
-
}
|
|
898
|
-
// Hunk navigation and staging
|
|
899
|
-
navigateNextHunk() {
|
|
900
|
-
const current = this.uiState.state.selectedHunkIndex;
|
|
901
|
-
if (this.bottomPaneHunkCount > 0 && current < this.bottomPaneHunkCount - 1) {
|
|
902
|
-
this.uiState.setSelectedHunkIndex(current + 1);
|
|
903
|
-
this.scrollHunkIntoView(current + 1);
|
|
904
|
-
}
|
|
905
|
-
}
|
|
906
|
-
navigatePrevHunk() {
|
|
907
|
-
const current = this.uiState.state.selectedHunkIndex;
|
|
908
|
-
if (current > 0) {
|
|
909
|
-
this.uiState.setSelectedHunkIndex(current - 1);
|
|
910
|
-
this.scrollHunkIntoView(current - 1);
|
|
911
|
-
}
|
|
912
|
-
}
|
|
913
|
-
scrollHunkIntoView(hunkIndex) {
|
|
914
|
-
const boundary = this.bottomPaneHunkBoundaries[hunkIndex];
|
|
915
|
-
if (!boundary)
|
|
916
|
-
return;
|
|
917
|
-
const scrollOffset = this.uiState.state.diffScrollOffset;
|
|
918
|
-
const visibleHeight = this.layout.dimensions.bottomPaneHeight;
|
|
919
|
-
// If hunk header is outside the visible area, scroll so it's at top
|
|
920
|
-
if (boundary.startRow < scrollOffset || boundary.startRow >= scrollOffset + visibleHeight) {
|
|
921
|
-
this.uiState.setDiffScrollOffset(boundary.startRow);
|
|
922
|
-
}
|
|
923
|
-
}
|
|
924
|
-
selectHunkAtRow(visualRow) {
|
|
925
|
-
if (this.uiState.state.bottomTab !== 'diff')
|
|
926
|
-
return;
|
|
927
|
-
if (this.bottomPaneHunkBoundaries.length === 0)
|
|
928
|
-
return;
|
|
929
|
-
// Focus the diff pane so the hunk gutter appears
|
|
930
|
-
this.uiState.setPane('diff');
|
|
931
|
-
const absoluteRow = this.uiState.state.diffScrollOffset + visualRow;
|
|
932
|
-
for (let i = 0; i < this.bottomPaneHunkBoundaries.length; i++) {
|
|
933
|
-
const b = this.bottomPaneHunkBoundaries[i];
|
|
934
|
-
if (absoluteRow >= b.startRow && absoluteRow < b.endRow) {
|
|
935
|
-
this.uiState.setSelectedHunkIndex(i);
|
|
936
|
-
return;
|
|
937
|
-
}
|
|
938
|
-
}
|
|
939
|
-
}
|
|
940
|
-
async toggleCurrentHunk() {
|
|
941
|
-
const selectedFile = this.gitManager?.state.selectedFile;
|
|
942
|
-
if (!selectedFile || selectedFile.status === 'untracked')
|
|
943
|
-
return;
|
|
944
|
-
if (this.uiState.state.flatViewMode) {
|
|
945
|
-
await this.toggleCurrentHunkFlat();
|
|
946
|
-
}
|
|
947
|
-
else {
|
|
948
|
-
await this.toggleCurrentHunkCategorized(selectedFile);
|
|
949
|
-
}
|
|
950
|
-
}
|
|
951
|
-
async toggleCurrentHunkFlat() {
|
|
952
|
-
const mapping = this.combinedHunkMapping[this.uiState.state.selectedHunkIndex];
|
|
953
|
-
if (!mapping)
|
|
954
|
-
return;
|
|
955
|
-
const combined = this.gitManager?.state.combinedFileDiffs;
|
|
956
|
-
if (!combined)
|
|
957
|
-
return;
|
|
958
|
-
const rawDiff = mapping.source === 'unstaged' ? combined.unstaged.raw : combined.staged.raw;
|
|
959
|
-
const patch = extractHunkPatch(rawDiff, mapping.hunkIndex);
|
|
960
|
-
if (!patch)
|
|
961
|
-
return;
|
|
962
|
-
// Preserve hunk index across refresh — file stays selected via path-only fallback
|
|
963
|
-
this.pendingHunkIndex = this.uiState.state.selectedHunkIndex;
|
|
964
|
-
if (mapping.source === 'staged') {
|
|
965
|
-
await this.gitManager?.unstageHunk(patch);
|
|
966
|
-
}
|
|
967
|
-
else {
|
|
968
|
-
await this.gitManager?.stageHunk(patch);
|
|
969
|
-
}
|
|
970
|
-
}
|
|
971
|
-
async toggleCurrentHunkCategorized(selectedFile) {
|
|
972
|
-
const rawDiff = this.gitManager?.state.diff?.raw;
|
|
973
|
-
if (!rawDiff)
|
|
974
|
-
return;
|
|
975
|
-
const patch = extractHunkPatch(rawDiff, this.uiState.state.selectedHunkIndex);
|
|
976
|
-
if (!patch)
|
|
977
|
-
return;
|
|
978
|
-
const files = this.gitManager?.state.status?.files ?? [];
|
|
979
|
-
this.pendingSelectionAnchor = getCategoryForIndex(files, this.uiState.state.selectedIndex);
|
|
980
|
-
if (selectedFile.staged) {
|
|
981
|
-
await this.gitManager?.unstageHunk(patch);
|
|
982
|
-
}
|
|
983
|
-
else {
|
|
984
|
-
await this.gitManager?.stageHunk(patch);
|
|
985
|
-
}
|
|
986
|
-
}
|
|
987
|
-
async openFileFinder() {
|
|
988
|
-
const allPaths = (await this.explorerManager?.getAllFilePaths()) ?? [];
|
|
989
|
-
if (allPaths.length === 0)
|
|
990
|
-
return;
|
|
991
|
-
this.activeModal = new FileFinder(this.screen, allPaths, async (selectedPath) => {
|
|
992
|
-
this.activeModal = null;
|
|
993
|
-
// Switch to explorer tab if not already there
|
|
994
|
-
if (this.uiState.state.bottomTab !== 'explorer') {
|
|
995
|
-
this.uiState.setTab('explorer');
|
|
996
|
-
}
|
|
997
|
-
// Navigate to the selected file in explorer
|
|
998
|
-
const success = await this.explorerManager?.navigateToPath(selectedPath);
|
|
999
|
-
if (success) {
|
|
1000
|
-
// Sync selected index from explorer manager
|
|
1001
|
-
const selectedIndex = this.explorerManager?.state.selectedIndex ?? 0;
|
|
1002
|
-
this.uiState.setExplorerSelectedIndex(selectedIndex);
|
|
1003
|
-
this.uiState.setExplorerFileScrollOffset(0);
|
|
1004
|
-
// Scroll to make selected file visible
|
|
1005
|
-
const visibleHeight = this.layout.dimensions.topPaneHeight;
|
|
1006
|
-
if (selectedIndex >= visibleHeight) {
|
|
1007
|
-
this.uiState.setExplorerScrollOffset(selectedIndex - Math.floor(visibleHeight / 2));
|
|
1008
|
-
}
|
|
1009
|
-
else {
|
|
1010
|
-
this.uiState.setExplorerScrollOffset(0);
|
|
1011
|
-
}
|
|
1012
|
-
}
|
|
1013
|
-
this.render();
|
|
1014
|
-
}, () => {
|
|
1015
|
-
this.activeModal = null;
|
|
1016
|
-
this.render();
|
|
1017
|
-
});
|
|
1018
|
-
this.activeModal.focus();
|
|
1019
|
-
}
|
|
1020
617
|
async commit(message) {
|
|
1021
|
-
await this.gitManager?.commit(message);
|
|
618
|
+
await this.gitManager?.workingTree.commit(message);
|
|
1022
619
|
}
|
|
1023
620
|
async refresh() {
|
|
1024
|
-
await this.gitManager?.refresh();
|
|
621
|
+
await this.gitManager?.workingTree.refresh();
|
|
1025
622
|
}
|
|
1026
623
|
toggleMouseMode() {
|
|
1027
624
|
const willEnable = !this.uiState.state.mouseEnabled;
|
|
@@ -1036,6 +633,29 @@ export class App {
|
|
|
1036
633
|
program.disableMouse();
|
|
1037
634
|
}
|
|
1038
635
|
}
|
|
636
|
+
/**
|
|
637
|
+
* When auto-tab is enabled, switch tabs based on file count transitions:
|
|
638
|
+
* - Files disappear (prev > 0, current === 0): switch to history
|
|
639
|
+
* - Files appear (prev === 0, current > 0): switch to diff
|
|
640
|
+
* Always updates prevFileCount so enabling doesn't trigger on stale state.
|
|
641
|
+
*/
|
|
642
|
+
applyAutoTab() {
|
|
643
|
+
const files = this.gitManager?.workingTree.state.status?.files ?? [];
|
|
644
|
+
const currentCount = files.length;
|
|
645
|
+
const prev = this.prevFileCount;
|
|
646
|
+
this.prevFileCount = currentCount;
|
|
647
|
+
if (!this.uiState.state.autoTabEnabled)
|
|
648
|
+
return;
|
|
649
|
+
const tab = this.uiState.state.bottomTab;
|
|
650
|
+
if (prev > 0 && currentCount === 0 && (tab === 'diff' || tab === 'commit')) {
|
|
651
|
+
this.uiState.setHistorySelectedIndex(0);
|
|
652
|
+
this.uiState.setHistoryScrollOffset(0);
|
|
653
|
+
this.uiState.setTab('history');
|
|
654
|
+
}
|
|
655
|
+
else if (prev === 0 && currentCount > 0 && tab === 'history') {
|
|
656
|
+
this.uiState.setTab('diff');
|
|
657
|
+
}
|
|
658
|
+
}
|
|
1039
659
|
toggleFollow() {
|
|
1040
660
|
if (!this.followMode) {
|
|
1041
661
|
this.followMode = new FollowMode(this.config.targetFile, () => this.repoPath, {
|
|
@@ -1071,44 +691,56 @@ export class App {
|
|
|
1071
691
|
this.updateTopPane();
|
|
1072
692
|
this.updateBottomPane();
|
|
1073
693
|
// Restore hunk index after diff refresh (e.g. after hunk toggle in flat mode)
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
this.
|
|
694
|
+
const pendingHunk = this.staging.consumePendingHunkIndex();
|
|
695
|
+
if (pendingHunk !== null && this.bottomPaneHunkCount > 0) {
|
|
696
|
+
const restored = Math.min(pendingHunk, this.bottomPaneHunkCount - 1);
|
|
1077
697
|
this.uiState.setSelectedHunkIndex(restored);
|
|
1078
698
|
this.updateBottomPane(); // Re-render with correct hunk selection
|
|
1079
699
|
}
|
|
700
|
+
this.updateSeparators();
|
|
1080
701
|
this.updateFooter();
|
|
1081
702
|
this.screen.render();
|
|
1082
703
|
}
|
|
704
|
+
updateSeparators() {
|
|
705
|
+
const zone = this.uiState.state.focusedZone;
|
|
706
|
+
// Top-pane zones: fileList, historyList, compareList, explorerTree
|
|
707
|
+
const isTopPaneZone = zone === 'fileList' ||
|
|
708
|
+
zone === 'historyList' ||
|
|
709
|
+
zone === 'compareList' ||
|
|
710
|
+
zone === 'explorerTree';
|
|
711
|
+
this.layout.middleSeparator.style.fg = isTopPaneZone ? 'cyan' : 'gray';
|
|
712
|
+
}
|
|
1083
713
|
updateHeader() {
|
|
1084
|
-
const gitState = this.gitManager?.state;
|
|
714
|
+
const gitState = this.gitManager?.workingTree.state;
|
|
1085
715
|
const width = this.screen.width || 80;
|
|
1086
|
-
const content = formatHeader(this.repoPath, gitState?.status?.branch ?? null, gitState?.isLoading ?? false, gitState?.error ?? null, width);
|
|
716
|
+
const content = formatHeader(this.repoPath, gitState?.status?.branch ?? null, gitState?.isLoading ?? false, gitState?.error ?? null, width, this.gitManager?.remote.remoteState ?? null);
|
|
1087
717
|
this.layout.headerBox.setContent(content);
|
|
1088
718
|
}
|
|
1089
719
|
updateTopPane() {
|
|
1090
720
|
const state = this.uiState.state;
|
|
1091
721
|
const width = this.screen.width || 80;
|
|
1092
|
-
const files = this.gitManager?.state.status?.files ?? [];
|
|
722
|
+
const files = this.gitManager?.workingTree.state.status?.files ?? [];
|
|
1093
723
|
// Build and cache flat file list when in flat mode
|
|
1094
724
|
if (state.flatViewMode) {
|
|
1095
|
-
this.cachedFlatFiles = buildFlatFileList(files, this.gitManager?.state.hunkCounts ?? null);
|
|
725
|
+
this.cachedFlatFiles = buildFlatFileList(files, this.gitManager?.workingTree.state.hunkCounts ?? null);
|
|
1096
726
|
}
|
|
1097
|
-
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);
|
|
727
|
+
const content = renderTopPane(state, files, this.gitManager?.history.historyState?.commits ?? [], this.gitManager?.compare.compareState?.compareDiff ?? null, this.navigation.compareSelection, this.explorerManager?.state, width, this.layout.dimensions.topPaneHeight, this.gitManager?.workingTree.state.hunkCounts, state.flatViewMode ? this.cachedFlatFiles : undefined);
|
|
1098
728
|
this.layout.topPane.setContent(content);
|
|
1099
729
|
}
|
|
1100
730
|
updateBottomPane() {
|
|
1101
731
|
const state = this.uiState.state;
|
|
1102
732
|
const width = this.screen.width || 80;
|
|
1103
|
-
const files = this.gitManager?.state.status?.files ?? [];
|
|
733
|
+
const files = this.gitManager?.workingTree.state.status?.files ?? [];
|
|
1104
734
|
const stagedCount = files.filter((f) => f.staged).length;
|
|
1105
735
|
// Update staged count for commit validation
|
|
1106
736
|
this.commitFlowState.setStagedCount(stagedCount);
|
|
1107
737
|
// Pass selectedHunkIndex and staged status only when diff pane is focused on diff tab
|
|
1108
738
|
const diffPaneFocused = state.bottomTab === 'diff' && state.currentPane === 'diff';
|
|
1109
739
|
const hunkIndex = diffPaneFocused ? state.selectedHunkIndex : undefined;
|
|
1110
|
-
const isFileStaged = diffPaneFocused
|
|
1111
|
-
|
|
740
|
+
const isFileStaged = diffPaneFocused
|
|
741
|
+
? this.gitManager?.workingTree.state.selectedFile?.staged
|
|
742
|
+
: undefined;
|
|
743
|
+
const { content, totalRows, hunkCount, hunkBoundaries, hunkMapping } = renderBottomPane(state, this.gitManager?.workingTree.state.diff ?? null, this.gitManager?.history.historyState, this.gitManager?.compare.compareSelectionState, this.explorerManager?.state?.selectedFile ?? null, this.commitFlowState.state, stagedCount, this.currentTheme, width, this.layout.dimensions.bottomPaneHeight, hunkIndex, isFileStaged, state.flatViewMode ? this.gitManager?.workingTree.state.combinedFileDiffs : undefined, state.focusedZone);
|
|
1112
744
|
this.bottomPaneTotalRows = totalRows;
|
|
1113
745
|
this.bottomPaneHunkCount = hunkCount;
|
|
1114
746
|
this.bottomPaneHunkBoundaries = hunkBoundaries;
|
|
@@ -1149,6 +781,9 @@ export class App {
|
|
|
1149
781
|
if (this.commandServer) {
|
|
1150
782
|
this.commandServer.stop();
|
|
1151
783
|
}
|
|
784
|
+
if (this.remoteClearTimer) {
|
|
785
|
+
clearTimeout(this.remoteClearTimer);
|
|
786
|
+
}
|
|
1152
787
|
// Destroy screen (this will clean up terminal)
|
|
1153
788
|
this.screen.destroy();
|
|
1154
789
|
}
|