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.
Files changed (42) hide show
  1. package/.dependency-cruiser.cjs +67 -0
  2. package/.githooks/pre-commit +2 -0
  3. package/.githooks/pre-push +15 -0
  4. package/README.md +43 -35
  5. package/bun.lock +60 -4
  6. package/dist/App.js +495 -131
  7. package/dist/KeyBindings.js +134 -10
  8. package/dist/MouseHandlers.js +67 -20
  9. package/dist/core/ExplorerStateManager.js +37 -75
  10. package/dist/core/GitStateManager.js +252 -46
  11. package/dist/git/diff.js +99 -18
  12. package/dist/git/status.js +111 -54
  13. package/dist/git/test-helpers.js +67 -0
  14. package/dist/index.js +54 -43
  15. package/dist/ipc/CommandClient.js +6 -7
  16. package/dist/state/UIState.js +22 -0
  17. package/dist/types/remote.js +5 -0
  18. package/dist/ui/PaneRenderers.js +45 -15
  19. package/dist/ui/modals/BranchPicker.js +157 -0
  20. package/dist/ui/modals/CommitActionConfirm.js +66 -0
  21. package/dist/ui/modals/FileFinder.js +45 -75
  22. package/dist/ui/modals/HotkeysModal.js +35 -3
  23. package/dist/ui/modals/SoftResetConfirm.js +68 -0
  24. package/dist/ui/modals/StashListModal.js +98 -0
  25. package/dist/ui/modals/ThemePicker.js +1 -2
  26. package/dist/ui/widgets/CommitPanel.js +113 -7
  27. package/dist/ui/widgets/CompareListView.js +44 -23
  28. package/dist/ui/widgets/DiffView.js +216 -170
  29. package/dist/ui/widgets/ExplorerView.js +50 -54
  30. package/dist/ui/widgets/FileList.js +62 -95
  31. package/dist/ui/widgets/FlatFileList.js +65 -0
  32. package/dist/ui/widgets/Footer.js +25 -15
  33. package/dist/ui/widgets/Header.js +51 -9
  34. package/dist/ui/widgets/fileRowFormatters.js +73 -0
  35. package/dist/utils/ansiTruncate.js +0 -1
  36. package/dist/utils/displayRows.js +101 -21
  37. package/dist/utils/flatFileList.js +67 -0
  38. package/dist/utils/layoutCalculations.js +5 -3
  39. package/eslint.metrics.js +0 -1
  40. package/metrics/v0.2.2.json +229 -0
  41. package/metrics/v0.2.3.json +243 -0
  42. package/package.json +10 -3
@@ -1,5 +1,6 @@
1
1
  import { SPLIT_RATIO_STEP } from './ui/Layout.js';
2
2
  import { getFileAtIndex } from './ui/widgets/FileList.js';
3
+ import { getFlatFileAtIndex } from './utils/flatFileList.js';
3
4
  /**
4
5
  * Register all keyboard bindings on the blessed screen.
5
6
  */
@@ -41,10 +42,16 @@ export function setupKeyBindings(screen, actions, ctx) {
41
42
  ctx.uiState.togglePane();
42
43
  });
43
44
  // Staging operations (skip if modal is open)
45
+ // Context-aware: hunk staging when diff pane is focused on diff tab
44
46
  screen.key(['s'], () => {
45
47
  if (ctx.hasActiveModal())
46
48
  return;
47
- actions.stageSelected();
49
+ if (ctx.getBottomTab() === 'diff' && ctx.getCurrentPane() === 'diff') {
50
+ actions.toggleCurrentHunk();
51
+ }
52
+ else {
53
+ actions.stageSelected();
54
+ }
48
55
  });
49
56
  screen.key(['S-u'], () => {
50
57
  if (ctx.hasActiveModal())
@@ -85,7 +92,7 @@ export function setupKeyBindings(screen, actions, ctx) {
85
92
  if (ctx.hasActiveModal())
86
93
  return;
87
94
  if (ctx.getBottomTab() === 'explorer') {
88
- ctx.explorerManager?.toggleShowOnlyChanges();
95
+ ctx.getExplorerManager()?.toggleShowOnlyChanges();
89
96
  }
90
97
  });
91
98
  // Explorer: open file finder
@@ -96,6 +103,12 @@ export function setupKeyBindings(screen, actions, ctx) {
96
103
  actions.openFileFinder();
97
104
  }
98
105
  });
106
+ // Ctrl+P: open file finder from any tab
107
+ screen.key(['C-p'], () => {
108
+ if (ctx.hasActiveModal())
109
+ return;
110
+ actions.openFileFinder();
111
+ });
99
112
  // Commit (skip if modal is open)
100
113
  screen.key(['c'], () => {
101
114
  if (ctx.hasActiveModal())
@@ -117,6 +130,29 @@ export function setupKeyBindings(screen, actions, ctx) {
117
130
  ctx.uiState.toggleAutoTab();
118
131
  }
119
132
  });
133
+ // Ctrl+a: toggle amend on commit tab (works even when input is focused)
134
+ screen.key(['C-a'], () => {
135
+ if (ctx.getBottomTab() === 'commit') {
136
+ ctx.commitFlowState.toggleAmend();
137
+ actions.render();
138
+ }
139
+ });
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
+ });
120
156
  screen.key(['escape'], () => {
121
157
  if (ctx.getBottomTab() === 'commit') {
122
158
  if (ctx.isCommitInputFocused()) {
@@ -150,29 +186,117 @@ export function setupKeyBindings(screen, actions, ctx) {
150
186
  screen.key(['?'], () => ctx.uiState.toggleModal('hotkeys'));
151
187
  // Follow toggle
152
188
  screen.key(['f'], () => actions.toggleFollow());
153
- // Compare view: base branch picker
189
+ // Compare view: base branch picker / Commit tab: branch picker
154
190
  screen.key(['b'], () => {
191
+ if (ctx.hasActiveModal() || ctx.isCommitInputFocused())
192
+ return;
155
193
  if (ctx.getBottomTab() === 'compare') {
156
194
  ctx.uiState.openModal('baseBranch');
157
195
  }
196
+ else if (ctx.getBottomTab() === 'commit') {
197
+ actions.openBranchPicker();
198
+ }
158
199
  });
159
- // Compare view: toggle uncommitted
200
+ // u: toggle uncommitted in compare view
160
201
  screen.key(['u'], () => {
202
+ if (ctx.hasActiveModal())
203
+ return;
161
204
  if (ctx.getBottomTab() === 'compare') {
162
205
  ctx.uiState.toggleIncludeUncommitted();
163
206
  const includeUncommitted = ctx.uiState.state.includeUncommitted;
164
- ctx.gitManager?.refreshCompareDiff(includeUncommitted);
207
+ ctx.getGitManager()?.refreshCompareDiff(includeUncommitted);
208
+ }
209
+ });
210
+ // Toggle flat file view (diff/commit tab only)
211
+ screen.key(['h'], () => {
212
+ if (ctx.hasActiveModal())
213
+ return;
214
+ const tab = ctx.getBottomTab();
215
+ if (tab === 'diff' || tab === 'commit') {
216
+ ctx.uiState.toggleFlatViewMode();
165
217
  }
166
218
  });
167
219
  // Discard changes (with confirmation)
168
220
  screen.key(['d'], () => {
169
221
  if (ctx.getBottomTab() === 'diff') {
170
- const files = ctx.getStatusFiles();
171
- const selectedFile = getFileAtIndex(files, ctx.getSelectedIndex());
172
- // Only allow discard for unstaged modified files
173
- if (selectedFile && !selectedFile.staged && selectedFile.status !== 'untracked') {
174
- actions.showDiscardConfirm(selectedFile);
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
+ }
175
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
+ }
238
+ }
239
+ }
240
+ });
241
+ // Hunk navigation (only when diff pane focused on diff tab)
242
+ screen.key(['n'], () => {
243
+ if (ctx.hasActiveModal())
244
+ return;
245
+ if (ctx.getBottomTab() === 'diff' && ctx.getCurrentPane() === 'diff') {
246
+ actions.navigateNextHunk();
247
+ }
248
+ });
249
+ screen.key(['S-n'], () => {
250
+ if (ctx.hasActiveModal())
251
+ return;
252
+ if (ctx.getBottomTab() === 'diff' && ctx.getCurrentPane() === 'diff') {
253
+ actions.navigatePrevHunk();
254
+ }
255
+ });
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
+ // Cherry-pick selected commit (history tab only)
287
+ screen.key(['p'], () => {
288
+ if (ctx.hasActiveModal() || ctx.isCommitInputFocused())
289
+ return;
290
+ if (ctx.getBottomTab() === 'history') {
291
+ actions.cherryPickSelected();
292
+ }
293
+ });
294
+ // Revert selected commit (history tab only)
295
+ screen.key(['v'], () => {
296
+ if (ctx.hasActiveModal() || ctx.isCommitInputFocused())
297
+ return;
298
+ if (ctx.getBottomTab() === 'history') {
299
+ actions.revertSelected();
176
300
  }
177
301
  });
178
302
  }
@@ -1,4 +1,5 @@
1
1
  import { getFileListTotalRows, getFileIndexFromRow } from './ui/widgets/FileList.js';
2
+ import { getFlatFileListTotalRows } from './ui/widgets/FlatFileList.js';
2
3
  import { getCompareListTotalRows, getCompareSelectionFromRow, } from './ui/widgets/CompareListView.js';
3
4
  import { getExplorerTotalRows } from './ui/widgets/ExplorerView.js';
4
5
  import { getExplorerContentTotalRows } from './ui/widgets/ExplorerContent.js';
@@ -28,11 +29,60 @@ export function setupMouseHandlers(layout, actions, ctx) {
28
29
  handleTopPaneClick(clickedRow, mouse.x, actions, ctx);
29
30
  }
30
31
  });
32
+ // Click on bottom pane
33
+ layout.bottomPane.on('click', (mouse) => {
34
+ const clickedRow = layout.screenYToBottomPaneRow(mouse.y);
35
+ if (clickedRow >= 0) {
36
+ 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
+ }
44
+ }
45
+ else {
46
+ actions.selectHunkAtRow(clickedRow);
47
+ }
48
+ }
49
+ });
31
50
  // Click on footer for tabs and toggles
32
51
  layout.footerBox.on('click', (mouse) => {
33
52
  handleFooterClick(mouse.x, actions, ctx);
34
53
  });
35
54
  }
55
+ function handleFileListClick(row, x, actions, ctx) {
56
+ const state = ctx.uiState.state;
57
+ if (state.flatViewMode) {
58
+ // Flat mode: row 0 is header, files start at row 1
59
+ const absoluteRow = row + state.fileListScrollOffset;
60
+ const fileIndex = absoluteRow - 1; // subtract header row
61
+ 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
+ }
71
+ }
72
+ 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
+ }
84
+ }
85
+ }
36
86
  function handleTopPaneClick(row, x, actions, ctx) {
37
87
  const state = ctx.uiState.state;
38
88
  if (state.bottomTab === 'history') {
@@ -50,23 +100,19 @@ function handleTopPaneClick(row, x, actions, ctx) {
50
100
  }
51
101
  else if (state.bottomTab === 'explorer') {
52
102
  const index = state.explorerScrollOffset + row;
53
- ctx.explorerManager?.selectIndex(index);
54
- ctx.uiState.setExplorerSelectedIndex(index);
103
+ const explorerManager = ctx.getExplorerManager();
104
+ const isAlreadySelected = explorerManager?.state.selectedIndex === index;
105
+ const displayRow = explorerManager?.state.displayRows[index];
106
+ if (isAlreadySelected && displayRow?.node.isDirectory) {
107
+ actions.enterExplorerDirectory();
108
+ }
109
+ else {
110
+ explorerManager?.selectIndex(index);
111
+ ctx.uiState.setExplorerSelectedIndex(index);
112
+ }
55
113
  }
56
114
  else {
57
- // Diff tab - select file
58
- const files = ctx.getStatusFiles();
59
- const fileIndex = getFileIndexFromRow(row + state.fileListScrollOffset, files);
60
- if (fileIndex !== null && fileIndex >= 0) {
61
- // Check if click is on the action button [+] or [-] (columns 2-4)
62
- if (x !== undefined && x >= 2 && x <= 4) {
63
- actions.toggleFileByIndex(fileIndex);
64
- }
65
- else {
66
- ctx.uiState.setSelectedIndex(fileIndex);
67
- actions.selectFileByIndex(fileIndex);
68
- }
69
- }
115
+ handleFileListClick(row, x, actions, ctx);
70
116
  }
71
117
  }
72
118
  function handleFooterClick(x, actions, ctx) {
@@ -102,7 +148,7 @@ function handleFooterClick(x, actions, ctx) {
102
148
  actions.toggleFollow();
103
149
  }
104
150
  else if (x >= 34 && x <= 43 && ctx.uiState.state.bottomTab === 'explorer') {
105
- ctx.explorerManager?.toggleShowOnlyChanges();
151
+ ctx.getExplorerManager()?.toggleShowOnlyChanges();
106
152
  }
107
153
  else if (x === 0) {
108
154
  ctx.uiState.openModal('hotkeys');
@@ -124,14 +170,15 @@ function handleTopPaneScroll(delta, layout, ctx) {
124
170
  ctx.uiState.setCompareScrollOffset(newOffset);
125
171
  }
126
172
  else if (state.bottomTab === 'explorer') {
127
- const totalRows = getExplorerTotalRows(ctx.explorerManager?.state.displayRows ?? []);
173
+ const totalRows = getExplorerTotalRows(ctx.getExplorerManager()?.state.displayRows ?? []);
128
174
  const maxOffset = Math.max(0, totalRows - visibleHeight);
129
175
  const newOffset = Math.min(maxOffset, Math.max(0, state.explorerScrollOffset + delta));
130
176
  ctx.uiState.setExplorerScrollOffset(newOffset);
131
177
  }
132
178
  else {
133
- const files = ctx.getStatusFiles();
134
- const totalRows = getFileListTotalRows(files);
179
+ const totalRows = state.flatViewMode
180
+ ? getFlatFileListTotalRows(ctx.getCachedFlatFiles())
181
+ : getFileListTotalRows(ctx.getStatusFiles());
135
182
  const maxOffset = Math.max(0, totalRows - visibleHeight);
136
183
  const newOffset = Math.min(maxOffset, Math.max(0, state.fileListScrollOffset + delta));
137
184
  ctx.uiState.setFileListScrollOffset(newOffset);
@@ -142,7 +189,7 @@ function handleBottomPaneScroll(delta, layout, ctx) {
142
189
  const visibleHeight = layout.dimensions.bottomPaneHeight;
143
190
  const width = ctx.getScreenWidth();
144
191
  if (state.bottomTab === 'explorer') {
145
- const selectedFile = ctx.explorerManager?.state.selectedFile;
192
+ const selectedFile = ctx.getExplorerManager()?.state.selectedFile;
146
193
  const totalRows = getExplorerContentTotalRows(selectedFile?.content ?? null, selectedFile?.path ?? null, selectedFile?.truncated ?? false, width, state.wrapMode);
147
194
  const maxOffset = Math.max(0, totalRows - visibleHeight);
148
195
  const newOffset = Math.min(maxOffset, Math.max(0, state.explorerFileScrollOffset + delta));
@@ -2,6 +2,7 @@ import * as fs from 'node:fs';
2
2
  import * as path from 'node:path';
3
3
  import { EventEmitter } from 'node:events';
4
4
  import { getIgnoredFiles } from '../git/ignoreUtils.js';
5
+ import { listAllFiles } from '../git/status.js';
5
6
  const MAX_FILE_SIZE = 1024 * 1024; // 1MB
6
7
  const WARN_FILE_SIZE = 100 * 1024; // 100KB
7
8
  /**
@@ -25,6 +26,7 @@ export class ExplorerStateManager extends EventEmitter {
25
26
  options;
26
27
  expandedPaths = new Set();
27
28
  gitStatusMap = { files: new Map(), directories: new Set() };
29
+ _cachedFilePaths = null;
28
30
  _state = {
29
31
  currentPath: '',
30
32
  tree: null,
@@ -61,9 +63,12 @@ export class ExplorerStateManager extends EventEmitter {
61
63
  }
62
64
  /**
63
65
  * Update git status map and refresh display.
66
+ * Also invalidates the file path cache so the next file finder open gets fresh data.
64
67
  */
65
68
  setGitStatus(statusMap) {
66
69
  this.gitStatusMap = statusMap;
70
+ // Invalidate file path cache — reload in background
71
+ this.loadFilePaths();
67
72
  // Refresh display to show updated status
68
73
  if (this._state.tree) {
69
74
  this.applyGitStatusToTree(this._state.tree);
@@ -123,7 +128,7 @@ export class ExplorerStateManager extends EventEmitter {
123
128
  /**
124
129
  * Build a tree node for a directory path.
125
130
  */
126
- async buildTreeNode(relativePath, depth) {
131
+ async buildTreeNode(relativePath, _depth) {
127
132
  try {
128
133
  const fullPath = path.join(this.repoPath, relativePath);
129
134
  const stats = await fs.promises.stat(fullPath);
@@ -153,7 +158,7 @@ export class ExplorerStateManager extends EventEmitter {
153
158
  }
154
159
  return node;
155
160
  }
156
- catch (err) {
161
+ catch {
157
162
  return null;
158
163
  }
159
164
  }
@@ -211,7 +216,7 @@ export class ExplorerStateManager extends EventEmitter {
211
216
  this.collapseNode(node, children);
212
217
  node.childrenLoaded = true;
213
218
  }
214
- catch (err) {
219
+ catch {
215
220
  node.childrenLoaded = true;
216
221
  node.children = [];
217
222
  }
@@ -262,43 +267,21 @@ export class ExplorerStateManager extends EventEmitter {
262
267
  /**
263
268
  * Flatten tree into display rows.
264
269
  */
270
+ shouldIncludeNode(node) {
271
+ if (!this.options.showOnlyChanges)
272
+ return true;
273
+ if (node.isDirectory)
274
+ return !!node.hasChangedChildren;
275
+ return !!node.gitStatus;
276
+ }
265
277
  flattenTree(root) {
266
278
  const rows = [];
267
- const traverse = (node, depth, parentIsLast) => {
268
- // Skip root node in display (but process its children)
269
- if (depth === 0) {
270
- for (let i = 0; i < node.children.length; i++) {
271
- const child = node.children[i];
272
- const isLast = i === node.children.length - 1;
273
- // Apply filter if showOnlyChanges is enabled
274
- if (this.options.showOnlyChanges) {
275
- if (child.isDirectory && !child.hasChangedChildren)
276
- continue;
277
- if (!child.isDirectory && !child.gitStatus)
278
- continue;
279
- }
280
- rows.push({
281
- node: child,
282
- depth: 0,
283
- isLast,
284
- parentIsLast: [],
285
- });
286
- if (child.isDirectory && child.expanded) {
287
- traverse(child, 1, [isLast]);
288
- }
289
- }
290
- return;
291
- }
279
+ const traverseChildren = (node, depth, parentIsLast) => {
292
280
  for (let i = 0; i < node.children.length; i++) {
293
281
  const child = node.children[i];
294
282
  const isLast = i === node.children.length - 1;
295
- // Apply filter if showOnlyChanges is enabled
296
- if (this.options.showOnlyChanges) {
297
- if (child.isDirectory && !child.hasChangedChildren)
298
- continue;
299
- if (!child.isDirectory && !child.gitStatus)
300
- continue;
301
- }
283
+ if (!this.shouldIncludeNode(child))
284
+ continue;
302
285
  rows.push({
303
286
  node: child,
304
287
  depth,
@@ -306,11 +289,12 @@ export class ExplorerStateManager extends EventEmitter {
306
289
  parentIsLast: [...parentIsLast],
307
290
  });
308
291
  if (child.isDirectory && child.expanded) {
309
- traverse(child, depth + 1, [...parentIsLast, isLast]);
292
+ traverseChildren(child, depth + 1, [...parentIsLast, isLast]);
310
293
  }
311
294
  }
312
295
  };
313
- traverse(root, 0, []);
296
+ // Start from root's children at depth 0 (root itself is not displayed)
297
+ traverseChildren(root, 0, []);
314
298
  return rows;
315
299
  }
316
300
  /**
@@ -559,45 +543,23 @@ export class ExplorerStateManager extends EventEmitter {
559
543
  return search(this._state.tree);
560
544
  }
561
545
  /**
562
- * Get all file paths in the repo (for file finder).
563
- * Scans the filesystem directly to get all files, not just expanded ones.
546
+ * Load all file paths using git ls-files (fast, single git command).
547
+ * Stores result in cache for instant access by FileFinder.
564
548
  */
565
- async getAllFilePaths() {
566
- const paths = [];
567
- const scanDir = async (dirPath) => {
568
- try {
569
- const fullPath = path.join(this.repoPath, dirPath);
570
- const entries = await fs.promises.readdir(fullPath, { withFileTypes: true });
571
- // Build list of paths for gitignore check
572
- const pathsToCheck = entries.map((e) => (dirPath ? path.join(dirPath, e.name) : e.name));
573
- // Get ignored files
574
- const ignoredFiles = this.options.hideGitignored
575
- ? await getIgnoredFiles(this.repoPath, pathsToCheck)
576
- : new Set();
577
- for (const entry of entries) {
578
- // Filter dot-prefixed hidden files
579
- if (this.options.hideHidden && entry.name.startsWith('.')) {
580
- continue;
581
- }
582
- const entryPath = dirPath ? path.join(dirPath, entry.name) : entry.name;
583
- // Filter gitignored files
584
- if (this.options.hideGitignored && ignoredFiles.has(entryPath)) {
585
- continue;
586
- }
587
- if (entry.isDirectory()) {
588
- await scanDir(entryPath);
589
- }
590
- else {
591
- paths.push(entryPath);
592
- }
593
- }
594
- }
595
- catch (err) {
596
- // Ignore errors for individual directories
597
- }
598
- };
599
- await scanDir('');
600
- return paths;
549
+ async loadFilePaths() {
550
+ try {
551
+ this._cachedFilePaths = await listAllFiles(this.repoPath);
552
+ }
553
+ catch {
554
+ this._cachedFilePaths = [];
555
+ }
556
+ }
557
+ /**
558
+ * Get cached file paths (for file finder).
559
+ * Returns empty array if not yet loaded.
560
+ */
561
+ getCachedFilePaths() {
562
+ return this._cachedFilePaths ?? [];
601
563
  }
602
564
  /**
603
565
  * Navigate to a specific file path in the tree.