diffstalker 0.2.3 → 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.
Files changed (46) hide show
  1. package/.dependency-cruiser.cjs +2 -2
  2. package/dist/App.js +278 -758
  3. package/dist/KeyBindings.js +103 -91
  4. package/dist/ModalController.js +166 -0
  5. package/dist/MouseHandlers.js +37 -30
  6. package/dist/NavigationController.js +290 -0
  7. package/dist/StagingOperations.js +199 -0
  8. package/dist/config.js +39 -0
  9. package/dist/core/CompareManager.js +134 -0
  10. package/dist/core/ExplorerStateManager.js +7 -3
  11. package/dist/core/GitStateManager.js +28 -771
  12. package/dist/core/HistoryManager.js +72 -0
  13. package/dist/core/RemoteOperationManager.js +109 -0
  14. package/dist/core/WorkingTreeManager.js +412 -0
  15. package/dist/index.js +57 -57
  16. package/dist/state/FocusRing.js +40 -0
  17. package/dist/state/UIState.js +82 -48
  18. package/dist/ui/PaneRenderers.js +3 -6
  19. package/dist/ui/modals/BaseBranchPicker.js +4 -7
  20. package/dist/ui/modals/CommitActionConfirm.js +4 -4
  21. package/dist/ui/modals/DiscardConfirm.js +4 -7
  22. package/dist/ui/modals/FileFinder.js +3 -6
  23. package/dist/ui/modals/HotkeysModal.js +17 -21
  24. package/dist/ui/modals/Modal.js +1 -0
  25. package/dist/ui/modals/RepoPicker.js +109 -0
  26. package/dist/ui/modals/ThemePicker.js +4 -7
  27. package/dist/ui/widgets/CommitPanel.js +26 -94
  28. package/dist/ui/widgets/CompareListView.js +1 -11
  29. package/dist/ui/widgets/DiffView.js +2 -27
  30. package/dist/ui/widgets/ExplorerContent.js +1 -4
  31. package/dist/ui/widgets/ExplorerView.js +1 -11
  32. package/dist/ui/widgets/FileList.js +2 -8
  33. package/dist/ui/widgets/Footer.js +1 -0
  34. package/dist/utils/ansi.js +38 -0
  35. package/dist/utils/ansiTruncate.js +1 -5
  36. package/dist/utils/displayRows.js +72 -59
  37. package/dist/utils/fileCategories.js +7 -0
  38. package/dist/utils/fileResolution.js +23 -0
  39. package/dist/utils/languageDetection.js +3 -2
  40. package/dist/utils/logger.js +32 -0
  41. package/metrics/v0.2.4.json +236 -0
  42. package/package.json +1 -1
  43. package/dist/ui/modals/BranchPicker.js +0 -157
  44. package/dist/ui/modals/SoftResetConfirm.js +0 -68
  45. package/dist/ui/modals/StashListModal.js +0 -98
  46. package/dist/utils/layoutCalculations.js +0 -100
@@ -1,12 +1,17 @@
1
1
  import { SPLIT_RATIO_STEP } from './ui/Layout.js';
2
- import { getFileAtIndex } from './ui/widgets/FileList.js';
3
- import { getFlatFileAtIndex } from './utils/flatFileList.js';
4
2
  /**
5
3
  * Register all keyboard bindings on the blessed screen.
6
4
  */
7
5
  export function setupKeyBindings(screen, actions, ctx) {
8
- // Quit
9
- screen.key(['q', 'C-c'], () => {
6
+ // Quit: q closes modal if open, Ctrl+C always exits
7
+ screen.key(['q'], () => {
8
+ if (ctx.hasActiveModal()) {
9
+ actions.closeActiveModal();
10
+ return;
11
+ }
12
+ actions.exit();
13
+ });
14
+ screen.key(['C-c'], () => {
10
15
  actions.exit();
11
16
  });
12
17
  // Navigation (skip if modal is open)
@@ -35,11 +40,16 @@ export function setupKeyBindings(screen, actions, ctx) {
35
40
  ctx.uiState.setTab(tab);
36
41
  });
37
42
  }
38
- // Pane toggle (skip if modal is open)
43
+ // Focus zone cycling (skip if modal or commit input is active)
39
44
  screen.key(['tab'], () => {
40
- if (ctx.hasActiveModal())
45
+ if (ctx.hasActiveModal() || ctx.isCommitInputFocused())
41
46
  return;
42
- ctx.uiState.togglePane();
47
+ ctx.uiState.advanceFocus();
48
+ });
49
+ screen.key(['S-tab'], () => {
50
+ if (ctx.hasActiveModal() || ctx.isCommitInputFocused())
51
+ return;
52
+ ctx.uiState.retreatFocus();
43
53
  });
44
54
  // Staging operations (skip if modal is open)
45
55
  // Context-aware: hunk staging when diff pane is focused on diff tab
@@ -72,6 +82,17 @@ export function setupKeyBindings(screen, actions, ctx) {
72
82
  screen.key(['enter', 'space'], () => {
73
83
  if (ctx.hasActiveModal())
74
84
  return;
85
+ const zone = ctx.getFocusedZone();
86
+ // Zone-aware dispatch for commit panel elements
87
+ if (zone === 'commitMessage' && !ctx.isCommitInputFocused()) {
88
+ actions.focusCommitInput();
89
+ return;
90
+ }
91
+ if (zone === 'commitAmend') {
92
+ ctx.commitFlowState.toggleAmend();
93
+ actions.render();
94
+ return;
95
+ }
75
96
  if (ctx.getBottomTab() === 'explorer' && ctx.getCurrentPane() === 'explorer') {
76
97
  actions.enterExplorerDirectory();
77
98
  }
@@ -122,6 +143,8 @@ export function setupKeyBindings(screen, actions, ctx) {
122
143
  }
123
144
  });
124
145
  screen.key(['a'], () => {
146
+ if (ctx.hasActiveModal())
147
+ return;
125
148
  if (ctx.getBottomTab() === 'commit' && !ctx.isCommitInputFocused()) {
126
149
  ctx.commitFlowState.toggleAmend();
127
150
  actions.render();
@@ -137,23 +160,12 @@ export function setupKeyBindings(screen, actions, ctx) {
137
160
  actions.render();
138
161
  }
139
162
  });
140
- // Remote operations (global, any tab)
141
- screen.key(['S-p'], () => {
142
- if (ctx.hasActiveModal() || ctx.isCommitInputFocused() || ctx.isRemoteInProgress())
143
- return;
144
- actions.push();
145
- });
146
- screen.key(['S-f'], () => {
147
- if (ctx.hasActiveModal() || ctx.isCommitInputFocused() || ctx.isRemoteInProgress())
148
- return;
149
- actions.fetchRemote();
150
- });
151
- screen.key(['S-r'], () => {
152
- if (ctx.hasActiveModal() || ctx.isCommitInputFocused() || ctx.isRemoteInProgress())
153
- return;
154
- actions.pullRebase();
155
- });
163
+ // Escape: close modal first, then commit-tab escape logic
156
164
  screen.key(['escape'], () => {
165
+ if (ctx.hasActiveModal()) {
166
+ actions.closeActiveModal();
167
+ return;
168
+ }
157
169
  if (ctx.getBottomTab() === 'commit') {
158
170
  if (ctx.isCommitInputFocused()) {
159
171
  actions.unfocusCommitInput();
@@ -163,12 +175,32 @@ export function setupKeyBindings(screen, actions, ctx) {
163
175
  }
164
176
  }
165
177
  });
166
- // Refresh
167
- screen.key(['r'], () => actions.refresh());
168
- // Display toggles
169
- screen.key(['w'], () => ctx.uiState.toggleWrapMode());
170
- screen.key(['m'], () => actions.toggleMouseMode());
171
- screen.key(['S-t'], () => ctx.uiState.toggleAutoTab());
178
+ // Repo picker (toggle)
179
+ screen.key(['r'], () => {
180
+ if (ctx.getActiveModalType() === 'repoPicker') {
181
+ actions.closeActiveModal();
182
+ return;
183
+ }
184
+ if (ctx.hasActiveModal())
185
+ return;
186
+ actions.openRepoPicker();
187
+ });
188
+ // Display toggles (guarded)
189
+ screen.key(['w'], () => {
190
+ if (ctx.hasActiveModal())
191
+ return;
192
+ ctx.uiState.toggleWrapMode();
193
+ });
194
+ screen.key(['m'], () => {
195
+ if (ctx.hasActiveModal())
196
+ return;
197
+ actions.toggleMouseMode();
198
+ });
199
+ screen.key(['S-t'], () => {
200
+ if (ctx.hasActiveModal())
201
+ return;
202
+ ctx.uiState.toggleAutoTab();
203
+ });
172
204
  // Split ratio adjustments
173
205
  screen.key(['-', '_', '['], () => {
174
206
  ctx.uiState.adjustSplitRatio(-SPLIT_RATIO_STEP);
@@ -180,21 +212,42 @@ export function setupKeyBindings(screen, actions, ctx) {
180
212
  ctx.layout.setSplitRatio(ctx.uiState.state.splitRatio);
181
213
  actions.render();
182
214
  });
183
- // Theme picker
184
- screen.key(['t'], () => ctx.uiState.openModal('theme'));
185
- // Hotkeys modal
186
- screen.key(['?'], () => ctx.uiState.toggleModal('hotkeys'));
187
- // Follow toggle
188
- screen.key(['f'], () => actions.toggleFollow());
189
- // Compare view: base branch picker / Commit tab: branch picker
215
+ // Theme picker (toggle)
216
+ screen.key(['t'], () => {
217
+ if (ctx.getActiveModalType() === 'theme') {
218
+ actions.closeActiveModal();
219
+ return;
220
+ }
221
+ if (ctx.hasActiveModal())
222
+ return;
223
+ actions.openThemePicker();
224
+ });
225
+ // Hotkeys modal (toggle)
226
+ screen.key(['?'], () => {
227
+ if (ctx.getActiveModalType() === 'hotkeys') {
228
+ actions.closeActiveModal();
229
+ return;
230
+ }
231
+ if (ctx.hasActiveModal())
232
+ return;
233
+ actions.openHotkeysModal();
234
+ });
235
+ // Follow toggle (guarded)
236
+ screen.key(['f'], () => {
237
+ if (ctx.hasActiveModal())
238
+ return;
239
+ actions.toggleFollow();
240
+ });
241
+ // Compare view: base branch picker (toggle)
190
242
  screen.key(['b'], () => {
191
- if (ctx.hasActiveModal() || ctx.isCommitInputFocused())
243
+ if (ctx.getActiveModalType() === 'baseBranch') {
244
+ actions.closeActiveModal();
192
245
  return;
193
- if (ctx.getBottomTab() === 'compare') {
194
- ctx.uiState.openModal('baseBranch');
195
246
  }
196
- else if (ctx.getBottomTab() === 'commit') {
197
- actions.openBranchPicker();
247
+ if (ctx.hasActiveModal())
248
+ return;
249
+ if (ctx.getBottomTab() === 'compare') {
250
+ actions.openBaseBranchPicker();
198
251
  }
199
252
  });
200
253
  // u: toggle uncommitted in compare view
@@ -204,7 +257,7 @@ export function setupKeyBindings(screen, actions, ctx) {
204
257
  if (ctx.getBottomTab() === 'compare') {
205
258
  ctx.uiState.toggleIncludeUncommitted();
206
259
  const includeUncommitted = ctx.uiState.state.includeUncommitted;
207
- ctx.getGitManager()?.refreshCompareDiff(includeUncommitted);
260
+ ctx.getGitManager()?.compare.refreshCompareDiff(includeUncommitted);
208
261
  }
209
262
  });
210
263
  // Toggle flat file view (diff/commit tab only)
@@ -216,25 +269,14 @@ export function setupKeyBindings(screen, actions, ctx) {
216
269
  ctx.uiState.toggleFlatViewMode();
217
270
  }
218
271
  });
219
- // Discard changes (with confirmation)
272
+ // Discard changes (with confirmation, guarded)
220
273
  screen.key(['d'], () => {
274
+ if (ctx.hasActiveModal())
275
+ return;
221
276
  if (ctx.getBottomTab() === 'diff') {
222
- if (ctx.uiState.state.flatViewMode) {
223
- const flatEntry = getFlatFileAtIndex(ctx.getCachedFlatFiles(), ctx.getSelectedIndex());
224
- if (flatEntry?.unstagedEntry) {
225
- const file = flatEntry.unstagedEntry;
226
- if (file.status !== 'untracked') {
227
- actions.showDiscardConfirm(file);
228
- }
229
- }
230
- }
231
- else {
232
- const files = ctx.getStatusFiles();
233
- const selectedFile = getFileAtIndex(files, ctx.getSelectedIndex());
234
- // Only allow discard for unstaged modified files
235
- if (selectedFile && !selectedFile.staged && selectedFile.status !== 'untracked') {
236
- actions.showDiscardConfirm(selectedFile);
237
- }
277
+ const file = ctx.resolveFileAtIndex(ctx.getSelectedIndex());
278
+ if (file && !file.staged && file.status !== 'untracked') {
279
+ actions.openDiscardConfirm(file);
238
280
  }
239
281
  }
240
282
  });
@@ -253,42 +295,12 @@ export function setupKeyBindings(screen, actions, ctx) {
253
295
  actions.navigatePrevHunk();
254
296
  }
255
297
  });
256
- // Stash: save (global)
257
- screen.key(['S-s'], () => {
258
- if (ctx.hasActiveModal() || ctx.isCommitInputFocused() || ctx.isRemoteInProgress())
259
- return;
260
- actions.stash();
261
- });
262
- // Stash: pop (commit tab only)
263
- screen.key(['o'], () => {
264
- if (ctx.hasActiveModal() || ctx.isCommitInputFocused())
265
- return;
266
- if (ctx.getBottomTab() === 'commit') {
267
- actions.stashPop();
268
- }
269
- });
270
- // Stash: list modal (commit tab only)
271
- screen.key(['l'], () => {
272
- if (ctx.hasActiveModal() || ctx.isCommitInputFocused())
273
- return;
274
- if (ctx.getBottomTab() === 'commit') {
275
- actions.openStashListModal();
276
- }
277
- });
278
- // Soft reset HEAD~1 (commit tab only)
279
- screen.key(['S-x'], () => {
280
- if (ctx.hasActiveModal() || ctx.isCommitInputFocused() || ctx.isRemoteInProgress())
281
- return;
282
- if (ctx.getBottomTab() === 'commit') {
283
- actions.showSoftResetConfirm();
284
- }
285
- });
286
298
  // Cherry-pick selected commit (history tab only)
287
299
  screen.key(['p'], () => {
288
300
  if (ctx.hasActiveModal() || ctx.isCommitInputFocused())
289
301
  return;
290
302
  if (ctx.getBottomTab() === 'history') {
291
- actions.cherryPickSelected();
303
+ actions.openCherryPickConfirm();
292
304
  }
293
305
  });
294
306
  // Revert selected commit (history tab only)
@@ -296,7 +308,7 @@ export function setupKeyBindings(screen, actions, ctx) {
296
308
  if (ctx.hasActiveModal() || ctx.isCommitInputFocused())
297
309
  return;
298
310
  if (ctx.getBottomTab() === 'history') {
299
- actions.revertSelected();
311
+ actions.openRevertConfirm();
300
312
  }
301
313
  });
302
314
  }
@@ -0,0 +1,166 @@
1
+ import { ThemePicker } from './ui/modals/ThemePicker.js';
2
+ import { HotkeysModal } from './ui/modals/HotkeysModal.js';
3
+ import { BaseBranchPicker } from './ui/modals/BaseBranchPicker.js';
4
+ import { DiscardConfirm } from './ui/modals/DiscardConfirm.js';
5
+ import { FileFinder } from './ui/modals/FileFinder.js';
6
+ import { CommitActionConfirm } from './ui/modals/CommitActionConfirm.js';
7
+ import { RepoPicker } from './ui/modals/RepoPicker.js';
8
+ import { saveConfig } from './config.js';
9
+ import * as logger from './utils/logger.js';
10
+ /**
11
+ * Manages all modal dialogs: creation, focus, and dismissal.
12
+ * Single source of truth for modal state.
13
+ */
14
+ export class ModalController {
15
+ ctx;
16
+ activeModal = null;
17
+ activeModalType = null;
18
+ constructor(ctx) {
19
+ this.ctx = ctx;
20
+ }
21
+ hasActiveModal() {
22
+ return this.activeModal !== null;
23
+ }
24
+ getActiveModalType() {
25
+ return this.activeModalType;
26
+ }
27
+ closeActiveModal() {
28
+ if (this.activeModal) {
29
+ this.activeModal.destroy();
30
+ this.activeModal = null;
31
+ this.activeModalType = null;
32
+ this.ctx.render();
33
+ }
34
+ }
35
+ clearModal() {
36
+ this.activeModal = null;
37
+ this.activeModalType = null;
38
+ }
39
+ openThemePicker() {
40
+ this.activeModalType = 'theme';
41
+ this.activeModal = new ThemePicker(this.ctx.screen, this.ctx.getCurrentTheme(), (theme) => {
42
+ this.ctx.setCurrentTheme(theme);
43
+ saveConfig({ theme });
44
+ this.clearModal();
45
+ this.ctx.render();
46
+ }, () => {
47
+ this.clearModal();
48
+ });
49
+ this.activeModal.focus();
50
+ }
51
+ openHotkeysModal() {
52
+ this.activeModalType = 'hotkeys';
53
+ this.activeModal = new HotkeysModal(this.ctx.screen, () => {
54
+ this.clearModal();
55
+ });
56
+ this.activeModal.focus();
57
+ }
58
+ openBaseBranchPicker() {
59
+ const gm = this.ctx.getGitManager();
60
+ if (!gm)
61
+ return;
62
+ this.activeModalType = 'baseBranch';
63
+ gm.compare
64
+ .getCandidateBaseBranches()
65
+ .then((branches) => {
66
+ const currentBranch = gm.compare.compareState.compareBaseBranch ?? null;
67
+ const modal = new BaseBranchPicker(this.ctx.screen, branches, currentBranch, (branch) => {
68
+ this.clearModal();
69
+ const includeUncommitted = this.ctx.uiState.state.includeUncommitted;
70
+ gm.compare.setCompareBaseBranch(branch, includeUncommitted);
71
+ }, () => {
72
+ this.clearModal();
73
+ });
74
+ this.activeModal = modal;
75
+ modal.focus();
76
+ })
77
+ .catch((err) => {
78
+ this.clearModal();
79
+ logger.error('Failed to load base branches', err);
80
+ });
81
+ }
82
+ openDiscardConfirm(file) {
83
+ this.activeModalType = 'discard';
84
+ this.activeModal = new DiscardConfirm(this.ctx.screen, file.path, async () => {
85
+ this.clearModal();
86
+ await this.ctx.getGitManager()?.workingTree.discard(file);
87
+ }, () => {
88
+ this.clearModal();
89
+ });
90
+ this.activeModal.focus();
91
+ }
92
+ async openFileFinder() {
93
+ const explorer = this.ctx.getExplorerManager();
94
+ let allPaths = explorer?.getCachedFilePaths() ?? [];
95
+ if (allPaths.length === 0) {
96
+ await explorer?.loadFilePaths();
97
+ allPaths = explorer?.getCachedFilePaths() ?? [];
98
+ }
99
+ if (allPaths.length === 0)
100
+ return;
101
+ this.activeModalType = 'fileFinder';
102
+ this.activeModal = new FileFinder(this.ctx.screen, allPaths, async (selectedPath) => {
103
+ this.clearModal();
104
+ if (this.ctx.uiState.state.bottomTab !== 'explorer') {
105
+ this.ctx.uiState.setTab('explorer');
106
+ }
107
+ const success = await explorer?.navigateToPath(selectedPath);
108
+ if (success) {
109
+ const selectedIndex = explorer?.state.selectedIndex ?? 0;
110
+ this.ctx.uiState.setExplorerSelectedIndex(selectedIndex);
111
+ this.ctx.uiState.setExplorerFileScrollOffset(0);
112
+ const visibleHeight = this.ctx.getTopPaneHeight();
113
+ if (selectedIndex >= visibleHeight) {
114
+ this.ctx.uiState.setExplorerScrollOffset(selectedIndex - Math.floor(visibleHeight / 2));
115
+ }
116
+ else {
117
+ this.ctx.uiState.setExplorerScrollOffset(0);
118
+ }
119
+ }
120
+ this.ctx.render();
121
+ }, () => {
122
+ this.clearModal();
123
+ this.ctx.render();
124
+ });
125
+ this.activeModal.focus();
126
+ }
127
+ openCherryPickConfirm() {
128
+ const commit = this.ctx.getGitManager()?.history.historyState.selectedCommit;
129
+ if (!commit)
130
+ return;
131
+ this.activeModalType = 'commitAction';
132
+ this.activeModal = new CommitActionConfirm(this.ctx.screen, 'Cherry-pick', commit, () => {
133
+ this.clearModal();
134
+ this.ctx.getGitManager()?.remote.cherryPick(commit.hash);
135
+ }, () => {
136
+ this.clearModal();
137
+ });
138
+ this.activeModal.focus();
139
+ }
140
+ openRevertConfirm() {
141
+ const commit = this.ctx.getGitManager()?.history.historyState.selectedCommit;
142
+ if (!commit)
143
+ return;
144
+ this.activeModalType = 'commitAction';
145
+ this.activeModal = new CommitActionConfirm(this.ctx.screen, 'Revert', commit, () => {
146
+ this.clearModal();
147
+ this.ctx.getGitManager()?.remote.revertCommit(commit.hash);
148
+ }, () => {
149
+ this.clearModal();
150
+ });
151
+ this.activeModal.focus();
152
+ }
153
+ openRepoPicker() {
154
+ const repos = this.ctx.getRecentRepos();
155
+ const currentRepo = this.ctx.getRepoPath();
156
+ this.activeModalType = 'repoPicker';
157
+ this.activeModal = new RepoPicker(this.ctx.screen, repos, currentRepo, (selected) => {
158
+ this.clearModal();
159
+ this.ctx.onRepoSwitch(selected);
160
+ }, () => {
161
+ this.clearModal();
162
+ this.ctx.render();
163
+ });
164
+ this.activeModal.focus();
165
+ }
166
+ }
@@ -1,3 +1,4 @@
1
+ import { TAB_ZONES } from './state/UIState.js';
1
2
  import { getFileListTotalRows, getFileIndexFromRow } from './ui/widgets/FileList.js';
2
3
  import { getFlatFileListTotalRows } from './ui/widgets/FlatFileList.js';
3
4
  import { getCompareListTotalRows, getCompareSelectionFromRow, } from './ui/widgets/CompareListView.js';
@@ -22,7 +23,8 @@ export function setupMouseHandlers(layout, actions, ctx) {
22
23
  layout.bottomPane.on('wheelup', () => {
23
24
  handleBottomPaneScroll(-SCROLL_AMOUNT, layout, ctx);
24
25
  });
25
- // Click on top pane to select item
26
+ // Click on top pane to select item (does NOT change focus zone —
27
+ // preserves diff pane focus so hunk staging with 's' keeps working)
26
28
  layout.topPane.on('click', (mouse) => {
27
29
  const clickedRow = layout.screenYToTopPaneRow(mouse.y);
28
30
  if (clickedRow >= 0) {
@@ -34,15 +36,13 @@ export function setupMouseHandlers(layout, actions, ctx) {
34
36
  const clickedRow = layout.screenYToBottomPaneRow(mouse.y);
35
37
  if (clickedRow >= 0) {
36
38
  if (ctx.uiState.state.bottomTab === 'commit') {
37
- // Row 6 (0-indexed) is the amend checkbox row
38
- if (clickedRow === 6) {
39
- actions.toggleAmend();
40
- }
41
- else {
42
- actions.focusCommitInput();
43
- }
39
+ handleCommitPaneClick(clickedRow, actions, ctx);
44
40
  }
45
41
  else {
42
+ // Set focus to the bottom-pane zone for this tab
43
+ const zones = TAB_ZONES[ctx.uiState.state.bottomTab];
44
+ const bottomZone = zones[zones.length - 1];
45
+ ctx.uiState.setFocusedZone(bottomZone);
46
46
  actions.selectHunkAtRow(clickedRow);
47
47
  }
48
48
  }
@@ -54,33 +54,40 @@ export function setupMouseHandlers(layout, actions, ctx) {
54
54
  }
55
55
  function handleFileListClick(row, x, actions, ctx) {
56
56
  const state = ctx.uiState.state;
57
+ // Row-to-index mapping differs between flat and categorized mode
58
+ let fileIndex;
57
59
  if (state.flatViewMode) {
58
60
  // Flat mode: row 0 is header, files start at row 1
59
61
  const absoluteRow = row + state.fileListScrollOffset;
60
- const fileIndex = absoluteRow - 1; // subtract header row
62
+ const idx = absoluteRow - 1; // subtract header row
61
63
  const flatFiles = ctx.getCachedFlatFiles();
62
- if (fileIndex < 0 || fileIndex >= flatFiles.length)
63
- return;
64
- if (x !== undefined && x >= 2 && x <= 4) {
65
- actions.toggleFileByIndex(fileIndex);
66
- }
67
- else {
68
- ctx.uiState.setSelectedIndex(fileIndex);
69
- actions.selectFileByIndex(fileIndex);
70
- }
64
+ fileIndex = idx >= 0 && idx < flatFiles.length ? idx : null;
71
65
  }
72
66
  else {
73
- const files = ctx.getStatusFiles();
74
- const fileIndex = getFileIndexFromRow(row + state.fileListScrollOffset, files);
75
- if (fileIndex === null || fileIndex < 0)
76
- return;
77
- if (x !== undefined && x >= 2 && x <= 4) {
78
- actions.toggleFileByIndex(fileIndex);
79
- }
80
- else {
81
- ctx.uiState.setSelectedIndex(fileIndex);
82
- actions.selectFileByIndex(fileIndex);
83
- }
67
+ fileIndex = getFileIndexFromRow(row + state.fileListScrollOffset, ctx.getStatusFiles());
68
+ }
69
+ if (fileIndex === null || fileIndex < 0)
70
+ return;
71
+ if (x !== undefined && x >= 2 && x <= 4) {
72
+ actions.toggleFileByIndex(fileIndex);
73
+ }
74
+ else {
75
+ ctx.uiState.setSelectedIndex(fileIndex);
76
+ actions.selectFileByIndex(fileIndex);
77
+ }
78
+ }
79
+ function handleCommitPaneClick(row, actions, ctx) {
80
+ // Commit panel layout: rows 0-4 = title + message box, row 5 = blank, row 6 = amend
81
+ const absoluteRow = row + ctx.uiState.state.diffScrollOffset;
82
+ if (absoluteRow === 6) {
83
+ ctx.uiState.setFocusedZone('commitAmend');
84
+ }
85
+ else if (absoluteRow >= 2 && absoluteRow <= 4) {
86
+ ctx.uiState.setFocusedZone('commitMessage');
87
+ actions.focusCommitInput();
88
+ }
89
+ else {
90
+ ctx.uiState.setFocusedZone('commitMessage');
84
91
  }
85
92
  }
86
93
  function handleTopPaneClick(row, x, actions, ctx) {
@@ -151,7 +158,7 @@ function handleFooterClick(x, actions, ctx) {
151
158
  ctx.getExplorerManager()?.toggleShowOnlyChanges();
152
159
  }
153
160
  else if (x === 0) {
154
- ctx.uiState.openModal('hotkeys');
161
+ actions.openHotkeysModal();
155
162
  }
156
163
  }
157
164
  function handleTopPaneScroll(delta, layout, ctx) {