dmux 5.3.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 (244) hide show
  1. package/README.md +2 -1
  2. package/dist/DmuxApp.d.ts.map +1 -1
  3. package/dist/DmuxApp.js +226 -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.d.ts.map +1 -1
  10. package/dist/actions/implementations/closeAction.js +47 -5
  11. package/dist/actions/implementations/closeAction.js.map +1 -1
  12. package/dist/actions/implementations/mergeAction.d.ts.map +1 -1
  13. package/dist/actions/implementations/mergeAction.js +51 -16
  14. package/dist/actions/implementations/mergeAction.js.map +1 -1
  15. package/dist/actions/implementations/viewAction.d.ts.map +1 -1
  16. package/dist/actions/implementations/viewAction.js +7 -0
  17. package/dist/actions/implementations/viewAction.js.map +1 -1
  18. package/dist/actions/index.d.ts.map +1 -1
  19. package/dist/actions/index.js +24 -0
  20. package/dist/actions/index.js.map +1 -1
  21. package/dist/actions/merge/multiMergeOrchestrator.js +1 -1
  22. package/dist/actions/merge/multiMergeOrchestrator.js.map +1 -1
  23. package/dist/actions/types.d.ts +19 -4
  24. package/dist/actions/types.d.ts.map +1 -1
  25. package/dist/actions/types.js +91 -0
  26. package/dist/actions/types.js.map +1 -1
  27. package/dist/components/indicators/Spinner.d.ts +2 -0
  28. package/dist/components/indicators/Spinner.d.ts.map +1 -1
  29. package/dist/components/indicators/Spinner.js +4 -4
  30. package/dist/components/indicators/Spinner.js.map +1 -1
  31. package/dist/components/panes/KebabMenu.d.ts +2 -2
  32. package/dist/components/panes/KebabMenu.d.ts.map +1 -1
  33. package/dist/components/panes/KebabMenu.js +9 -4
  34. package/dist/components/panes/KebabMenu.js.map +1 -1
  35. package/dist/components/panes/PaneCard.d.ts.map +1 -1
  36. package/dist/components/panes/PaneCard.js +20 -4
  37. package/dist/components/panes/PaneCard.js.map +1 -1
  38. package/dist/components/panes/PanesGrid.d.ts +1 -0
  39. package/dist/components/panes/PanesGrid.d.ts.map +1 -1
  40. package/dist/components/panes/PanesGrid.js +11 -3
  41. package/dist/components/panes/PanesGrid.js.map +1 -1
  42. package/dist/components/popups/agentChoicePopup.js +29 -24
  43. package/dist/components/popups/agentChoicePopup.js.map +1 -1
  44. package/dist/components/popups/agentChoiceSelection.d.ts +8 -0
  45. package/dist/components/popups/agentChoiceSelection.d.ts.map +1 -0
  46. package/dist/components/popups/agentChoiceSelection.js +14 -0
  47. package/dist/components/popups/agentChoiceSelection.js.map +1 -0
  48. package/dist/components/popups/kebabMenuPopup.js +9 -4
  49. package/dist/components/popups/kebabMenuPopup.js.map +1 -1
  50. package/dist/components/popups/notificationSoundsPopup.d.ts +25 -0
  51. package/dist/components/popups/notificationSoundsPopup.d.ts.map +1 -0
  52. package/dist/components/popups/notificationSoundsPopup.js +165 -0
  53. package/dist/components/popups/notificationSoundsPopup.js.map +1 -0
  54. package/dist/components/popups/settingsPopup.js +361 -26
  55. package/dist/components/popups/settingsPopup.js.map +1 -1
  56. package/dist/components/popups/shortcutsPopup.js +11 -5
  57. package/dist/components/popups/shortcutsPopup.js.map +1 -1
  58. package/dist/constants/layout.d.ts +9 -0
  59. package/dist/constants/layout.d.ts.map +1 -0
  60. package/dist/constants/layout.js +9 -0
  61. package/dist/constants/layout.js.map +1 -0
  62. package/dist/hooks/useActionSystem.d.ts +9 -5
  63. package/dist/hooks/useActionSystem.d.ts.map +1 -1
  64. package/dist/hooks/useActionSystem.js +21 -19
  65. package/dist/hooks/useActionSystem.js.map +1 -1
  66. package/dist/hooks/useInputHandling.d.ts +1 -0
  67. package/dist/hooks/useInputHandling.d.ts.map +1 -1
  68. package/dist/hooks/useInputHandling.js +499 -79
  69. package/dist/hooks/useInputHandling.js.map +1 -1
  70. package/dist/hooks/usePaneCreation.d.ts +4 -2
  71. package/dist/hooks/usePaneCreation.d.ts.map +1 -1
  72. package/dist/hooks/usePaneCreation.js +6 -0
  73. package/dist/hooks/usePaneCreation.js.map +1 -1
  74. package/dist/hooks/usePaneLoading.d.ts +1 -0
  75. package/dist/hooks/usePaneLoading.d.ts.map +1 -1
  76. package/dist/hooks/usePaneLoading.js +10 -7
  77. package/dist/hooks/usePaneLoading.js.map +1 -1
  78. package/dist/hooks/usePaneSync.js +2 -2
  79. package/dist/hooks/usePaneSync.js.map +1 -1
  80. package/dist/hooks/usePanes.d.ts.map +1 -1
  81. package/dist/hooks/usePanes.js +18 -4
  82. package/dist/hooks/usePanes.js.map +1 -1
  83. package/dist/hooks/useProjectActivity.d.ts +7 -0
  84. package/dist/hooks/useProjectActivity.d.ts.map +1 -0
  85. package/dist/hooks/useProjectActivity.js +79 -0
  86. package/dist/hooks/useProjectActivity.js.map +1 -0
  87. package/dist/hooks/useServices.d.ts +3 -0
  88. package/dist/hooks/useServices.d.ts.map +1 -1
  89. package/dist/hooks/useServices.js +4 -0
  90. package/dist/hooks/useServices.js.map +1 -1
  91. package/dist/index.js +59 -15
  92. package/dist/index.js.map +1 -1
  93. package/dist/layout/LayoutCalculator.d.ts.map +1 -1
  94. package/dist/layout/LayoutCalculator.js +4 -1
  95. package/dist/layout/LayoutCalculator.js.map +1 -1
  96. package/dist/services/DmuxAttentionService.d.ts +33 -0
  97. package/dist/services/DmuxAttentionService.d.ts.map +1 -0
  98. package/dist/services/DmuxAttentionService.js +172 -0
  99. package/dist/services/DmuxAttentionService.js.map +1 -0
  100. package/dist/services/DmuxFocusService.d.ts +58 -0
  101. package/dist/services/DmuxFocusService.d.ts.map +1 -0
  102. package/dist/services/DmuxFocusService.js +671 -0
  103. package/dist/services/DmuxFocusService.js.map +1 -0
  104. package/dist/services/PaneAnalyzer.d.ts +11 -2
  105. package/dist/services/PaneAnalyzer.d.ts.map +1 -1
  106. package/dist/services/PaneAnalyzer.js +88 -22
  107. package/dist/services/PaneAnalyzer.js.map +1 -1
  108. package/dist/services/PopupManager.d.ts +31 -14
  109. package/dist/services/PopupManager.d.ts.map +1 -1
  110. package/dist/services/PopupManager.js +147 -68
  111. package/dist/services/PopupManager.js.map +1 -1
  112. package/dist/services/StatusDetector.d.ts +14 -0
  113. package/dist/services/StatusDetector.d.ts.map +1 -1
  114. package/dist/services/StatusDetector.js +60 -12
  115. package/dist/services/StatusDetector.js.map +1 -1
  116. package/dist/services/TmuxHookManager.d.ts.map +1 -1
  117. package/dist/services/TmuxHookManager.js +4 -2
  118. package/dist/services/TmuxHookManager.js.map +1 -1
  119. package/dist/services/TmuxService.d.ts +37 -2
  120. package/dist/services/TmuxService.d.ts.map +1 -1
  121. package/dist/services/TmuxService.js +138 -16
  122. package/dist/services/TmuxService.js.map +1 -1
  123. package/dist/types/activity.d.ts +4 -0
  124. package/dist/types/activity.d.ts.map +1 -0
  125. package/dist/types/activity.js +2 -0
  126. package/dist/types/activity.js.map +1 -0
  127. package/dist/types.d.ts +18 -1
  128. package/dist/types.d.ts.map +1 -1
  129. package/dist/utils/attachAgent.d.ts +4 -0
  130. package/dist/utils/attachAgent.d.ts.map +1 -1
  131. package/dist/utils/attachAgent.js +18 -7
  132. package/dist/utils/attachAgent.js.map +1 -1
  133. package/dist/utils/controlPaneRecovery.d.ts +2 -0
  134. package/dist/utils/controlPaneRecovery.d.ts.map +1 -0
  135. package/dist/utils/controlPaneRecovery.js +156 -0
  136. package/dist/utils/controlPaneRecovery.js.map +1 -0
  137. package/dist/utils/devWatchExit.d.ts +2 -0
  138. package/dist/utils/devWatchExit.d.ts.map +1 -0
  139. package/dist/utils/devWatchExit.js +10 -0
  140. package/dist/utils/devWatchExit.js.map +1 -0
  141. package/dist/utils/dmuxCommand.d.ts +3 -0
  142. package/dist/utils/dmuxCommand.d.ts.map +1 -0
  143. package/dist/utils/dmuxCommand.js +18 -0
  144. package/dist/utils/dmuxCommand.js.map +1 -0
  145. package/dist/utils/fileBrowser.d.ts +61 -0
  146. package/dist/utils/fileBrowser.d.ts.map +1 -0
  147. package/dist/utils/fileBrowser.js +567 -0
  148. package/dist/utils/fileBrowser.js.map +1 -0
  149. package/dist/utils/focusDetection.d.ts +38 -0
  150. package/dist/utils/focusDetection.d.ts.map +1 -0
  151. package/dist/utils/focusDetection.js +57 -0
  152. package/dist/utils/focusDetection.js.map +1 -0
  153. package/dist/utils/generated-agents-doc.d.ts +1 -1
  154. package/dist/utils/generated-agents-doc.js +1 -1
  155. package/dist/utils/git.d.ts +4 -0
  156. package/dist/utils/git.d.ts.map +1 -1
  157. package/dist/utils/git.js +15 -0
  158. package/dist/utils/git.js.map +1 -1
  159. package/dist/utils/layoutManager.d.ts +5 -1
  160. package/dist/utils/layoutManager.d.ts.map +1 -1
  161. package/dist/utils/layoutManager.js +103 -26
  162. package/dist/utils/layoutManager.js.map +1 -1
  163. package/dist/utils/mergeTargets.d.ts +17 -0
  164. package/dist/utils/mergeTargets.d.ts.map +1 -0
  165. package/dist/utils/mergeTargets.js +132 -0
  166. package/dist/utils/mergeTargets.js.map +1 -0
  167. package/dist/utils/mergeValidation.d.ts.map +1 -1
  168. package/dist/utils/mergeValidation.js +12 -5
  169. package/dist/utils/mergeValidation.js.map +1 -1
  170. package/dist/utils/notificationSoundPreview.d.ts +10 -0
  171. package/dist/utils/notificationSoundPreview.d.ts.map +1 -0
  172. package/dist/utils/notificationSoundPreview.js +54 -0
  173. package/dist/utils/notificationSoundPreview.js.map +1 -0
  174. package/dist/utils/notificationSounds.d.ts +17 -0
  175. package/dist/utils/notificationSounds.d.ts.map +1 -0
  176. package/dist/utils/notificationSounds.js +123 -0
  177. package/dist/utils/notificationSounds.js.map +1 -0
  178. package/dist/utils/paneAttentionHeuristics.d.ts +4 -0
  179. package/dist/utils/paneAttentionHeuristics.d.ts.map +1 -0
  180. package/dist/utils/paneAttentionHeuristics.js +135 -0
  181. package/dist/utils/paneAttentionHeuristics.js.map +1 -0
  182. package/dist/utils/paneCreation.d.ts +3 -1
  183. package/dist/utils/paneCreation.d.ts.map +1 -1
  184. package/dist/utils/paneCreation.js +23 -5
  185. package/dist/utils/paneCreation.js.map +1 -1
  186. package/dist/utils/paneVisibility.d.ts +12 -0
  187. package/dist/utils/paneVisibility.d.ts.map +1 -0
  188. package/dist/utils/paneVisibility.js +60 -0
  189. package/dist/utils/paneVisibility.js.map +1 -0
  190. package/dist/utils/processShutdown.d.ts +4 -0
  191. package/dist/utils/processShutdown.d.ts.map +1 -0
  192. package/dist/utils/processShutdown.js +27 -0
  193. package/dist/utils/processShutdown.js.map +1 -0
  194. package/dist/utils/promptStore.d.ts.map +1 -1
  195. package/dist/utils/promptStore.js +6 -0
  196. package/dist/utils/promptStore.js.map +1 -1
  197. package/dist/utils/reopenWorktree.d.ts.map +1 -1
  198. package/dist/utils/reopenWorktree.js +8 -0
  199. package/dist/utils/reopenWorktree.js.map +1 -1
  200. package/dist/utils/runtimePaths.d.ts +1 -0
  201. package/dist/utils/runtimePaths.d.ts.map +1 -1
  202. package/dist/utils/runtimePaths.js +3 -0
  203. package/dist/utils/runtimePaths.js.map +1 -1
  204. package/dist/utils/settingsManager.d.ts +3 -0
  205. package/dist/utils/settingsManager.d.ts.map +1 -1
  206. package/dist/utils/settingsManager.js +203 -11
  207. package/dist/utils/settingsManager.js.map +1 -1
  208. package/dist/utils/tmux.d.ts +5 -1
  209. package/dist/utils/tmux.d.ts.map +1 -1
  210. package/dist/utils/tmux.js +23 -5
  211. package/dist/utils/tmux.js.map +1 -1
  212. package/dist/utils/tmuxConfigOnboarding.js +1 -1
  213. package/dist/utils/tmuxConfigOnboarding.js.map +1 -1
  214. package/dist/utils/tmuxHookCommands.d.ts +14 -0
  215. package/dist/utils/tmuxHookCommands.d.ts.map +1 -0
  216. package/dist/utils/tmuxHookCommands.js +30 -0
  217. package/dist/utils/tmuxHookCommands.js.map +1 -0
  218. package/dist/utils/tmuxRuntimeCompatibility.d.ts +11 -0
  219. package/dist/utils/tmuxRuntimeCompatibility.d.ts.map +1 -0
  220. package/dist/utils/tmuxRuntimeCompatibility.js +71 -0
  221. package/dist/utils/tmuxRuntimeCompatibility.js.map +1 -0
  222. package/dist/utils/worktreeMetadata.d.ts +9 -0
  223. package/dist/utils/worktreeMetadata.d.ts.map +1 -0
  224. package/dist/utils/worktreeMetadata.js +60 -0
  225. package/dist/utils/worktreeMetadata.js.map +1 -0
  226. package/dist/workers/PaneWorker.js +64 -128
  227. package/dist/workers/PaneWorker.js.map +1 -1
  228. package/dist/workers/WorkerMessages.d.ts +4 -1
  229. package/dist/workers/WorkerMessages.d.ts.map +1 -1
  230. package/dist/workers/WorkerMessages.js.map +1 -1
  231. package/native/macos/dmux-helper-Info.plist +30 -0
  232. package/native/macos/dmux-helper-icon.png +0 -0
  233. package/native/macos/dmux-helper.swift +831 -0
  234. package/native/macos/sounds/dmux-braam.caf +0 -0
  235. package/native/macos/sounds/dmux-brass.caf +0 -0
  236. package/native/macos/sounds/dmux-ding-bell.caf +0 -0
  237. package/native/macos/sounds/dmux-future.caf +0 -0
  238. package/native/macos/sounds/dmux-harp.caf +0 -0
  239. package/native/macos/sounds/dmux-quiet-bells.caf +0 -0
  240. package/native/macos/sounds/dmux-sonar.caf +0 -0
  241. package/native/macos/sounds/dmux-success.caf +0 -0
  242. package/native/macos/sounds/dmux-triumphant-trumpet.caf +0 -0
  243. package/native/macos/sounds/dmux-war-horn.caf +0 -0
  244. 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