dmux 5.4.0 → 5.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (203) hide show
  1. package/README.md +1 -0
  2. package/dist/DmuxApp.d.ts.map +1 -1
  3. package/dist/DmuxApp.js +225 -51
  4. package/dist/DmuxApp.js.map +1 -1
  5. package/dist/FileBrowserApp.d.ts +4 -0
  6. package/dist/FileBrowserApp.d.ts.map +1 -0
  7. package/dist/FileBrowserApp.js +693 -0
  8. package/dist/FileBrowserApp.js.map +1 -0
  9. package/dist/actions/implementations/closeAction.js +2 -2
  10. package/dist/actions/implementations/closeAction.js.map +1 -1
  11. package/dist/actions/implementations/mergeAction.d.ts.map +1 -1
  12. package/dist/actions/implementations/mergeAction.js +51 -16
  13. package/dist/actions/implementations/mergeAction.js.map +1 -1
  14. package/dist/actions/implementations/viewAction.d.ts.map +1 -1
  15. package/dist/actions/implementations/viewAction.js +7 -0
  16. package/dist/actions/implementations/viewAction.js.map +1 -1
  17. package/dist/actions/index.d.ts.map +1 -1
  18. package/dist/actions/index.js +12 -0
  19. package/dist/actions/index.js.map +1 -1
  20. package/dist/actions/merge/multiMergeOrchestrator.js +1 -1
  21. package/dist/actions/merge/multiMergeOrchestrator.js.map +1 -1
  22. package/dist/actions/types.d.ts +18 -4
  23. package/dist/actions/types.d.ts.map +1 -1
  24. package/dist/actions/types.js +82 -0
  25. package/dist/actions/types.js.map +1 -1
  26. package/dist/components/indicators/Spinner.d.ts +2 -0
  27. package/dist/components/indicators/Spinner.d.ts.map +1 -1
  28. package/dist/components/indicators/Spinner.js +4 -4
  29. package/dist/components/indicators/Spinner.js.map +1 -1
  30. package/dist/components/panes/KebabMenu.d.ts +2 -2
  31. package/dist/components/panes/KebabMenu.d.ts.map +1 -1
  32. package/dist/components/panes/KebabMenu.js +9 -4
  33. package/dist/components/panes/KebabMenu.js.map +1 -1
  34. package/dist/components/panes/PaneCard.d.ts.map +1 -1
  35. package/dist/components/panes/PaneCard.js +20 -4
  36. package/dist/components/panes/PaneCard.js.map +1 -1
  37. package/dist/components/panes/PanesGrid.d.ts +1 -0
  38. package/dist/components/panes/PanesGrid.d.ts.map +1 -1
  39. package/dist/components/panes/PanesGrid.js +11 -3
  40. package/dist/components/panes/PanesGrid.js.map +1 -1
  41. package/dist/components/popups/kebabMenuPopup.js +9 -4
  42. package/dist/components/popups/kebabMenuPopup.js.map +1 -1
  43. package/dist/components/popups/notificationSoundsPopup.d.ts +25 -0
  44. package/dist/components/popups/notificationSoundsPopup.d.ts.map +1 -0
  45. package/dist/components/popups/notificationSoundsPopup.js +165 -0
  46. package/dist/components/popups/notificationSoundsPopup.js.map +1 -0
  47. package/dist/components/popups/settingsPopup.js +15 -1
  48. package/dist/components/popups/settingsPopup.js.map +1 -1
  49. package/dist/components/popups/shortcutsPopup.js +8 -3
  50. package/dist/components/popups/shortcutsPopup.js.map +1 -1
  51. package/dist/hooks/useActionSystem.d.ts +9 -5
  52. package/dist/hooks/useActionSystem.d.ts.map +1 -1
  53. package/dist/hooks/useActionSystem.js +21 -19
  54. package/dist/hooks/useActionSystem.js.map +1 -1
  55. package/dist/hooks/useInputHandling.d.ts +1 -0
  56. package/dist/hooks/useInputHandling.d.ts.map +1 -1
  57. package/dist/hooks/useInputHandling.js +300 -15
  58. package/dist/hooks/useInputHandling.js.map +1 -1
  59. package/dist/hooks/usePaneCreation.d.ts +4 -2
  60. package/dist/hooks/usePaneCreation.d.ts.map +1 -1
  61. package/dist/hooks/usePaneCreation.js +6 -0
  62. package/dist/hooks/usePaneCreation.js.map +1 -1
  63. package/dist/hooks/usePaneLoading.d.ts +1 -0
  64. package/dist/hooks/usePaneLoading.d.ts.map +1 -1
  65. package/dist/hooks/usePaneLoading.js +10 -7
  66. package/dist/hooks/usePaneLoading.js.map +1 -1
  67. package/dist/hooks/usePaneSync.js +2 -2
  68. package/dist/hooks/usePaneSync.js.map +1 -1
  69. package/dist/hooks/useProjectActivity.d.ts +7 -0
  70. package/dist/hooks/useProjectActivity.d.ts.map +1 -0
  71. package/dist/hooks/useProjectActivity.js +79 -0
  72. package/dist/hooks/useProjectActivity.js.map +1 -0
  73. package/dist/hooks/useServices.d.ts +2 -0
  74. package/dist/hooks/useServices.d.ts.map +1 -1
  75. package/dist/hooks/useServices.js +2 -0
  76. package/dist/hooks/useServices.js.map +1 -1
  77. package/dist/index.js +53 -12
  78. package/dist/index.js.map +1 -1
  79. package/dist/services/DmuxAttentionService.d.ts +33 -0
  80. package/dist/services/DmuxAttentionService.d.ts.map +1 -0
  81. package/dist/services/DmuxAttentionService.js +172 -0
  82. package/dist/services/DmuxAttentionService.js.map +1 -0
  83. package/dist/services/DmuxFocusService.d.ts +58 -0
  84. package/dist/services/DmuxFocusService.d.ts.map +1 -0
  85. package/dist/services/DmuxFocusService.js +671 -0
  86. package/dist/services/DmuxFocusService.js.map +1 -0
  87. package/dist/services/PaneAnalyzer.d.ts +11 -2
  88. package/dist/services/PaneAnalyzer.d.ts.map +1 -1
  89. package/dist/services/PaneAnalyzer.js +88 -22
  90. package/dist/services/PaneAnalyzer.js.map +1 -1
  91. package/dist/services/PopupManager.d.ts +24 -14
  92. package/dist/services/PopupManager.d.ts.map +1 -1
  93. package/dist/services/PopupManager.js +109 -62
  94. package/dist/services/PopupManager.js.map +1 -1
  95. package/dist/services/StatusDetector.d.ts +14 -0
  96. package/dist/services/StatusDetector.d.ts.map +1 -1
  97. package/dist/services/StatusDetector.js +60 -12
  98. package/dist/services/StatusDetector.js.map +1 -1
  99. package/dist/services/TmuxService.d.ts +37 -2
  100. package/dist/services/TmuxService.d.ts.map +1 -1
  101. package/dist/services/TmuxService.js +138 -16
  102. package/dist/services/TmuxService.js.map +1 -1
  103. package/dist/types/activity.d.ts +4 -0
  104. package/dist/types/activity.d.ts.map +1 -0
  105. package/dist/types/activity.js +2 -0
  106. package/dist/types/activity.js.map +1 -0
  107. package/dist/types.d.ts +11 -0
  108. package/dist/types.d.ts.map +1 -1
  109. package/dist/utils/devWatchExit.d.ts +2 -0
  110. package/dist/utils/devWatchExit.d.ts.map +1 -0
  111. package/dist/utils/devWatchExit.js +10 -0
  112. package/dist/utils/devWatchExit.js.map +1 -0
  113. package/dist/utils/dmuxCommand.d.ts +3 -0
  114. package/dist/utils/dmuxCommand.d.ts.map +1 -0
  115. package/dist/utils/dmuxCommand.js +18 -0
  116. package/dist/utils/dmuxCommand.js.map +1 -0
  117. package/dist/utils/fileBrowser.d.ts +61 -0
  118. package/dist/utils/fileBrowser.d.ts.map +1 -0
  119. package/dist/utils/fileBrowser.js +567 -0
  120. package/dist/utils/fileBrowser.js.map +1 -0
  121. package/dist/utils/focusDetection.d.ts +38 -0
  122. package/dist/utils/focusDetection.d.ts.map +1 -0
  123. package/dist/utils/focusDetection.js +57 -0
  124. package/dist/utils/focusDetection.js.map +1 -0
  125. package/dist/utils/generated-agents-doc.d.ts +1 -1
  126. package/dist/utils/generated-agents-doc.js +1 -1
  127. package/dist/utils/git.d.ts +4 -0
  128. package/dist/utils/git.d.ts.map +1 -1
  129. package/dist/utils/git.js +15 -0
  130. package/dist/utils/git.js.map +1 -1
  131. package/dist/utils/mergeTargets.d.ts +17 -0
  132. package/dist/utils/mergeTargets.d.ts.map +1 -0
  133. package/dist/utils/mergeTargets.js +132 -0
  134. package/dist/utils/mergeTargets.js.map +1 -0
  135. package/dist/utils/mergeValidation.d.ts.map +1 -1
  136. package/dist/utils/mergeValidation.js +12 -5
  137. package/dist/utils/mergeValidation.js.map +1 -1
  138. package/dist/utils/notificationSoundPreview.d.ts +10 -0
  139. package/dist/utils/notificationSoundPreview.d.ts.map +1 -0
  140. package/dist/utils/notificationSoundPreview.js +54 -0
  141. package/dist/utils/notificationSoundPreview.js.map +1 -0
  142. package/dist/utils/notificationSounds.d.ts +17 -0
  143. package/dist/utils/notificationSounds.d.ts.map +1 -0
  144. package/dist/utils/notificationSounds.js +123 -0
  145. package/dist/utils/notificationSounds.js.map +1 -0
  146. package/dist/utils/paneAttentionHeuristics.d.ts +4 -0
  147. package/dist/utils/paneAttentionHeuristics.d.ts.map +1 -0
  148. package/dist/utils/paneAttentionHeuristics.js +135 -0
  149. package/dist/utils/paneAttentionHeuristics.js.map +1 -0
  150. package/dist/utils/paneCreation.d.ts +3 -1
  151. package/dist/utils/paneCreation.d.ts.map +1 -1
  152. package/dist/utils/paneCreation.js +23 -5
  153. package/dist/utils/paneCreation.js.map +1 -1
  154. package/dist/utils/paneVisibility.d.ts +12 -0
  155. package/dist/utils/paneVisibility.d.ts.map +1 -0
  156. package/dist/utils/paneVisibility.js +60 -0
  157. package/dist/utils/paneVisibility.js.map +1 -0
  158. package/dist/utils/processShutdown.d.ts +4 -0
  159. package/dist/utils/processShutdown.d.ts.map +1 -0
  160. package/dist/utils/processShutdown.js +27 -0
  161. package/dist/utils/processShutdown.js.map +1 -0
  162. package/dist/utils/promptStore.d.ts.map +1 -1
  163. package/dist/utils/promptStore.js +6 -0
  164. package/dist/utils/promptStore.js.map +1 -1
  165. package/dist/utils/reopenWorktree.d.ts.map +1 -1
  166. package/dist/utils/reopenWorktree.js +8 -0
  167. package/dist/utils/reopenWorktree.js.map +1 -1
  168. package/dist/utils/runtimePaths.d.ts +1 -0
  169. package/dist/utils/runtimePaths.d.ts.map +1 -1
  170. package/dist/utils/runtimePaths.js +3 -0
  171. package/dist/utils/runtimePaths.js.map +1 -1
  172. package/dist/utils/settingsManager.d.ts.map +1 -1
  173. package/dist/utils/settingsManager.js +41 -7
  174. package/dist/utils/settingsManager.js.map +1 -1
  175. package/dist/utils/tmuxConfigOnboarding.js +1 -1
  176. package/dist/utils/tmuxConfigOnboarding.js.map +1 -1
  177. package/dist/utils/tmuxRuntimeCompatibility.d.ts +11 -0
  178. package/dist/utils/tmuxRuntimeCompatibility.d.ts.map +1 -0
  179. package/dist/utils/tmuxRuntimeCompatibility.js +71 -0
  180. package/dist/utils/tmuxRuntimeCompatibility.js.map +1 -0
  181. package/dist/utils/worktreeMetadata.d.ts +9 -0
  182. package/dist/utils/worktreeMetadata.d.ts.map +1 -0
  183. package/dist/utils/worktreeMetadata.js +60 -0
  184. package/dist/utils/worktreeMetadata.js.map +1 -0
  185. package/dist/workers/PaneWorker.js +64 -128
  186. package/dist/workers/PaneWorker.js.map +1 -1
  187. package/dist/workers/WorkerMessages.d.ts +4 -1
  188. package/dist/workers/WorkerMessages.d.ts.map +1 -1
  189. package/dist/workers/WorkerMessages.js.map +1 -1
  190. package/native/macos/dmux-helper-Info.plist +30 -0
  191. package/native/macos/dmux-helper-icon.png +0 -0
  192. package/native/macos/dmux-helper.swift +831 -0
  193. package/native/macos/sounds/dmux-braam.caf +0 -0
  194. package/native/macos/sounds/dmux-brass.caf +0 -0
  195. package/native/macos/sounds/dmux-ding-bell.caf +0 -0
  196. package/native/macos/sounds/dmux-future.caf +0 -0
  197. package/native/macos/sounds/dmux-harp.caf +0 -0
  198. package/native/macos/sounds/dmux-quiet-bells.caf +0 -0
  199. package/native/macos/sounds/dmux-sonar.caf +0 -0
  200. package/native/macos/sounds/dmux-success.caf +0 -0
  201. package/native/macos/sounds/dmux-triumphant-trumpet.caf +0 -0
  202. package/native/macos/sounds/dmux-war-horn.caf +0 -0
  203. package/package.json +3 -1
@@ -0,0 +1,693 @@
1
+ import React, { useEffect, useMemo, useState } from 'react';
2
+ import { Box, Text, useInput, useStdout } from 'ink';
3
+ import stringWidth from 'string-width';
4
+ import path from 'path';
5
+ import { spawn } from 'child_process';
6
+ import { buildBrowserSearchEntries, buildBrowserTree, computeModifiedTimes, flattenBrowserTree, getAncestorPaths, getCurrentDirectoryPath, getStatusColor, loadBrowserSnapshot, loadCodePreview, loadDiffPreview, } from './utils/fileBrowser.js';
7
+ import { POPUP_CONFIG } from './components/popups/config.js';
8
+ import { COLORS } from './theme/colors.js';
9
+ const SORT_OPTIONS = [
10
+ { id: 'sort-name', label: 'Sort by name', description: 'Alphabetical tree order' },
11
+ { id: 'sort-modified', label: 'Sort by modified time', description: 'Recently touched files first' },
12
+ { id: 'sort-status', label: 'Sort by git status', description: 'Changed files first' },
13
+ { id: 'filter-all', label: 'Show all files', description: 'Tracked and untracked files' },
14
+ { id: 'filter-diffed', label: 'Show changed files only', description: 'Only files with git changes' },
15
+ ];
16
+ function clipToWidth(value, maxWidth) {
17
+ if (maxWidth <= 0 || stringWidth(value) <= maxWidth) {
18
+ return maxWidth <= 0 ? '' : value;
19
+ }
20
+ let clipped = '';
21
+ let width = 0;
22
+ for (const char of value) {
23
+ const charWidth = stringWidth(char);
24
+ if (width + charWidth > Math.max(1, maxWidth - 1)) {
25
+ break;
26
+ }
27
+ clipped += char;
28
+ width += charWidth;
29
+ }
30
+ return `${clipped}…`;
31
+ }
32
+ function clipFromLeft(value, maxWidth) {
33
+ if (maxWidth <= 0 || stringWidth(value) <= maxWidth) {
34
+ return maxWidth <= 0 ? '' : value;
35
+ }
36
+ const ellipsis = '…';
37
+ let clipped = '';
38
+ for (const char of Array.from(value).reverse()) {
39
+ if (stringWidth(`${ellipsis}${clipped}`) + stringWidth(char) > maxWidth) {
40
+ break;
41
+ }
42
+ clipped = `${char}${clipped}`;
43
+ }
44
+ return `${ellipsis}${clipped}`;
45
+ }
46
+ function clipPathToWidth(value, maxWidth) {
47
+ if (maxWidth <= 0 || stringWidth(value) <= maxWidth) {
48
+ return maxWidth <= 0 ? '' : value;
49
+ }
50
+ const segments = value.split('/');
51
+ let visibleTail = segments.pop() || value;
52
+ while (segments.length > 0) {
53
+ const candidate = `${segments[segments.length - 1]}/${visibleTail}`;
54
+ if (stringWidth(`…/${candidate}`) > maxWidth) {
55
+ break;
56
+ }
57
+ visibleTail = candidate;
58
+ segments.pop();
59
+ }
60
+ const prefixed = `…/${visibleTail}`;
61
+ if (stringWidth(prefixed) <= maxWidth) {
62
+ return prefixed;
63
+ }
64
+ return clipFromLeft(value, maxWidth);
65
+ }
66
+ function getVisibleRange(selectedIndex, totalItems, maxVisible) {
67
+ if (totalItems <= maxVisible) {
68
+ return { start: 0, end: totalItems };
69
+ }
70
+ const half = Math.floor(maxVisible / 2);
71
+ let start = Math.max(0, selectedIndex - half);
72
+ let end = Math.min(totalItems, start + maxVisible);
73
+ if (end - start < maxVisible) {
74
+ start = Math.max(0, end - maxVisible);
75
+ }
76
+ return { start, end };
77
+ }
78
+ function getTrailingRowCount(totalRows, renderedRows) {
79
+ return Math.max(0, totalRows - renderedRows);
80
+ }
81
+ function renderSingleLine(value) {
82
+ return value.length > 0 ? value : ' ';
83
+ }
84
+ function readTerminalSize(stdout) {
85
+ return {
86
+ columns: process.stdout.columns ?? stdout?.columns ?? 120,
87
+ rows: process.stdout.rows ?? stdout?.rows ?? 40,
88
+ };
89
+ }
90
+ function openInSystem(targetPath) {
91
+ return new Promise((resolve, reject) => {
92
+ let command = '';
93
+ let args = [];
94
+ if (process.platform === 'darwin') {
95
+ command = 'open';
96
+ args = [targetPath];
97
+ }
98
+ else if (process.platform === 'win32') {
99
+ command = 'cmd';
100
+ args = ['/c', 'start', '', targetPath];
101
+ }
102
+ else {
103
+ command = 'xdg-open';
104
+ args = [targetPath];
105
+ }
106
+ const child = spawn(command, args, {
107
+ detached: true,
108
+ stdio: 'ignore',
109
+ });
110
+ child.once('error', reject);
111
+ child.once('spawn', resolve);
112
+ child.unref();
113
+ });
114
+ }
115
+ function getSortOptionIndex(sortMode, filterMode) {
116
+ if (filterMode === 'diffed') {
117
+ return SORT_OPTIONS.findIndex((option) => option.id === 'filter-diffed');
118
+ }
119
+ return SORT_OPTIONS.findIndex((option) => option.id === `sort-${sortMode}`);
120
+ }
121
+ function isFilterTypingInput(input, key) {
122
+ if (!input) {
123
+ return false;
124
+ }
125
+ if (key.ctrl || key.meta || key.return || key.tab || key.escape) {
126
+ return false;
127
+ }
128
+ if (key.upArrow
129
+ || key.downArrow
130
+ || key.leftArrow
131
+ || key.rightArrow
132
+ || key.pageUp
133
+ || key.pageDown) {
134
+ return false;
135
+ }
136
+ return true;
137
+ }
138
+ function getSearchEntryIcon(entry) {
139
+ if (entry.type === 'directory') {
140
+ return '';
141
+ }
142
+ return '';
143
+ }
144
+ function getSearchEntryColor(entry) {
145
+ if (!entry.exists) {
146
+ return COLORS.error;
147
+ }
148
+ if (entry.type === 'directory') {
149
+ return 'blue';
150
+ }
151
+ return 'white';
152
+ }
153
+ const FileBrowserApp = () => {
154
+ const rootPath = process.cwd();
155
+ const projectLabel = path.basename(rootPath);
156
+ const { stdout } = useStdout();
157
+ const [terminalSize, setTerminalSize] = useState(() => readTerminalSize(stdout));
158
+ const terminalHeight = terminalSize.rows;
159
+ const terminalWidth = terminalSize.columns;
160
+ const [snapshot, setSnapshot] = useState(() => loadBrowserSnapshot(rootPath));
161
+ const [sortMode, setSortMode] = useState('name');
162
+ const [filterMode, setFilterMode] = useState('all');
163
+ const [filterQuery, setFilterQuery] = useState('');
164
+ const [listFocused, setListFocused] = useState(false);
165
+ const [expandedPaths, setExpandedPaths] = useState(() => new Set());
166
+ const [selectedPath, setSelectedPath] = useState(null);
167
+ const [sortMenuOpen, setSortMenuOpen] = useState(false);
168
+ const [sortMenuIndex, setSortMenuIndex] = useState(() => getSortOptionIndex('name', 'all'));
169
+ const [viewerPath, setViewerPath] = useState(null);
170
+ const [viewerMode, setViewerMode] = useState('code');
171
+ const [viewerScroll, setViewerScroll] = useState(0);
172
+ const [statusMessage, setStatusMessage] = useState('');
173
+ const [modifiedTimes, setModifiedTimes] = useState(null);
174
+ useEffect(() => {
175
+ if (!statusMessage) {
176
+ return;
177
+ }
178
+ const timer = setTimeout(() => setStatusMessage(''), 2500);
179
+ return () => clearTimeout(timer);
180
+ }, [statusMessage]);
181
+ useEffect(() => {
182
+ const refreshTerminalSize = () => {
183
+ setTerminalSize((current) => {
184
+ const next = readTerminalSize(stdout);
185
+ if (current.columns === next.columns && current.rows === next.rows) {
186
+ return current;
187
+ }
188
+ return next;
189
+ });
190
+ };
191
+ // A freshly split tmux pane can report a transient width on first paint.
192
+ // Refresh once immediately and again shortly after mount to pick up the final size
193
+ // without waiting for user input.
194
+ refreshTerminalSize();
195
+ const refreshTimers = [16, 80, 180].map((delay) => setTimeout(refreshTerminalSize, delay));
196
+ process.stdout.on('resize', refreshTerminalSize);
197
+ return () => {
198
+ refreshTimers.forEach((timer) => clearTimeout(timer));
199
+ process.stdout.off('resize', refreshTerminalSize);
200
+ };
201
+ }, [stdout]);
202
+ const filterActive = filterQuery.trim().length > 0;
203
+ const treeNodes = useMemo(() => buildBrowserTree(snapshot, {
204
+ sortMode,
205
+ filterMode,
206
+ modifiedTimes: sortMode === 'modified' ? modifiedTimes || undefined : undefined,
207
+ filterQuery: '',
208
+ activePath: viewerPath || selectedPath,
209
+ }), [snapshot, sortMode, filterMode, modifiedTimes, viewerPath, selectedPath]);
210
+ const visibleEntries = useMemo(() => {
211
+ if (filterActive) {
212
+ return buildBrowserSearchEntries(snapshot, {
213
+ sortMode,
214
+ filterMode,
215
+ modifiedTimes: sortMode === 'modified' ? modifiedTimes || undefined : undefined,
216
+ filterQuery,
217
+ activePath: viewerPath || selectedPath,
218
+ });
219
+ }
220
+ return flattenBrowserTree(treeNodes, expandedPaths);
221
+ }, [
222
+ filterActive,
223
+ snapshot,
224
+ sortMode,
225
+ filterMode,
226
+ modifiedTimes,
227
+ filterQuery,
228
+ viewerPath,
229
+ selectedPath,
230
+ treeNodes,
231
+ expandedPaths,
232
+ ]);
233
+ const entryByPath = useMemo(() => new Map(visibleEntries.map((entry) => [entry.path, entry])), [visibleEntries]);
234
+ useEffect(() => {
235
+ if (visibleEntries.length === 0) {
236
+ setSelectedPath(null);
237
+ setListFocused(false);
238
+ return;
239
+ }
240
+ if (!selectedPath || !entryByPath.has(selectedPath)) {
241
+ setSelectedPath(visibleEntries[0].path);
242
+ }
243
+ }, [visibleEntries, selectedPath, entryByPath]);
244
+ const selectedIndex = selectedPath
245
+ ? Math.max(0, visibleEntries.findIndex((entry) => entry.path === selectedPath))
246
+ : 0;
247
+ const selectedEntry = visibleEntries[selectedIndex];
248
+ const viewerFile = useMemo(() => snapshot.files.find((file) => file.path === viewerPath), [snapshot, viewerPath]);
249
+ const previewLines = useMemo(() => {
250
+ if (!viewerPath) {
251
+ return [];
252
+ }
253
+ if (viewerMode === 'diff') {
254
+ return loadDiffPreview(rootPath, viewerPath, viewerFile?.statusLabel || '');
255
+ }
256
+ return loadCodePreview(rootPath, viewerPath);
257
+ }, [rootPath, viewerPath, viewerMode, viewerFile?.statusLabel]);
258
+ const headerRows = 2;
259
+ const searchRows = 3;
260
+ const footerRows = 2;
261
+ const contentBoxHeight = Math.max(10, terminalHeight - headerRows - searchRows - footerRows);
262
+ const contentRows = Math.max(6, contentBoxHeight);
263
+ const listBodyRows = Math.max(1, contentRows - 1);
264
+ const viewerBodyRows = Math.max(4, contentRows - 2);
265
+ const currentViewerMaxOffset = Math.max(0, previewLines.length - viewerBodyRows);
266
+ useEffect(() => {
267
+ setViewerScroll((current) => Math.min(current, currentViewerMaxOffset));
268
+ }, [currentViewerMaxOffset]);
269
+ const refreshSnapshot = () => {
270
+ const nextSnapshot = loadBrowserSnapshot(rootPath);
271
+ setSnapshot(nextSnapshot);
272
+ if (sortMode === 'modified') {
273
+ setModifiedTimes(computeModifiedTimes(rootPath, nextSnapshot.files.map((file) => file.path)));
274
+ }
275
+ else {
276
+ setModifiedTimes(null);
277
+ }
278
+ setStatusMessage('Refreshed');
279
+ };
280
+ const ensureModifiedSortData = (nextSnapshot = snapshot) => {
281
+ if (modifiedTimes) {
282
+ return modifiedTimes;
283
+ }
284
+ const nextTimes = computeModifiedTimes(rootPath, nextSnapshot.files.map((file) => file.path));
285
+ setModifiedTimes(nextTimes);
286
+ return nextTimes;
287
+ };
288
+ const selectPathAndExpand = (nextPath) => {
289
+ const ancestors = getAncestorPaths(nextPath);
290
+ setExpandedPaths((current) => {
291
+ const next = new Set(current);
292
+ ancestors.forEach((ancestor) => next.add(ancestor));
293
+ return next;
294
+ });
295
+ setSelectedPath(nextPath);
296
+ };
297
+ const revealDirectoryFromSearch = (directoryPath) => {
298
+ setExpandedPaths((current) => {
299
+ const next = new Set(current);
300
+ getAncestorPaths(directoryPath).forEach((ancestor) => next.add(ancestor));
301
+ next.add(directoryPath);
302
+ return next;
303
+ });
304
+ setSelectedPath(directoryPath);
305
+ setFilterQuery('');
306
+ setListFocused(true);
307
+ };
308
+ const openViewer = (nextPath) => {
309
+ const file = snapshot.files.find((candidate) => candidate.path === nextPath);
310
+ selectPathAndExpand(nextPath);
311
+ setViewerPath(nextPath);
312
+ setViewerMode(file?.exists === false ? 'diff' : 'code');
313
+ setViewerScroll(0);
314
+ setSortMenuOpen(false);
315
+ setListFocused(true);
316
+ };
317
+ const toggleExpanded = (entry) => {
318
+ if (entry.type !== 'directory') {
319
+ openViewer(entry.path);
320
+ return;
321
+ }
322
+ if (filterActive) {
323
+ revealDirectoryFromSearch(entry.path);
324
+ return;
325
+ }
326
+ setExpandedPaths((current) => {
327
+ const next = new Set(current);
328
+ if (next.has(entry.path)) {
329
+ next.delete(entry.path);
330
+ }
331
+ else {
332
+ next.add(entry.path);
333
+ }
334
+ return next;
335
+ });
336
+ };
337
+ const handleOpenCurrentDirectory = async () => {
338
+ const activeEntry = viewerPath
339
+ ? {
340
+ path: viewerPath,
341
+ parentPath: viewerFile?.parentPath || null,
342
+ type: 'file',
343
+ }
344
+ : selectedEntry;
345
+ const targetPath = getCurrentDirectoryPath(rootPath, activeEntry);
346
+ try {
347
+ await openInSystem(targetPath);
348
+ setStatusMessage(`Opened ${targetPath}`);
349
+ }
350
+ catch (error) {
351
+ setStatusMessage(`Failed to open ${targetPath}: ${error instanceof Error ? error.message : String(error)}`);
352
+ }
353
+ };
354
+ const applySortSelection = (optionId) => {
355
+ if (optionId === 'sort-name') {
356
+ setSortMode('name');
357
+ setSortMenuOpen(false);
358
+ return;
359
+ }
360
+ if (optionId === 'sort-modified') {
361
+ ensureModifiedSortData();
362
+ setSortMode('modified');
363
+ setSortMenuOpen(false);
364
+ return;
365
+ }
366
+ if (optionId === 'sort-status') {
367
+ setSortMode('status');
368
+ setSortMenuOpen(false);
369
+ return;
370
+ }
371
+ if (optionId === 'filter-all') {
372
+ setFilterMode('all');
373
+ setSortMenuOpen(false);
374
+ return;
375
+ }
376
+ if (optionId === 'filter-diffed') {
377
+ setFilterMode('diffed');
378
+ setSortMenuOpen(false);
379
+ }
380
+ };
381
+ const backOutFromTree = () => {
382
+ if (filterActive) {
383
+ setFilterQuery('');
384
+ setListFocused(false);
385
+ return;
386
+ }
387
+ if (!listFocused) {
388
+ setStatusMessage('File browser stays open. Use pane controls to close it.');
389
+ return;
390
+ }
391
+ if (!selectedEntry) {
392
+ setListFocused(false);
393
+ return;
394
+ }
395
+ if (selectedEntry.type === 'directory' && selectedEntry.isExpanded) {
396
+ setExpandedPaths((current) => {
397
+ const next = new Set(current);
398
+ next.delete(selectedEntry.path);
399
+ return next;
400
+ });
401
+ return;
402
+ }
403
+ if (selectedEntry.parentPath) {
404
+ setSelectedPath(selectedEntry.parentPath);
405
+ return;
406
+ }
407
+ setListFocused(false);
408
+ };
409
+ useInput(async (input, key) => {
410
+ if (key.ctrl && input === 'c') {
411
+ process.exit(0);
412
+ return;
413
+ }
414
+ if (viewerPath) {
415
+ if (key.escape) {
416
+ setViewerPath(null);
417
+ setViewerScroll(0);
418
+ return;
419
+ }
420
+ if (input === 'd' || key.tab) {
421
+ setViewerMode((current) => (current === 'code' ? 'diff' : 'code'));
422
+ setViewerScroll(0);
423
+ return;
424
+ }
425
+ if (input === 'o') {
426
+ await handleOpenCurrentDirectory();
427
+ return;
428
+ }
429
+ if (key.upArrow) {
430
+ setViewerScroll((current) => Math.max(0, current - 1));
431
+ return;
432
+ }
433
+ if (key.downArrow) {
434
+ setViewerScroll((current) => Math.min(currentViewerMaxOffset, current + 1));
435
+ return;
436
+ }
437
+ if (key.pageUp) {
438
+ setViewerScroll((current) => Math.max(0, current - viewerBodyRows));
439
+ return;
440
+ }
441
+ if (key.pageDown) {
442
+ setViewerScroll((current) => Math.min(currentViewerMaxOffset, current + viewerBodyRows));
443
+ }
444
+ return;
445
+ }
446
+ if (sortMenuOpen) {
447
+ if (key.escape) {
448
+ setSortMenuOpen(false);
449
+ return;
450
+ }
451
+ if (key.upArrow) {
452
+ setSortMenuIndex((current) => Math.max(0, current - 1));
453
+ return;
454
+ }
455
+ if (key.downArrow) {
456
+ setSortMenuIndex((current) => Math.min(SORT_OPTIONS.length - 1, current + 1));
457
+ return;
458
+ }
459
+ if (key.return) {
460
+ applySortSelection(SORT_OPTIONS[sortMenuIndex]?.id || '');
461
+ }
462
+ return;
463
+ }
464
+ if (key.backspace || key.delete || input === '\x7f' || input === '\x08') {
465
+ if (filterQuery.length > 0) {
466
+ setFilterQuery((current) => current.slice(0, -1));
467
+ }
468
+ return;
469
+ }
470
+ if (key.ctrl && input === 'u') {
471
+ setFilterQuery('');
472
+ setListFocused(false);
473
+ return;
474
+ }
475
+ if (key.escape) {
476
+ backOutFromTree();
477
+ return;
478
+ }
479
+ if (input === 'S') {
480
+ setSortMenuIndex(getSortOptionIndex(sortMode, filterMode));
481
+ setSortMenuOpen(true);
482
+ return;
483
+ }
484
+ if (input === 'O') {
485
+ await handleOpenCurrentDirectory();
486
+ return;
487
+ }
488
+ if (input === 'R') {
489
+ refreshSnapshot();
490
+ return;
491
+ }
492
+ if (input === 'P') {
493
+ setListFocused(false);
494
+ return;
495
+ }
496
+ if (isFilterTypingInput(input, key)) {
497
+ setFilterQuery((current) => `${current}${input}`);
498
+ return;
499
+ }
500
+ if (!selectedEntry) {
501
+ return;
502
+ }
503
+ if (key.upArrow) {
504
+ if (!listFocused) {
505
+ return;
506
+ }
507
+ if (selectedIndex <= 0) {
508
+ setListFocused(false);
509
+ return;
510
+ }
511
+ setSelectedPath(visibleEntries[selectedIndex - 1]?.path || selectedEntry.path);
512
+ return;
513
+ }
514
+ if (key.downArrow) {
515
+ if (!listFocused) {
516
+ if (visibleEntries.length > 0) {
517
+ setListFocused(true);
518
+ setSelectedPath(visibleEntries[0].path);
519
+ }
520
+ return;
521
+ }
522
+ const nextIndex = Math.min(visibleEntries.length - 1, selectedIndex + 1);
523
+ setSelectedPath(visibleEntries[nextIndex]?.path || selectedEntry.path);
524
+ return;
525
+ }
526
+ if (key.leftArrow) {
527
+ if (!listFocused) {
528
+ return;
529
+ }
530
+ if (filterActive) {
531
+ setListFocused(false);
532
+ return;
533
+ }
534
+ if (selectedEntry.type === 'directory' && selectedEntry.isExpanded) {
535
+ setExpandedPaths((current) => {
536
+ const next = new Set(current);
537
+ next.delete(selectedEntry.path);
538
+ return next;
539
+ });
540
+ return;
541
+ }
542
+ if (selectedEntry.parentPath) {
543
+ setSelectedPath(selectedEntry.parentPath);
544
+ }
545
+ return;
546
+ }
547
+ if (key.rightArrow) {
548
+ if (!listFocused) {
549
+ if (visibleEntries.length > 0) {
550
+ setListFocused(true);
551
+ }
552
+ return;
553
+ }
554
+ if (selectedEntry.type === 'directory') {
555
+ if (filterActive) {
556
+ revealDirectoryFromSearch(selectedEntry.path);
557
+ }
558
+ else if (!selectedEntry.isExpanded) {
559
+ setExpandedPaths((current) => new Set(current).add(selectedEntry.path));
560
+ }
561
+ else {
562
+ const firstChild = visibleEntries[selectedIndex + 1];
563
+ if (firstChild && firstChild.parentPath === selectedEntry.path) {
564
+ setSelectedPath(firstChild.path);
565
+ }
566
+ }
567
+ }
568
+ else {
569
+ openViewer(selectedEntry.path);
570
+ }
571
+ return;
572
+ }
573
+ if (key.return) {
574
+ if (!listFocused) {
575
+ if (visibleEntries.length > 0) {
576
+ setListFocused(true);
577
+ }
578
+ return;
579
+ }
580
+ toggleExpanded(selectedEntry);
581
+ }
582
+ });
583
+ const listRange = getVisibleRange(selectedIndex, visibleEntries.length, listBodyRows);
584
+ const visibleListItems = visibleEntries.slice(listRange.start, listRange.end);
585
+ const visiblePreviewLines = previewLines.slice(viewerScroll, viewerScroll + viewerBodyRows);
586
+ const frameWidth = Math.max(30, terminalWidth - 2);
587
+ const searchWidth = frameWidth;
588
+ const searchInnerWidth = Math.max(24, searchWidth - 2);
589
+ const contentWidth = frameWidth;
590
+ const rowMarkerWidth = 2;
591
+ const searchIconWidth = filterActive ? 2 : 0;
592
+ const statusColumnWidth = 4;
593
+ const itemLabelWidth = Math.max(10, contentWidth - rowMarkerWidth - searchIconWidth - statusColumnWidth);
594
+ const filterFocused = !viewerPath && !sortMenuOpen && !listFocused;
595
+ const filterDisplay = filterQuery || 'Search files and directories';
596
+ const filterCursor = filterFocused ? '|' : '';
597
+ const listTrailingRows = getTrailingRowCount(listBodyRows, visibleListItems.length);
598
+ const viewerTrailingRows = getTrailingRowCount(viewerBodyRows, visiblePreviewLines.length);
599
+ const sortMenuRenderedRows = SORT_OPTIONS.length + 1;
600
+ const sortTrailingRows = getTrailingRowCount(contentRows, sortMenuRenderedRows);
601
+ const sectionTitle = viewerPath
602
+ ? ' Quick View'
603
+ : sortMenuOpen
604
+ ? ' Sort and Filter'
605
+ : filterActive
606
+ ? ' Search Results'
607
+ : ' Explorer';
608
+ const sectionSummary = viewerPath
609
+ ? `${viewerMode === 'code' ? 'Code view' : 'Diff view'} • Lines ${Math.min(viewerScroll + 1, previewLines.length)}-${Math.min(viewerScroll + viewerBodyRows, previewLines.length)} of ${previewLines.length}`
610
+ : sortMenuOpen
611
+ ? 'Choose a sort or filter mode'
612
+ : `${visibleEntries.length} ${filterActive ? 'matches' : visibleEntries.length === 1 ? 'item' : 'items'} • sort: ${sortMode} • ${filterMode === 'diffed' ? 'changed only' : 'all files'}`;
613
+ const footerHelp = viewerPath
614
+ ? 'Esc back • d toggle code/diff • o open directory • PgUp/PgDn scroll'
615
+ : sortMenuOpen
616
+ ? '↑↓ choose • Enter apply • Esc back'
617
+ : 'Type to filter • ↓ focus list • Enter open • Shift+S sort • Shift+O open dir • Shift+R refresh • Esc back';
618
+ return (React.createElement(Box, { flexDirection: "column", paddingX: 1, width: terminalWidth },
619
+ React.createElement(Box, { flexDirection: "column", width: frameWidth },
620
+ React.createElement(Box, { width: frameWidth },
621
+ React.createElement(Text, { bold: true, color: COLORS.accent, wrap: "truncate-end" }, renderSingleLine(`Files: ${projectLabel}`))),
622
+ React.createElement(Box, { width: frameWidth },
623
+ React.createElement(Text, { dimColor: true, wrap: "truncate-end" }, renderSingleLine(clipFromLeft(rootPath, frameWidth))))),
624
+ React.createElement(Box, { borderStyle: POPUP_CONFIG.borderStyle, borderColor: filterFocused ? POPUP_CONFIG.inputBorderColor : POPUP_CONFIG.borderColor, width: searchWidth, height: searchRows, flexDirection: "column" },
625
+ React.createElement(Box, { width: searchInnerWidth },
626
+ React.createElement(Text, { bold: true, color: filterFocused ? POPUP_CONFIG.inputBorderColor : POPUP_CONFIG.borderColor },
627
+ "\uF002",
628
+ ' '),
629
+ React.createElement(Text, { color: filterFocused ? 'white' : undefined, dimColor: !filterQuery, wrap: "truncate-end" }, renderSingleLine(clipToWidth(`${filterDisplay}${filterQuery ? filterCursor : filterFocused ? filterCursor : ''}`, searchInnerWidth - 2))))),
630
+ viewerPath ? (React.createElement(Box, { flexDirection: "column", width: contentWidth, height: contentBoxHeight },
631
+ React.createElement(Box, { width: contentWidth },
632
+ React.createElement(Text, { bold: true, color: "yellow", wrap: "truncate-end" }, renderSingleLine(clipToWidth(`${sectionTitle} • ${viewerPath}`, contentWidth)))),
633
+ React.createElement(Box, { width: contentWidth },
634
+ React.createElement(Text, { dimColor: true, wrap: "truncate-end" }, renderSingleLine(clipToWidth(sectionSummary, contentWidth)))),
635
+ visiblePreviewLines.map((line, index) => (React.createElement(Box, { key: `${viewerScroll + index}`, width: contentWidth },
636
+ React.createElement(Text, { wrap: "truncate-end" }, line.length > 0 ? line : ' ')))),
637
+ Array.from({ length: viewerTrailingRows }, (_, index) => (React.createElement(Box, { key: `viewer-pad-${index}`, width: contentWidth },
638
+ React.createElement(Text, null, " ")))))) : sortMenuOpen ? (React.createElement(Box, { flexDirection: "column", width: contentWidth, height: contentBoxHeight },
639
+ React.createElement(Box, { width: contentWidth },
640
+ React.createElement(Text, { bold: true, color: "cyan", wrap: "truncate-end" }, sectionTitle)),
641
+ SORT_OPTIONS.map((option, index) => {
642
+ const selected = index === sortMenuIndex;
643
+ return (React.createElement(Box, { key: option.id, width: contentWidth },
644
+ React.createElement(Text, { bold: selected, color: selected ? 'black' : 'white', backgroundColor: selected ? COLORS.accent : undefined, wrap: "truncate-end" }, renderSingleLine(clipToWidth(`${selected ? '▌ ' : ' '}${option.label} • ${option.description}`, contentWidth)))));
645
+ }),
646
+ Array.from({ length: sortTrailingRows }, (_, index) => (React.createElement(Box, { key: `sort-pad-${index}`, width: contentWidth },
647
+ React.createElement(Text, null, " ")))))) : (React.createElement(Box, { flexDirection: "column", width: contentWidth, height: contentBoxHeight },
648
+ React.createElement(Box, { width: contentWidth },
649
+ React.createElement(Text, { bold: true, color: listFocused ? POPUP_CONFIG.borderColor : 'gray', wrap: "truncate-end" }, renderSingleLine(clipToWidth(`${sectionTitle} • ${sectionSummary}`, contentWidth)))),
650
+ visibleEntries.length === 0 ? (React.createElement(React.Fragment, null,
651
+ React.createElement(Box, { width: contentWidth },
652
+ React.createElement(Text, { dimColor: true, wrap: "truncate-end" }, "No files match the current filter.")),
653
+ Array.from({ length: Math.max(0, listBodyRows - 1) }, (_, index) => (React.createElement(Box, { key: `empty-pad-${index}`, width: contentWidth },
654
+ React.createElement(Text, null, " ")))))) : (React.createElement(React.Fragment, null,
655
+ visibleListItems.map((entry) => {
656
+ const selected = listFocused && entry.path === selectedPath;
657
+ const rowBackground = selected ? COLORS.accent : undefined;
658
+ const statusText = statusColumnWidth > 1
659
+ ? (entry.statusLabel || '').padStart(statusColumnWidth - 1, ' ')
660
+ : entry.statusLabel || '';
661
+ const selectionMarker = selected ? '▌ ' : ' ';
662
+ const entryLabel = filterActive
663
+ ? clipPathToWidth(entry.type === 'directory' ? `${entry.path}/` : entry.path, itemLabelWidth)
664
+ : clipToWidth(entry.displayLabel, itemLabelWidth);
665
+ const entryColor = selected
666
+ ? 'black'
667
+ : filterActive
668
+ ? getSearchEntryColor(entry)
669
+ : entry.type === 'directory'
670
+ ? 'blue'
671
+ : entry.exists
672
+ ? 'white'
673
+ : COLORS.error;
674
+ return (React.createElement(Box, { key: entry.path, width: contentWidth },
675
+ React.createElement(Box, { width: rowMarkerWidth },
676
+ React.createElement(Text, { color: selected ? 'black' : POPUP_CONFIG.borderColor, backgroundColor: rowBackground }, selectionMarker)),
677
+ filterActive ? (React.createElement(Box, { width: searchIconWidth },
678
+ React.createElement(Text, { color: entryColor, backgroundColor: rowBackground }, getSearchEntryIcon(entry)))) : null,
679
+ React.createElement(Box, { width: itemLabelWidth },
680
+ React.createElement(Text, { bold: selected || entry.type === 'directory', color: entryColor, backgroundColor: rowBackground, wrap: "truncate-end" }, renderSingleLine(entryLabel))),
681
+ React.createElement(Box, { width: statusColumnWidth, justifyContent: "flex-end" },
682
+ React.createElement(Text, { color: selected ? 'black' : getStatusColor(entry.statusLabel), backgroundColor: rowBackground }, statusText))));
683
+ }),
684
+ Array.from({ length: listTrailingRows }, (_, index) => (React.createElement(Box, { key: `list-pad-${index}`, width: contentWidth },
685
+ React.createElement(Text, null, " ")))))))),
686
+ React.createElement(Box, { flexDirection: "column", width: frameWidth },
687
+ React.createElement(Box, { width: frameWidth },
688
+ React.createElement(Text, { color: statusMessage ? 'green' : undefined, wrap: "truncate-end" }, renderSingleLine(clipToWidth(statusMessage || ' ', frameWidth)))),
689
+ React.createElement(Box, { width: frameWidth },
690
+ React.createElement(Text, { dimColor: true, wrap: "truncate-end" }, renderSingleLine(clipToWidth(footerHelp, frameWidth)))))));
691
+ };
692
+ export default FileBrowserApp;
693
+ //# sourceMappingURL=FileBrowserApp.js.map