dmux 1.6.0 → 2.0.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 (139) hide show
  1. package/README.md +12 -8
  2. package/dist/AutoUpdater.d.ts +2 -2
  3. package/dist/AutoUpdater.d.ts.map +1 -1
  4. package/dist/AutoUpdater.js +18 -7
  5. package/dist/AutoUpdater.js.map +1 -1
  6. package/dist/CleanTextInput.d.ts.map +1 -1
  7. package/dist/CleanTextInput.js +12 -4
  8. package/dist/CleanTextInput.js.map +1 -1
  9. package/dist/DmuxApp.d.ts +1 -10
  10. package/dist/DmuxApp.d.ts.map +1 -1
  11. package/dist/DmuxApp.js +146 -1483
  12. package/dist/DmuxApp.js.map +1 -1
  13. package/dist/components/AgentChoiceDialog.d.ts +7 -0
  14. package/dist/components/AgentChoiceDialog.d.ts.map +1 -0
  15. package/dist/components/AgentChoiceDialog.js +12 -0
  16. package/dist/components/AgentChoiceDialog.js.map +1 -0
  17. package/dist/components/CloseOptionsDialog.d.ts +9 -0
  18. package/dist/components/CloseOptionsDialog.d.ts.map +1 -0
  19. package/dist/components/CloseOptionsDialog.js +30 -0
  20. package/dist/components/CloseOptionsDialog.js.map +1 -0
  21. package/dist/components/CommandPromptDialog.d.ts +9 -0
  22. package/dist/components/CommandPromptDialog.d.ts.map +1 -0
  23. package/dist/components/CommandPromptDialog.js +20 -0
  24. package/dist/components/CommandPromptDialog.js.map +1 -0
  25. package/dist/components/CreatingIndicator.d.ts +7 -0
  26. package/dist/components/CreatingIndicator.d.ts.map +1 -0
  27. package/dist/components/CreatingIndicator.js +10 -0
  28. package/dist/components/CreatingIndicator.js.map +1 -0
  29. package/dist/components/FileCopyPrompt.d.ts +4 -0
  30. package/dist/components/FileCopyPrompt.d.ts.map +1 -0
  31. package/dist/components/FileCopyPrompt.js +13 -0
  32. package/dist/components/FileCopyPrompt.js.map +1 -0
  33. package/dist/components/FooterHelp.d.ts +8 -0
  34. package/dist/components/FooterHelp.d.ts.map +1 -0
  35. package/dist/components/FooterHelp.js +12 -0
  36. package/dist/components/FooterHelp.js.map +1 -0
  37. package/dist/components/LoadingIndicator.d.ts +4 -0
  38. package/dist/components/LoadingIndicator.d.ts.map +1 -0
  39. package/dist/components/LoadingIndicator.js +10 -0
  40. package/dist/components/LoadingIndicator.js.map +1 -0
  41. package/dist/components/MergeConfirmationDialog.d.ts +8 -0
  42. package/dist/components/MergeConfirmationDialog.d.ts.map +1 -0
  43. package/dist/components/MergeConfirmationDialog.js +13 -0
  44. package/dist/components/MergeConfirmationDialog.js.map +1 -0
  45. package/dist/components/NewPaneDialog.d.ts +9 -0
  46. package/dist/components/NewPaneDialog.d.ts.map +1 -0
  47. package/dist/components/NewPaneDialog.js +13 -0
  48. package/dist/components/NewPaneDialog.js.map +1 -0
  49. package/dist/components/PaneCard.d.ts +9 -0
  50. package/dist/components/PaneCard.d.ts.map +1 -0
  51. package/dist/components/PaneCard.js +37 -0
  52. package/dist/components/PaneCard.js.map +1 -0
  53. package/dist/components/PanesGrid.d.ts +11 -0
  54. package/dist/components/PanesGrid.d.ts.map +1 -0
  55. package/dist/components/PanesGrid.js +11 -0
  56. package/dist/components/PanesGrid.js.map +1 -0
  57. package/dist/components/RunningIndicator.d.ts +4 -0
  58. package/dist/components/RunningIndicator.d.ts.map +1 -0
  59. package/dist/components/RunningIndicator.js +9 -0
  60. package/dist/components/RunningIndicator.js.map +1 -0
  61. package/dist/components/UpdateDialog.d.ts +7 -0
  62. package/dist/components/UpdateDialog.d.ts.map +1 -0
  63. package/dist/components/UpdateDialog.js +27 -0
  64. package/dist/components/UpdateDialog.js.map +1 -0
  65. package/dist/components/UpdatingIndicator.d.ts +4 -0
  66. package/dist/components/UpdatingIndicator.d.ts.map +1 -0
  67. package/dist/components/UpdatingIndicator.js +9 -0
  68. package/dist/components/UpdatingIndicator.js.map +1 -0
  69. package/dist/hooks/useAgentDetection.d.ts +4 -0
  70. package/dist/hooks/useAgentDetection.d.ts.map +1 -0
  71. package/dist/hooks/useAgentDetection.js +71 -0
  72. package/dist/hooks/useAgentDetection.js.map +1 -0
  73. package/dist/hooks/useAgentStatus.d.ts +11 -0
  74. package/dist/hooks/useAgentStatus.d.ts.map +1 -0
  75. package/dist/hooks/useAgentStatus.js +172 -0
  76. package/dist/hooks/useAgentStatus.js.map +1 -0
  77. package/dist/hooks/useAutoUpdater.d.ts +20 -0
  78. package/dist/hooks/useAutoUpdater.d.ts.map +1 -0
  79. package/dist/hooks/useAutoUpdater.js +100 -0
  80. package/dist/hooks/useAutoUpdater.js.map +1 -0
  81. package/dist/hooks/useCommandRunner.d.ts +18 -0
  82. package/dist/hooks/useCommandRunner.d.ts.map +1 -0
  83. package/dist/hooks/useCommandRunner.js +44 -0
  84. package/dist/hooks/useCommandRunner.js.map +1 -0
  85. package/dist/hooks/useNavigation.d.ts +8 -0
  86. package/dist/hooks/useNavigation.d.ts.map +1 -0
  87. package/dist/hooks/useNavigation.js +62 -0
  88. package/dist/hooks/useNavigation.js.map +1 -0
  89. package/dist/hooks/usePaneCreation.d.ts +16 -0
  90. package/dist/hooks/usePaneCreation.d.ts.map +1 -0
  91. package/dist/hooks/usePaneCreation.js +133 -0
  92. package/dist/hooks/usePaneCreation.js.map +1 -0
  93. package/dist/hooks/usePaneRunner.d.ts +17 -0
  94. package/dist/hooks/usePaneRunner.d.ts.map +1 -0
  95. package/dist/hooks/usePaneRunner.js +149 -0
  96. package/dist/hooks/usePaneRunner.js.map +1 -0
  97. package/dist/hooks/usePanes.d.ts +9 -0
  98. package/dist/hooks/usePanes.d.ts.map +1 -0
  99. package/dist/hooks/usePanes.js +222 -0
  100. package/dist/hooks/usePanes.js.map +1 -0
  101. package/dist/hooks/useProjectSettings.d.ts +6 -0
  102. package/dist/hooks/useProjectSettings.d.ts.map +1 -0
  103. package/dist/hooks/useProjectSettings.js +46 -0
  104. package/dist/hooks/useProjectSettings.js.map +1 -0
  105. package/dist/hooks/useTerminalWidth.d.ts +2 -0
  106. package/dist/hooks/useTerminalWidth.d.ts.map +1 -0
  107. package/dist/hooks/useTerminalWidth.js +13 -0
  108. package/dist/hooks/useTerminalWidth.js.map +1 -0
  109. package/dist/hooks/useWorktreeActions.d.ts +17 -0
  110. package/dist/hooks/useWorktreeActions.d.ts.map +1 -0
  111. package/dist/hooks/useWorktreeActions.js +190 -0
  112. package/dist/hooks/useWorktreeActions.js.map +1 -0
  113. package/dist/index.js +86 -11
  114. package/dist/index.js.map +1 -1
  115. package/dist/types.d.ts +38 -0
  116. package/dist/types.d.ts.map +1 -0
  117. package/dist/types.js +2 -0
  118. package/dist/types.js.map +1 -0
  119. package/dist/utils/commands.d.ts +6 -0
  120. package/dist/utils/commands.d.ts.map +1 -0
  121. package/dist/utils/commands.js +35 -0
  122. package/dist/utils/commands.js.map +1 -0
  123. package/dist/utils/input.d.ts +13 -0
  124. package/dist/utils/input.d.ts.map +1 -0
  125. package/dist/utils/input.js +100 -0
  126. package/dist/utils/input.js.map +1 -0
  127. package/dist/utils/slug.d.ts +3 -0
  128. package/dist/utils/slug.d.ts.map +1 -0
  129. package/dist/utils/slug.js +58 -0
  130. package/dist/utils/slug.js.map +1 -0
  131. package/dist/utils/tmux.d.ts +4 -0
  132. package/dist/utils/tmux.d.ts.map +1 -0
  133. package/dist/utils/tmux.js +62 -0
  134. package/dist/utils/tmux.js.map +1 -0
  135. package/dist/workers/updateChecker.d.ts +2 -0
  136. package/dist/workers/updateChecker.d.ts.map +1 -0
  137. package/dist/workers/updateChecker.js +33 -0
  138. package/dist/workers/updateChecker.js.map +1 -0
  139. package/package.json +6 -2
package/dist/DmuxApp.js CHANGED
@@ -1,15 +1,39 @@
1
1
  import React, { useState, useEffect } from 'react';
2
2
  import { Box, Text, useInput, useApp } from 'ink';
3
- import CleanTextInput from './CleanTextInput.js';
4
- import StyledTextInput from './StyledTextInput.js';
5
3
  import { execSync } from 'child_process';
6
- import fs from 'fs/promises';
7
4
  import path from 'path';
8
5
  import { createRequire } from 'module';
6
+ // Hooks
7
+ import usePanes from './hooks/usePanes.js';
8
+ import useProjectSettings from './hooks/useProjectSettings.js';
9
+ import useTerminalWidth from './hooks/useTerminalWidth.js';
10
+ import useNavigation from './hooks/useNavigation.js';
11
+ import useAutoUpdater from './hooks/useAutoUpdater.js';
12
+ import useAgentDetection from './hooks/useAgentDetection.js';
13
+ import useAgentStatus from './hooks/useAgentStatus.js';
14
+ import useWorktreeActions from './hooks/useWorktreeActions.js';
15
+ import usePaneRunner from './hooks/usePaneRunner.js';
16
+ import usePaneCreation from './hooks/usePaneCreation.js';
17
+ // Utils
18
+ import { applySmartLayout } from './utils/tmux.js';
19
+ import { suggestCommand } from './utils/commands.js';
20
+ import { generateSlug } from './utils/slug.js';
9
21
  const require = createRequire(import.meta.url);
10
22
  const packageJson = require('../package.json');
11
- const DmuxApp = ({ dmuxDir, panesFile, projectName, sessionName, settingsFile, autoUpdater }) => {
12
- const [panes, setPanes] = useState([]);
23
+ import PanesGrid from './components/PanesGrid.js';
24
+ import NewPaneDialog from './components/NewPaneDialog.js';
25
+ import AgentChoiceDialog from './components/AgentChoiceDialog.js';
26
+ import CloseOptionsDialog from './components/CloseOptionsDialog.js';
27
+ import MergeConfirmationDialog from './components/MergeConfirmationDialog.js';
28
+ import CommandPromptDialog from './components/CommandPromptDialog.js';
29
+ import FileCopyPrompt from './components/FileCopyPrompt.js';
30
+ import LoadingIndicator from './components/LoadingIndicator.js';
31
+ import RunningIndicator from './components/RunningIndicator.js';
32
+ import UpdatingIndicator from './components/UpdatingIndicator.js';
33
+ import CreatingIndicator from './components/CreatingIndicator.js';
34
+ import FooterHelp from './components/FooterHelp.js';
35
+ const DmuxApp = ({ panesFile, projectName, sessionName, settingsFile, autoUpdater }) => {
36
+ /* panes state moved to usePanes */
13
37
  const [selectedIndex, setSelectedIndex] = useState(0);
14
38
  const [showNewPaneDialog, setShowNewPaneDialog] = useState(false);
15
39
  const [newPanePrompt, setNewPanePrompt] = useState('');
@@ -20,43 +44,56 @@ const DmuxApp = ({ dmuxDir, panesFile, projectName, sessionName, settingsFile, a
20
44
  const [selectedCloseOption, setSelectedCloseOption] = useState(0);
21
45
  const [closingPane, setClosingPane] = useState(null);
22
46
  const [isCreatingPane, setIsCreatingPane] = useState(false);
23
- const [projectSettings, setProjectSettings] = useState({});
47
+ const { projectSettings, saveSettings } = useProjectSettings(settingsFile);
24
48
  const [showCommandPrompt, setShowCommandPrompt] = useState(null);
25
49
  const [commandInput, setCommandInput] = useState('');
26
50
  const [showFileCopyPrompt, setShowFileCopyPrompt] = useState(false);
27
51
  const [currentCommandType, setCurrentCommandType] = useState(null);
28
52
  const [runningCommand, setRunningCommand] = useState(false);
29
- const [updateInfo, setUpdateInfo] = useState(null);
30
- const [showUpdateDialog, setShowUpdateDialog] = useState(false);
31
- const [isUpdating, setIsUpdating] = useState(false);
32
- const [isLoading, setIsLoading] = useState(true);
53
+ // Update state handled by hook
54
+ const { updateInfo, showUpdateDialog, isUpdating, performUpdate, skipUpdate, dismissUpdate, updateAvailable } = useAutoUpdater(autoUpdater, setStatusMessage);
33
55
  const { exit } = useApp();
34
56
  // Agent selection state
35
- const [availableAgents, setAvailableAgents] = useState([]);
57
+ const { availableAgents } = useAgentDetection();
36
58
  const [showAgentChoiceDialog, setShowAgentChoiceDialog] = useState(false);
37
59
  const [agentChoice, setAgentChoice] = useState(null);
38
60
  const [pendingPrompt, setPendingPrompt] = useState('');
39
61
  // Track terminal dimensions for responsive layout
40
- const [terminalWidth, setTerminalWidth] = useState(process.stdout.columns || 80);
62
+ const terminalWidth = useTerminalWidth();
63
+ // Panes state and persistence
64
+ const skipLoading = showNewPaneDialog || showMergeConfirmation || showCloseOptions ||
65
+ !!showCommandPrompt || showFileCopyPrompt;
66
+ const { panes, setPanes, isLoading, loadPanes, savePanes } = usePanes(panesFile, skipLoading);
67
+ // Worktree actions
68
+ const { closePane, mergeWorktree, mergeAndPrune, deleteUnsavedChanges, handleCloseOption } = useWorktreeActions({
69
+ panes,
70
+ savePanes,
71
+ setStatusMessage,
72
+ setShowMergeConfirmation,
73
+ setMergedPane,
74
+ });
75
+ // Pane runner
76
+ const { copyNonGitFiles, runCommandInternal, monitorTestOutput, monitorDevOutput, attachBackgroundWindow } = usePaneRunner({
77
+ panes,
78
+ savePanes,
79
+ projectSettings,
80
+ setStatusMessage,
81
+ setRunningCommand,
82
+ });
83
+ // Pane creation
84
+ const { openInEditor: openEditor2, createNewPane: createNewPaneHook } = usePaneCreation({
85
+ panes,
86
+ savePanes,
87
+ projectName,
88
+ setIsCreatingPane,
89
+ setStatusMessage,
90
+ setNewPanePrompt,
91
+ loadPanes,
92
+ });
41
93
  // Load panes and settings on mount and refresh periodically
42
94
  useEffect(() => {
43
- // Load immediately for initial data
44
- loadPanes();
45
- loadSettings();
46
- // Start polling after a short delay to avoid competing with startup
47
- let pollingInterval;
48
- const startPolling = setTimeout(() => {
49
- pollingInterval = setInterval(loadPanes, 3000); // Poll every 3 seconds instead of 2
50
- }, 1000); // Wait 1 second before starting polling
51
- // Handle terminal resize
52
- const handleResize = () => {
53
- setTerminalWidth(process.stdout.columns || 80);
54
- };
55
- // Add resize listener
56
- process.stdout.on('resize', handleResize);
57
95
  // Add cleanup handlers for process termination
58
96
  const handleTermination = () => {
59
- // Clear screen before exit
60
97
  process.stdout.write('\x1b[2J\x1b[H');
61
98
  process.stdout.write('\x1b[3J');
62
99
  try {
@@ -68,10 +105,6 @@ const DmuxApp = ({ dmuxDir, panesFile, projectName, sessionName, settingsFile, a
68
105
  process.on('SIGINT', handleTermination);
69
106
  process.on('SIGTERM', handleTermination);
70
107
  return () => {
71
- clearTimeout(startPolling);
72
- if (pollingInterval)
73
- clearInterval(pollingInterval);
74
- process.stdout.removeListener('resize', handleResize);
75
108
  process.removeListener('SIGINT', handleTermination);
76
109
  process.removeListener('SIGTERM', handleTermination);
77
110
  };
@@ -90,703 +123,35 @@ const DmuxApp = ({ dmuxDir, panesFile, projectName, sessionName, settingsFile, a
90
123
  !showCloseOptions &&
91
124
  !showCommandPrompt &&
92
125
  !showFileCopyPrompt &&
93
- !showUpdateDialog &&
94
126
  !showAgentChoiceDialog &&
95
127
  !isCreatingPane &&
96
128
  !runningCommand &&
97
129
  !isUpdating) {
98
130
  setShowNewPaneDialog(true);
99
131
  }
100
- }, [isLoading, panes.length, showNewPaneDialog, showMergeConfirmation, showCloseOptions, showCommandPrompt, showFileCopyPrompt, showUpdateDialog, showAgentChoiceDialog, isCreatingPane, runningCommand, isUpdating]);
101
- // Check for updates periodically
102
- useEffect(() => {
103
- if (!autoUpdater)
104
- return;
105
- const checkForUpdates = async () => {
106
- try {
107
- const info = await autoUpdater.checkForUpdates();
108
- if (await autoUpdater.shouldShowUpdateNotification(info)) {
109
- setUpdateInfo(info);
110
- setShowUpdateDialog(true);
111
- }
112
- }
113
- catch {
114
- // Silently ignore update check failures
115
- }
116
- };
117
- // Delay initial update check to avoid slowing startup
118
- const initialCheckTimer = setTimeout(() => {
119
- checkForUpdates();
120
- }, 3000); // Check after 3 seconds
121
- // Check for updates every 6 hours
122
- const updateInterval = setInterval(checkForUpdates, 6 * 60 * 60 * 1000);
123
- return () => {
124
- clearTimeout(initialCheckTimer);
125
- clearInterval(updateInterval);
126
- };
127
- }, [autoUpdater]);
128
- // Detect available agents on mount
132
+ }, [isLoading, panes.length, showNewPaneDialog, showMergeConfirmation, showCloseOptions, showCommandPrompt, showFileCopyPrompt, showAgentChoiceDialog, isCreatingPane, runningCommand, isUpdating]);
133
+ // Update checking moved to useAutoUpdater
134
+ // Set default agent choice when detection completes
129
135
  useEffect(() => {
130
- (async () => {
131
- try {
132
- const agents = [];
133
- const hasClaude = await findClaudeCommand();
134
- if (hasClaude)
135
- agents.push('claude');
136
- const hasopencode = await findopencodeCommand();
137
- if (hasopencode)
138
- agents.push('opencode');
139
- setAvailableAgents(agents);
140
- setAgentChoice(agents[0] || 'claude');
141
- }
142
- catch { }
143
- })();
144
- }, []);
145
- // Monitor Claude status in all panes with proper dependency tracking
146
- useEffect(() => {
147
- if (panes.length === 0)
148
- return;
149
- // Defer Claude monitoring to avoid slowing down startup and dialog interactions
150
- const startupDelay = setTimeout(() => {
151
- const monitorAgentStatus = async () => {
152
- // Skip monitoring if any dialog is open to avoid UI freezing
153
- if (showNewPaneDialog || showMergeConfirmation || showCloseOptions ||
154
- showCommandPrompt || showFileCopyPrompt || showUpdateDialog) {
155
- return;
156
- }
157
- // Monitor agent status for all panes
158
- const updatedPanesWithNulls = await Promise.all(panes.map(async (pane) => {
159
- try {
160
- // Skip if recently checked (within 500ms to avoid overlapping checks)
161
- if (pane.lastAgentCheck && Date.now() - pane.lastAgentCheck < 500) {
162
- return pane;
163
- }
164
- // First check if pane exists before trying to capture
165
- let paneExists = false;
166
- try {
167
- const paneIds = execSync(`tmux list-panes -F '#{pane_id}'`, {
168
- encoding: 'utf-8',
169
- stdio: 'pipe',
170
- timeout: 500 // Quick timeout
171
- }).trim().split('\n').filter(id => id && id.startsWith('%'));
172
- paneExists = paneIds.includes(pane.paneId);
173
- }
174
- catch {
175
- // If we can't check, assume pane exists to avoid false removals
176
- paneExists = true;
177
- }
178
- if (!paneExists) {
179
- // Pane doesn't exist anymore, mark for removal
180
- return null;
181
- }
182
- // Capture the last 30 lines of the pane for better detection
183
- const captureOutput = execSync(`tmux capture-pane -t '${pane.paneId}' -p -S -30`, { encoding: 'utf-8', stdio: 'pipe' });
184
- const lines = captureOutput.split('\n');
185
- const lastLines = lines.slice(-10).join('\n');
186
- // Determine working patterns based on agent
187
- let isWorking = false;
188
- if (pane.agent === 'opencode') {
189
- const workingPatterns = [
190
- /esc\s+(to\s+)?interrupt/i,
191
- /working(\.|\.{2}|\.{3})/i,
192
- ];
193
- isWorking = workingPatterns.some(pattern => pattern.test(lastLines));
194
- }
195
- else {
196
- // Default to Claude patterns
197
- const workingPatterns = [
198
- /esc to interrupt/i,
199
- ];
200
- isWorking = workingPatterns.some(pattern => pattern.test(captureOutput));
201
- }
202
- // Attention patterns - generic prompts needing user input
203
- const attentionPatterns = [
204
- /\?\s*$/m,
205
- /y\/n/i,
206
- /yes.*no/i,
207
- /\ballow\b.*\?/i,
208
- /\bapprove\b.*\?/i,
209
- /\bgrant\b.*\?/i,
210
- /\btrust\b.*\?/i,
211
- /\baccept\b.*\?/i,
212
- /\bcontinue\b.*\?/i,
213
- /\bproceed\b.*\?/i,
214
- /permission/i,
215
- /confirmation/i,
216
- /press.*enter/i,
217
- /waiting for/i,
218
- /are you sure/i,
219
- /would you like/i,
220
- /do you want/i,
221
- /please confirm/i,
222
- /requires.*approval/i,
223
- /needs.*input/i,
224
- /⏵⏵\s*accept edits/i,
225
- /shift\+tab to cycle/i,
226
- ];
227
- // Claude-specific input box detection
228
- const hasClaudeInputBox = /╭─+╮/.test(lastLines) && /╰─+╯/.test(lastLines) && /│\s+>\s+.*│/.test(lastLines);
229
- const needsAttention = attentionPatterns.some(pattern => pattern.test(captureOutput)) || (pane.agent !== 'opencode' && hasClaudeInputBox);
230
- // Determine status - working takes precedence
231
- let newStatus = 'idle';
232
- if (isWorking) {
233
- newStatus = 'working';
234
- }
235
- else if (needsAttention && !isWorking) {
236
- newStatus = 'waiting';
237
- }
238
- // Additional Claude-specific checks
239
- if (pane.agent !== 'opencode') {
240
- if (/accept edits/i.test(captureOutput) && !/esc to interrupt/i.test(captureOutput)) {
241
- newStatus = 'waiting';
242
- }
243
- if (hasClaudeInputBox && !isWorking) {
244
- newStatus = 'waiting';
245
- }
246
- const claudeQuestionPatterns = [
247
- /I (can|could|should|would|will|may|might)/i,
248
- /Let me know/i,
249
- /Please (tell|let|inform|advise)/i,
250
- /Would you prefer/i,
251
- /Should I (proceed|continue|go ahead)/i,
252
- ];
253
- if (claudeQuestionPatterns.some(pattern => pattern.test(lastLines)) && !isWorking) {
254
- newStatus = 'waiting';
255
- }
256
- }
257
- // Return updated pane if status changed
258
- if (pane.agentStatus !== newStatus) {
259
- return {
260
- ...pane,
261
- agentStatus: newStatus,
262
- lastAgentCheck: Date.now()
263
- };
264
- }
265
- // Just update timestamp
266
- return {
267
- ...pane,
268
- lastAgentCheck: Date.now()
269
- };
270
- }
271
- catch (error) {
272
- // If we can't capture the pane, it might be dead - mark it for removal
273
- return null; // Will be filtered out below
274
- }
275
- }));
276
- // Filter out null values (dead panes) and keep only valid panes
277
- const updatedPanes = updatedPanesWithNulls.filter((pane) => pane !== null);
278
- // Check if panes were removed
279
- const panesRemoved = updatedPanes.length < panes.length;
280
- if (panesRemoved) {
281
- // Save the updated list with removed panes
282
- await fs.writeFile(panesFile, JSON.stringify(updatedPanes, null, 2));
283
- // Reload to sync state properly
284
- await loadPanes();
285
- }
286
- else {
287
- // Only update status if no structural changes
288
- const hasStatusChanges = updatedPanes.some((pane, index) => pane.agentStatus !== panes[index]?.agentStatus);
289
- if (hasStatusChanges) {
290
- // Update state with new status but don't save to file
291
- // Agent status is ephemeral and shouldn't persist
292
- setPanes(updatedPanes);
293
- }
294
- }
295
- };
296
- // Run monitoring after delay
297
- monitorAgentStatus();
298
- // Set up interval for continuous monitoring (less frequent for better performance)
299
- const agentInterval = setInterval(monitorAgentStatus, 2000); // Check every 2 seconds
300
- return () => {
301
- clearInterval(agentInterval);
302
- };
303
- }, 500); // Wait 500ms before starting monitoring
304
- return () => {
305
- clearTimeout(startupDelay);
306
- };
307
- }, [panes, panesFile, showNewPaneDialog, showMergeConfirmation, showCloseOptions,
308
- showCommandPrompt, showFileCopyPrompt, showUpdateDialog]); // Re-run when panes or dialogs change
309
- const loadSettings = async () => {
310
- try {
311
- const content = await fs.readFile(settingsFile, 'utf-8');
312
- const settings = JSON.parse(content);
313
- setProjectSettings(settings);
314
- }
315
- catch {
316
- // Settings file doesn't exist yet, that's ok
317
- setProjectSettings({});
318
- }
319
- };
320
- const saveSettings = async (settings) => {
321
- await fs.writeFile(settingsFile, JSON.stringify(settings, null, 2));
322
- setProjectSettings(settings);
323
- };
324
- const loadPanes = async () => {
325
- // Skip loading if any dialog is open to avoid UI freezing
326
- if (showNewPaneDialog || showMergeConfirmation || showCloseOptions ||
327
- showCommandPrompt || showFileCopyPrompt || showUpdateDialog) {
328
- return;
329
- }
330
- if (isLoading) {
331
- // Don't set loading to false immediately - keep it true for initial load
332
- }
333
- try {
334
- const content = await fs.readFile(panesFile, 'utf-8');
335
- const loadedPanes = JSON.parse(content);
336
- // Get all pane IDs with retries for stability
337
- let allPaneIds = [];
338
- let retryCount = 0;
339
- const maxRetries = 2;
340
- while (retryCount <= maxRetries) {
341
- try {
342
- const output = execSync(`tmux list-panes -F '#{pane_id}'`, {
343
- encoding: 'utf-8',
344
- stdio: 'pipe',
345
- timeout: 1000 // 1 second timeout
346
- });
347
- allPaneIds = output.trim().split('\n').filter(id => id && id.startsWith('%'));
348
- // If we got results, break out of retry loop
349
- if (allPaneIds.length > 0 || retryCount === maxRetries) {
350
- break;
351
- }
352
- }
353
- catch (error) {
354
- // On error, wait a bit before retry
355
- if (retryCount < maxRetries) {
356
- await new Promise(resolve => setTimeout(resolve, 100));
357
- }
358
- }
359
- retryCount++;
360
- }
361
- // Filter out dead panes - but keep panes from file as source of truth
362
- // Only remove panes that definitively don't exist
363
- const activePanes = loadedPanes.filter(pane => {
364
- // If we couldn't get pane list, keep all panes (safer)
365
- if (allPaneIds.length === 0) {
366
- return true;
367
- }
368
- return allPaneIds.includes(pane.paneId);
369
- });
370
- // Only update state if there's an actual change to avoid re-renders
371
- const currentPaneIds = panes.map(p => p.paneId).sort().join(',');
372
- const newPaneIds = activePanes.map(p => p.paneId).sort().join(',');
373
- if (currentPaneIds !== newPaneIds || panes.length === 0) {
374
- // Batch update all pane titles in one go (only if needed)
375
- if (activePanes.length > 0) {
376
- // Update titles for all active panes at once
377
- activePanes.forEach(pane => {
378
- try {
379
- execSync(`tmux select-pane -t '${pane.paneId}' -T "${pane.slug}"`, { stdio: 'pipe' });
380
- }
381
- catch {
382
- // Ignore if setting title fails
383
- }
384
- });
385
- }
386
- setPanes(activePanes);
387
- // Save cleaned list
388
- if (activePanes.length !== loadedPanes.length) {
389
- await fs.writeFile(panesFile, JSON.stringify(activePanes, null, 2));
390
- }
391
- }
392
- // Set loading to false after first load
393
- if (isLoading) {
394
- setIsLoading(false);
395
- }
396
- }
397
- catch {
398
- // Don't clear panes on error - keep current state
399
- // Set loading to false even on error
400
- if (isLoading) {
401
- setIsLoading(false);
402
- }
403
- }
404
- };
405
- const getPanePositions = () => {
406
- try {
407
- const output = execSync(`tmux list-panes -F '#{pane_id} #{pane_left} #{pane_top} #{pane_width} #{pane_height}'`, { encoding: 'utf-8', stdio: 'pipe' }).trim();
408
- return output.split('\n').map(line => {
409
- const [paneId, left, top, width, height] = line.split(' ');
410
- return {
411
- paneId,
412
- left: parseInt(left),
413
- top: parseInt(top),
414
- width: parseInt(width),
415
- height: parseInt(height)
416
- };
417
- });
418
- }
419
- catch {
420
- return [];
421
- }
422
- };
423
- // Calculate the visual grid position of cards based on their index
424
- const getCardGridPosition = (index) => {
425
- // Cards are displayed in a flexbox with wrapping
426
- // With card width of 35 and typical terminal width of 80-120, we get 2-3 cards per row
427
- const cardWidth = 35 + 2; // Card width plus gap
428
- const cardsPerRow = Math.max(1, Math.floor(terminalWidth / cardWidth));
429
- const row = Math.floor(index / cardsPerRow);
430
- const col = index % cardsPerRow;
431
- return { row, col };
432
- };
433
- const findCardInDirection = (currentIndex, direction) => {
434
- const totalItems = panes.length + (isLoading ? 0 : 1); // +1 for "New dmux pane" button when not loading
435
- const currentPos = getCardGridPosition(currentIndex);
436
- // Calculate cards per row based on current terminal width
437
- const cardWidth = 35 + 2; // Card width plus gap
438
- const cardsPerRow = Math.max(1, Math.floor(terminalWidth / cardWidth));
439
- let targetIndex = null;
440
- switch (direction) {
441
- case 'up':
442
- // Move up one row, same column
443
- if (currentPos.row > 0) {
444
- targetIndex = (currentPos.row - 1) * cardsPerRow + currentPos.col;
445
- // Make sure target exists
446
- if (targetIndex >= totalItems) {
447
- // Try to find the last item in the row above
448
- targetIndex = Math.min((currentPos.row - 1) * cardsPerRow + cardsPerRow - 1, totalItems - 1);
449
- }
450
- }
451
- break;
452
- case 'down':
453
- // Move down one row, same column
454
- targetIndex = (currentPos.row + 1) * cardsPerRow + currentPos.col;
455
- if (targetIndex >= totalItems) {
456
- // If moving down from last row, try to go to "New dmux pane" if not already there
457
- if (currentIndex < totalItems - 1) {
458
- targetIndex = totalItems - 1;
459
- }
460
- else {
461
- targetIndex = null;
462
- }
463
- }
464
- break;
465
- case 'left':
466
- // Move left one column, same row
467
- if (currentPos.col > 0) {
468
- targetIndex = currentIndex - 1;
469
- }
470
- else if (currentPos.row > 0) {
471
- // Wrap to end of previous row
472
- targetIndex = currentPos.row * cardsPerRow - 1;
473
- if (targetIndex >= totalItems) {
474
- targetIndex = totalItems - 1;
475
- }
476
- }
477
- break;
478
- case 'right':
479
- // Move right one column, same row
480
- if (currentPos.col < cardsPerRow - 1 && currentIndex < totalItems - 1) {
481
- targetIndex = currentIndex + 1;
482
- }
483
- else if ((currentPos.row + 1) * cardsPerRow < totalItems) {
484
- // Wrap to start of next row
485
- targetIndex = (currentPos.row + 1) * cardsPerRow;
486
- }
487
- break;
488
- }
489
- // Validate target index
490
- if (targetIndex !== null && targetIndex >= 0 && targetIndex < totalItems) {
491
- return targetIndex;
492
- }
493
- return null;
494
- };
495
- const findPaneInDirection = (currentPane, direction) => {
496
- const positions = getPanePositions();
497
- const currentPos = positions.find(p => p.paneId === currentPane.paneId);
498
- if (!currentPos)
499
- return null;
500
- // Calculate center point of current pane
501
- const currentCenterX = currentPos.left + currentPos.width / 2;
502
- const currentCenterY = currentPos.top + currentPos.height / 2;
503
- // Filter panes based on direction
504
- const candidatePanes = panes.filter(pane => {
505
- if (pane.paneId === currentPane.paneId)
506
- return false;
507
- const pos = positions.find(p => p.paneId === pane.paneId);
508
- if (!pos)
509
- return false;
510
- const centerX = pos.left + pos.width / 2;
511
- const centerY = pos.top + pos.height / 2;
512
- switch (direction) {
513
- case 'up':
514
- return centerY < currentCenterY;
515
- case 'down':
516
- return centerY > currentCenterY;
517
- case 'left':
518
- return centerX < currentCenterX;
519
- case 'right':
520
- return centerX > currentCenterX;
521
- default:
522
- return false;
523
- }
524
- });
525
- if (candidatePanes.length === 0)
526
- return null;
527
- // Find the closest pane in the given direction
528
- let closestPane = candidatePanes[0];
529
- let minDistance = Infinity;
530
- for (const pane of candidatePanes) {
531
- const pos = positions.find(p => p.paneId === pane.paneId);
532
- if (!pos)
533
- continue;
534
- const centerX = pos.left + pos.width / 2;
535
- const centerY = pos.top + pos.height / 2;
536
- let distance;
537
- switch (direction) {
538
- case 'up':
539
- case 'down':
540
- // For vertical movement, prioritize vertical alignment, then distance
541
- const xDiff = Math.abs(centerX - currentCenterX);
542
- const yDiff = Math.abs(centerY - currentCenterY);
543
- // Weight horizontal difference less than vertical
544
- distance = yDiff + xDiff * 0.3;
545
- break;
546
- case 'left':
547
- case 'right':
548
- // For horizontal movement, prioritize horizontal alignment, then distance
549
- const xDiff2 = Math.abs(centerX - currentCenterX);
550
- const yDiff2 = Math.abs(centerY - currentCenterY);
551
- // Weight vertical difference less than horizontal
552
- distance = xDiff2 + yDiff2 * 0.3;
553
- break;
554
- }
555
- if (distance < minDistance) {
556
- minDistance = distance;
557
- closestPane = pane;
558
- }
559
- }
560
- return closestPane;
561
- };
562
- const savePanes = async (newPanes) => {
563
- // Filter out any panes that no longer exist in tmux before saving
564
- let activePanes = newPanes;
565
- try {
566
- const paneIds = execSync(`tmux list-panes -F '#{pane_id}'`, {
567
- encoding: 'utf-8',
568
- stdio: 'pipe',
569
- timeout: 1000
570
- }).trim().split('\n').filter(id => id && id.startsWith('%'));
571
- // Only keep panes that still exist
572
- activePanes = newPanes.filter(pane => paneIds.includes(pane.paneId));
573
- }
574
- catch {
575
- // If we can't verify, save all panes
576
- activePanes = newPanes;
577
- }
578
- await fs.writeFile(panesFile, JSON.stringify(activePanes, null, 2));
579
- setPanes(activePanes);
580
- };
581
- const applySmartLayout = (paneCount) => {
582
- try {
583
- // Progressive layout strategy based on pane count
584
- if (paneCount <= 2) {
585
- // 2 panes: side by side
586
- execSync('tmux select-layout even-horizontal', { stdio: 'pipe' });
587
- }
588
- else if (paneCount === 3) {
589
- // 3 panes: primary top, two bottom
590
- execSync('tmux select-layout main-horizontal', { stdio: 'pipe' });
591
- }
592
- else if (paneCount === 4) {
593
- // 4 panes: 2x2 grid
594
- execSync('tmux select-layout tiled', { stdio: 'pipe' });
595
- }
596
- else if (paneCount === 5) {
597
- // 5 panes: 2 top, 3 bottom
598
- execSync('tmux select-layout tiled', { stdio: 'pipe' });
599
- // Custom adjustment for better 2-over-3 layout
600
- try {
601
- execSync('tmux resize-pane -t 0 -y 50%', { stdio: 'pipe' });
602
- }
603
- catch { }
604
- }
605
- else if (paneCount === 6) {
606
- // 6 panes: 3x2 grid
607
- execSync('tmux select-layout tiled', { stdio: 'pipe' });
608
- }
609
- else if (paneCount <= 9) {
610
- // 7-9 panes: 3x3 grid
611
- execSync('tmux select-layout tiled', { stdio: 'pipe' });
612
- }
613
- else if (paneCount <= 12) {
614
- // 10-12 panes: 3x4 grid
615
- execSync('tmux select-layout tiled', { stdio: 'pipe' });
616
- }
617
- else if (paneCount <= 16) {
618
- // 13-16 panes: 4x4 grid
619
- execSync('tmux select-layout tiled', { stdio: 'pipe' });
620
- }
621
- else {
622
- // More than 16: use tiled for best automatic arrangement
623
- execSync('tmux select-layout tiled', { stdio: 'pipe' });
624
- }
625
- // Refresh client to ensure layout is applied
626
- execSync('tmux refresh-client', { stdio: 'pipe' });
627
- }
628
- catch (error) {
629
- // Fallback to even-horizontal if custom layout fails
630
- try {
631
- execSync('tmux select-layout even-horizontal', { stdio: 'pipe' });
632
- }
633
- catch { }
634
- }
635
- };
636
- const findClaudeCommand = async () => {
637
- // Prefer passive detection to avoid triggering installers
638
- try {
639
- const userShell = process.env.SHELL || '/bin/bash';
640
- const result = execSync(`${userShell} -i -c "command -v claude 2>/dev/null || which claude 2>/dev/null"`, { encoding: 'utf-8', stdio: 'pipe' }).trim();
641
- if (result)
642
- return result.split('\n')[0];
643
- }
644
- catch { }
645
- // Check common installation paths without executing
646
- const commonPaths = [
647
- `${process.env.HOME}/.claude/local/claude`,
648
- `${process.env.HOME}/.local/bin/claude`,
649
- '/usr/local/bin/claude',
650
- '/opt/homebrew/bin/claude',
651
- '/usr/bin/claude',
652
- `${process.env.HOME}/bin/claude`,
653
- ];
654
- for (const p of commonPaths) {
655
- try {
656
- await fs.access(p);
657
- return p;
658
- }
659
- catch { }
660
- }
661
- return null;
662
- };
663
- const findopencodeCommand = async () => {
664
- // Passive detection only (no execution)
665
- try {
666
- const userShell = process.env.SHELL || '/bin/bash';
667
- const result = execSync(`${userShell} -i -c "command -v opencode 2>/dev/null || which opencode 2>/dev/null"`, { encoding: 'utf-8', stdio: 'pipe' }).trim();
668
- if (result)
669
- return result.split('\n')[0];
670
- }
671
- catch { }
672
- const commonPaths = [
673
- '/opt/homebrew/bin/opencode',
674
- '/usr/local/bin/opencode',
675
- `${process.env.HOME}/.local/bin/opencode`,
676
- `${process.env.HOME}/bin/opencode`,
677
- ];
678
- for (const p of commonPaths) {
679
- try {
680
- await fs.access(p);
681
- return p;
682
- }
683
- catch { }
684
- }
685
- return null;
686
- };
687
- const callClaudeCode = async (prompt) => {
688
- try {
689
- // Use a simpler approach: pipe the prompt via stdin and capture output
690
- const result = execSync(`echo "${prompt.replace(/"/g, '\\"')}" | claude --no-interactive --max-turns 1 2>/dev/null | head -n 5`, {
691
- encoding: 'utf-8',
692
- stdio: 'pipe',
693
- timeout: 5000 // 5 second timeout
694
- });
695
- // Extract just the content (first few lines should have the response)
696
- const lines = result.trim().split('\n');
697
- const response = lines.join(' ').trim();
698
- return response || null;
699
- }
700
- catch (error) {
701
- // Claude not available or error occurred
702
- return null;
703
- }
704
- };
705
- const generateSlug = async (prompt) => {
706
- if (!prompt) {
707
- return `dmux-${Date.now()}`;
708
- }
709
- // Try OpenRouter first if API key is available
710
- const apiKey = process.env.OPENROUTER_API_KEY;
711
- if (apiKey) {
712
- try {
713
- const response = await fetch('https://openrouter.ai/api/v1/chat/completions', {
714
- method: 'POST',
715
- headers: {
716
- 'Content-Type': 'application/json',
717
- 'Authorization': `Bearer ${apiKey}`,
718
- },
719
- body: JSON.stringify({
720
- model: 'openai/gpt-4o-mini',
721
- messages: [
722
- {
723
- role: 'user',
724
- content: `Generate a 1-2 word kebab-case slug for this prompt. Only respond with the slug, nothing else: "${prompt}"`
725
- }
726
- ],
727
- max_tokens: 10,
728
- temperature: 0.3
729
- })
730
- });
731
- if (response.ok) {
732
- const data = await response.json();
733
- const slug = data.choices[0].message.content.trim().toLowerCase().replace(/[^a-z0-9-]/g, '');
734
- if (slug)
735
- return slug;
736
- }
737
- }
738
- catch {
739
- // Fall through to Claude Code
740
- }
741
- }
742
- // Try Claude Code as fallback
743
- const claudeResponse = await callClaudeCode(`Generate a 1-2 word kebab-case slug for this prompt. Only respond with the slug, nothing else: "${prompt}"`);
744
- if (claudeResponse) {
745
- const slug = claudeResponse.trim().toLowerCase().replace(/[^a-z0-9-]/g, '');
746
- if (slug)
747
- return slug;
748
- }
749
- // Final fallback
750
- return `dmux-${Date.now()}`;
751
- };
752
- const openInEditor = async () => {
753
- try {
754
- const os = require('os');
755
- const fs = require('fs');
756
- const tmpFile = path.join(os.tmpdir(), `dmux-prompt-${Date.now()}.md`);
757
- // Write current prompt to temp file
758
- fs.writeFileSync(tmpFile, newPanePrompt || '# Enter your Claude prompt here\n\n');
759
- // Get editor from environment or use default
760
- const editor = process.env.EDITOR || process.env.VISUAL || 'nano';
761
- // Clear screen and open editor
762
- process.stdout.write('\x1b[2J\x1b[H');
763
- // Use spawn to open editor in foreground
764
- const { spawn } = require('child_process');
765
- const editorProcess = spawn(editor, [tmpFile], {
766
- stdio: 'inherit',
767
- shell: true
768
- });
769
- editorProcess.on('close', (code) => {
770
- // Read the file back
771
- try {
772
- const content = fs.readFileSync(tmpFile, 'utf8')
773
- .replace(/^# Enter your Claude prompt here\s*\n*/m, '')
774
- .trim();
775
- setNewPanePrompt(content);
776
- // Clean up temp file
777
- fs.unlinkSync(tmpFile);
778
- // Clear screen and return to dmux
779
- process.stdout.write('\x1b[2J\x1b[H');
780
- }
781
- catch (error) {
782
- // If file read fails, just continue
783
- }
784
- });
785
- }
786
- catch (error) {
787
- // If editor fails, just continue with inline input
788
- }
789
- };
136
+ if (agentChoice == null && availableAgents.length > 0) {
137
+ setAgentChoice(availableAgents[0] || 'claude');
138
+ }
139
+ }, [availableAgents]);
140
+ // Monitor agent status across panes
141
+ useAgentStatus({
142
+ panes,
143
+ setPanes,
144
+ panesFile,
145
+ suspend: showNewPaneDialog || showMergeConfirmation || showCloseOptions || !!showCommandPrompt || showFileCopyPrompt,
146
+ loadPanes,
147
+ });
148
+ // loadPanes moved to usePanes
149
+ // getPanePositions moved to utils/tmux
150
+ // Navigation logic moved to hook
151
+ const { getCardGridPosition, findCardInDirection } = useNavigation(terminalWidth, panes.length, isLoading);
152
+ // findCardInDirection provided by useNavigation
153
+ // savePanes moved to usePanes
154
+ // applySmartLayout moved to utils/tmux
790
155
  const createNewPane = async (prompt, agent) => {
791
156
  setIsCreatingPane(true);
792
157
  setStatusMessage('Generating slug...');
@@ -804,8 +169,8 @@ const DmuxApp = ({ dmuxDir, panesFile, projectName, sessionName, settingsFile, a
804
169
  // Fallback to current directory if not in a git repo
805
170
  projectRoot = process.cwd();
806
171
  }
807
- // Create worktree path relative to project root
808
- const worktreePath = path.join(path.dirname(projectRoot), `${path.basename(projectRoot)}-${slug}`);
172
+ // Create worktree path relative to project root with dmux- prefix
173
+ const worktreePath = path.join(path.dirname(projectRoot), `dmux-${slug}`);
809
174
  // Get the original pane ID (where dmux is running) before clearing
810
175
  const originalPaneId = execSync('tmux display-message -p "#{pane_id}"', { encoding: 'utf-8' }).trim();
811
176
  // Multiple clearing strategies to prevent artifacts
@@ -899,11 +264,12 @@ const DmuxApp = ({ dmuxDir, panesFile, projectName, sessionName, settingsFile, a
899
264
  execSync(`tmux send-keys -t '${paneInfo}' '${escapedOpenCmd}'`, { stdio: 'pipe' });
900
265
  execSync(`tmux send-keys -t '${paneInfo}' Enter`, { stdio: 'pipe' });
901
266
  if (prompt && prompt.trim()) {
902
- await new Promise(resolve => setTimeout(resolve, 800));
267
+ await new Promise(resolve => setTimeout(resolve, 1500));
903
268
  const bufName = `dmux_prompt_${Date.now()}`;
904
269
  const promptEsc = prompt.replace(/\\/g, '\\\\').replace(/'/g, "'\\''");
905
270
  execSync(`tmux set-buffer -b '${bufName}' -- '${promptEsc}'`, { stdio: 'pipe' });
906
- execSync(`tmux paste-buffer -b '${bufName}' -t '${paneInfo}' -p`, { stdio: 'pipe' });
271
+ execSync(`tmux paste-buffer -b '${bufName}' -t '${paneInfo}'`, { stdio: 'pipe' });
272
+ await new Promise(resolve => setTimeout(resolve, 200));
907
273
  execSync(`tmux delete-buffer -b '${bufName}'`, { stdio: 'pipe' });
908
274
  execSync(`tmux send-keys -t '${paneInfo}' Enter`, { stdio: 'pipe' });
909
275
  }
@@ -1072,354 +438,6 @@ const DmuxApp = ({ dmuxDir, panesFile, projectName, sessionName, settingsFile, a
1072
438
  setTimeout(() => setStatusMessage(''), 2000);
1073
439
  }
1074
440
  };
1075
- const closePane = async (pane) => {
1076
- try {
1077
- // Kill associated test/dev windows if they exist
1078
- if (pane.testWindowId) {
1079
- try {
1080
- execSync(`tmux kill-window -t '${pane.testWindowId}'`, { stdio: 'pipe' });
1081
- }
1082
- catch { }
1083
- }
1084
- if (pane.devWindowId) {
1085
- try {
1086
- execSync(`tmux kill-window -t '${pane.devWindowId}'`, { stdio: 'pipe' });
1087
- }
1088
- catch { }
1089
- }
1090
- // Multiple clearing strategies to prevent artifacts
1091
- // 1. Clear screen with ANSI codes
1092
- process.stdout.write('\x1b[2J\x1b[H');
1093
- // 2. Fill with blank lines to push content off screen
1094
- process.stdout.write('\n'.repeat(100));
1095
- // 3. Clear tmux history and send clear command
1096
- try {
1097
- execSync('tmux clear-history', { stdio: 'pipe' });
1098
- execSync('tmux send-keys C-l', { stdio: 'pipe' });
1099
- }
1100
- catch { }
1101
- // 4. Force tmux to refresh the display
1102
- try {
1103
- execSync('tmux refresh-client', { stdio: 'pipe' });
1104
- }
1105
- catch { }
1106
- // Small delay to let clearing complete
1107
- await new Promise(resolve => setTimeout(resolve, 100));
1108
- // Kill the tmux pane
1109
- execSync(`tmux kill-pane -t '${pane.paneId}'`, { stdio: 'pipe' });
1110
- // Get current pane count to determine layout
1111
- const paneCount = parseInt(execSync('tmux list-panes | wc -l', { encoding: 'utf-8' }).trim());
1112
- // Apply smart layout after pane removal
1113
- if (paneCount > 1) {
1114
- applySmartLayout(paneCount);
1115
- }
1116
- // Remove from list
1117
- const updatedPanes = panes.filter(p => p.id !== pane.id);
1118
- await savePanes(updatedPanes);
1119
- setStatusMessage(`Closed pane: ${pane.slug}`);
1120
- setTimeout(() => setStatusMessage(''), 3000);
1121
- }
1122
- catch {
1123
- setStatusMessage('Failed to close pane');
1124
- setTimeout(() => setStatusMessage(''), 2000);
1125
- }
1126
- };
1127
- const mergeAndPrune = async (pane) => {
1128
- if (!pane.worktreePath) {
1129
- setStatusMessage('No worktree to merge');
1130
- setTimeout(() => setStatusMessage(''), 2000);
1131
- return;
1132
- }
1133
- try {
1134
- setStatusMessage('Checking worktree status...');
1135
- // Get current branch
1136
- const mainBranch = execSync('git branch --show-current', { encoding: 'utf-8' }).trim();
1137
- // Check for uncommitted changes in the worktree
1138
- const statusOutput = execSync(`git -C "${pane.worktreePath}" status --porcelain`, { encoding: 'utf-8' });
1139
- if (statusOutput.trim()) {
1140
- setStatusMessage('Generating commit message...');
1141
- // Get the diff for uncommitted changes
1142
- const diffOutput = execSync(`git -C "${pane.worktreePath}" diff HEAD`, { encoding: 'utf-8' });
1143
- const statusDetails = execSync(`git -C "${pane.worktreePath}" status`, { encoding: 'utf-8' });
1144
- // Generate commit message using LLM
1145
- const commitMessage = await generateCommitMessage(`${statusDetails}\n\n${diffOutput}`);
1146
- setStatusMessage('Committing changes...');
1147
- // Stage all changes and commit with generated message
1148
- execSync(`git -C "${pane.worktreePath}" add -A`, { stdio: 'pipe' });
1149
- // Escape the commit message for shell
1150
- const escapedMessage = commitMessage.replace(/'/g, "'\\''");
1151
- execSync(`git -C "${pane.worktreePath}" commit -m '${escapedMessage}'`, { stdio: 'pipe' });
1152
- }
1153
- setStatusMessage('Merging into main...');
1154
- // Try to merge the worktree branch
1155
- try {
1156
- execSync(`git merge ${pane.slug}`, { stdio: 'pipe' });
1157
- }
1158
- catch (mergeError) {
1159
- // Check if this is a merge conflict
1160
- const errorMessage = mergeError.message || mergeError.toString();
1161
- if (errorMessage.includes('CONFLICT') || errorMessage.includes('conflict')) {
1162
- // Merge conflict detected - exit dmux and inform user
1163
- // Merge conflict detected - exit dmux and inform user
1164
- process.stderr.write('\n\x1b[31m✗ Merge conflict detected!\x1b[0m\n');
1165
- process.stderr.write(`\nThere are merge conflicts when merging branch '${pane.slug}' into '${mainBranch}'.\n`);
1166
- process.stderr.write('\nTo resolve:\n');
1167
- process.stderr.write('1. Manually resolve the merge conflicts in your editor\n');
1168
- process.stderr.write('2. Stage the resolved files: git add <resolved-files>\n');
1169
- process.stderr.write('3. Complete the merge: git commit\n');
1170
- process.stderr.write('4. Run dmux again to continue managing your panes\n');
1171
- process.stderr.write('\nExiting dmux now...\n\n');
1172
- // Clean exit
1173
- process.stdout.write('\x1b[2J\x1b[H');
1174
- process.stdout.write('\x1b[3J');
1175
- try {
1176
- execSync('tmux clear-history', { stdio: 'pipe' });
1177
- }
1178
- catch { }
1179
- process.exit(1);
1180
- }
1181
- else {
1182
- // Some other merge error
1183
- throw mergeError;
1184
- }
1185
- }
1186
- // Remove worktree
1187
- execSync(`git worktree remove "${pane.worktreePath}"`, { stdio: 'pipe' });
1188
- // Delete branch
1189
- execSync(`git branch -d ${pane.slug}`, { stdio: 'pipe' });
1190
- // Close the pane (includes clearing)
1191
- await closePane(pane);
1192
- setStatusMessage(`Merged ${pane.slug} into ${mainBranch} and closed pane`);
1193
- setTimeout(() => setStatusMessage(''), 3000);
1194
- }
1195
- catch (error) {
1196
- setStatusMessage('Failed to merge - check git status');
1197
- setTimeout(() => setStatusMessage(''), 3000);
1198
- }
1199
- };
1200
- const deleteUnsavedChanges = async (pane) => {
1201
- if (!pane.worktreePath) {
1202
- // No worktree, just close the pane (includes clearing)
1203
- await closePane(pane);
1204
- return;
1205
- }
1206
- try {
1207
- setStatusMessage('Removing worktree with unsaved changes...');
1208
- // Force remove worktree (discards uncommitted changes)
1209
- execSync(`git worktree remove --force "${pane.worktreePath}"`, { stdio: 'pipe' });
1210
- // Delete branch
1211
- try {
1212
- execSync(`git branch -D ${pane.slug}`, { stdio: 'pipe' });
1213
- }
1214
- catch {
1215
- // Branch might not exist or have commits, that's ok
1216
- }
1217
- // Close the pane (includes clearing)
1218
- await closePane(pane);
1219
- setStatusMessage(`Deleted worktree ${pane.slug} and closed pane`);
1220
- setTimeout(() => setStatusMessage(''), 3000);
1221
- }
1222
- catch (error) {
1223
- setStatusMessage('Failed to delete worktree');
1224
- setTimeout(() => setStatusMessage(''), 3000);
1225
- }
1226
- };
1227
- const handleCloseOption = async (option, pane) => {
1228
- setShowCloseOptions(false);
1229
- setClosingPane(null);
1230
- setSelectedCloseOption(0);
1231
- switch (option) {
1232
- case 0: // Merge & Prune
1233
- await mergeAndPrune(pane);
1234
- break;
1235
- case 1: // Merge Only
1236
- await mergeWorktree(pane);
1237
- break;
1238
- case 2: // Delete Unsaved Changes
1239
- await deleteUnsavedChanges(pane);
1240
- break;
1241
- case 3: // Just Close
1242
- await closePane(pane);
1243
- break;
1244
- }
1245
- };
1246
- const generateCommitMessage = async (changes) => {
1247
- // Try OpenRouter first if API key is available
1248
- const apiKey = process.env.OPENROUTER_API_KEY;
1249
- if (apiKey) {
1250
- try {
1251
- const response = await fetch('https://openrouter.ai/api/v1/chat/completions', {
1252
- method: 'POST',
1253
- headers: {
1254
- 'Content-Type': 'application/json',
1255
- 'Authorization': `Bearer ${apiKey}`,
1256
- },
1257
- body: JSON.stringify({
1258
- model: 'openai/gpt-4o-mini',
1259
- messages: [
1260
- {
1261
- role: 'system',
1262
- content: 'You are a git commit message generator. Generate semantic commit messages following conventional commits format (feat:, fix:, docs:, style:, refactor:, test:, chore:). Be concise and specific.'
1263
- },
1264
- {
1265
- role: 'user',
1266
- content: `Generate a semantic commit message for these changes:\n\n${changes.substring(0, 3000)}`
1267
- }
1268
- ],
1269
- max_tokens: 100,
1270
- temperature: 0.3
1271
- })
1272
- });
1273
- if (response.ok) {
1274
- const data = await response.json();
1275
- const message = data.choices[0].message.content.trim();
1276
- if (message)
1277
- return message;
1278
- }
1279
- }
1280
- catch {
1281
- // Fall through to Claude Code
1282
- }
1283
- }
1284
- // Try Claude Code as fallback
1285
- const systemPrompt = 'You are a git commit message generator. Generate semantic commit messages following conventional commits format (feat:, fix:, docs:, style:, refactor:, test:, chore:). Be concise and specific.';
1286
- const userPrompt = `${systemPrompt}\n\nGenerate a semantic commit message for these changes:\n\n${changes.substring(0, 3000)}`;
1287
- const claudeResponse = await callClaudeCode(userPrompt);
1288
- if (claudeResponse) {
1289
- const message = claudeResponse.trim();
1290
- if (message)
1291
- return message;
1292
- }
1293
- // Final fallback
1294
- return 'chore: merge worktree changes';
1295
- };
1296
- const detectPackageManager = async () => {
1297
- try {
1298
- // Get project root
1299
- const projectRoot = execSync('git rev-parse --show-toplevel', {
1300
- encoding: 'utf-8',
1301
- stdio: 'pipe'
1302
- }).trim();
1303
- // Check if package.json exists
1304
- try {
1305
- await fs.access(path.join(projectRoot, 'package.json'));
1306
- // Check for lock files to determine package manager
1307
- const files = await fs.readdir(projectRoot);
1308
- if (files.includes('pnpm-lock.yaml')) {
1309
- return { manager: 'pnpm', hasPackageJson: true };
1310
- }
1311
- else if (files.includes('yarn.lock')) {
1312
- return { manager: 'yarn', hasPackageJson: true };
1313
- }
1314
- else if (files.includes('package-lock.json')) {
1315
- return { manager: 'npm', hasPackageJson: true };
1316
- }
1317
- else {
1318
- // Default to npm if no lock file found
1319
- return { manager: 'npm', hasPackageJson: true };
1320
- }
1321
- }
1322
- catch {
1323
- // No package.json found
1324
- return { manager: null, hasPackageJson: false };
1325
- }
1326
- }
1327
- catch {
1328
- return { manager: null, hasPackageJson: false };
1329
- }
1330
- };
1331
- const suggestCommand = async (type) => {
1332
- const { manager, hasPackageJson } = await detectPackageManager();
1333
- if (!hasPackageJson) {
1334
- return null;
1335
- }
1336
- // Suggest standard commands based on package manager
1337
- if (type === 'test') {
1338
- return `${manager} run test`;
1339
- }
1340
- else {
1341
- return `${manager} run dev`;
1342
- }
1343
- };
1344
- const copyNonGitFiles = async (worktreePath) => {
1345
- try {
1346
- setStatusMessage('Copying non-git files from main...');
1347
- // Get project root
1348
- const projectRoot = execSync('git rev-parse --show-toplevel', {
1349
- encoding: 'utf-8',
1350
- stdio: 'pipe'
1351
- }).trim();
1352
- // Use rsync to copy non-tracked files
1353
- // This copies everything except git-tracked files, .git, and common build directories
1354
- const rsyncCmd = `rsync -avz --exclude='.git' --exclude='node_modules' --exclude='dist' --exclude='build' --exclude='.next' --exclude='.turbo' "${projectRoot}/" "${worktreePath}/"`;
1355
- execSync(rsyncCmd, { stdio: 'pipe' });
1356
- setStatusMessage('Non-git files copied successfully');
1357
- setTimeout(() => setStatusMessage(''), 2000);
1358
- }
1359
- catch (error) {
1360
- setStatusMessage('Failed to copy non-git files');
1361
- setTimeout(() => setStatusMessage(''), 2000);
1362
- }
1363
- };
1364
- const runCommandInternal = async (type, pane) => {
1365
- if (!pane.worktreePath) {
1366
- setStatusMessage('No worktree path for this pane');
1367
- setTimeout(() => setStatusMessage(''), 2000);
1368
- return;
1369
- }
1370
- const command = type === 'test' ? projectSettings.testCommand : projectSettings.devCommand;
1371
- if (!command) {
1372
- setStatusMessage('No command configured');
1373
- setTimeout(() => setStatusMessage(''), 2000);
1374
- return;
1375
- }
1376
- try {
1377
- setRunningCommand(true);
1378
- setStatusMessage(`Starting ${type} in background window...`);
1379
- // Kill existing window if present
1380
- const existingWindowId = type === 'test' ? pane.testWindowId : pane.devWindowId;
1381
- if (existingWindowId) {
1382
- try {
1383
- execSync(`tmux kill-window -t '${existingWindowId}'`, { stdio: 'pipe' });
1384
- }
1385
- catch { }
1386
- }
1387
- // Create a new background window for the command
1388
- const windowName = `${pane.slug}-${type}`;
1389
- const windowId = execSync(`tmux new-window -d -n '${windowName}' -P -F '#{window_id}'`, { encoding: 'utf-8', stdio: 'pipe' }).trim();
1390
- // Create a log file to capture output
1391
- const logFile = `/tmp/dmux-${pane.id}-${type}.log`;
1392
- // Build the command with output capture
1393
- const fullCommand = `cd "${pane.worktreePath}" && ${command} 2>&1 | tee ${logFile}`;
1394
- // Send the command to the new window
1395
- execSync(`tmux send-keys -t '${windowId}' '${fullCommand.replace(/'/g, "'\\''")}' Enter`, { stdio: 'pipe' });
1396
- // Update pane with window info
1397
- const updatedPane = {
1398
- ...pane,
1399
- [type === 'test' ? 'testWindowId' : 'devWindowId']: windowId,
1400
- [type === 'test' ? 'testStatus' : 'devStatus']: 'running'
1401
- };
1402
- const updatedPanes = panes.map(p => p.id === pane.id ? updatedPane : p);
1403
- await savePanes(updatedPanes);
1404
- // Start monitoring the output
1405
- if (type === 'test') {
1406
- // For tests, monitor for completion
1407
- setTimeout(() => monitorTestOutput(pane.id, logFile), 2000);
1408
- }
1409
- else {
1410
- // For dev, monitor for server URL
1411
- setTimeout(() => monitorDevOutput(pane.id, logFile), 2000);
1412
- }
1413
- setRunningCommand(false);
1414
- setStatusMessage(`${type === 'test' ? 'Test' : 'Dev server'} started in background`);
1415
- setTimeout(() => setStatusMessage(''), 3000);
1416
- }
1417
- catch (error) {
1418
- setRunningCommand(false);
1419
- setStatusMessage(`Failed to run ${type} command`);
1420
- setTimeout(() => setStatusMessage(''), 3000);
1421
- }
1422
- };
1423
441
  const runCommand = async (type, pane) => {
1424
442
  if (!pane.worktreePath) {
1425
443
  setStatusMessage('No worktree path for this pane');
@@ -1492,229 +510,7 @@ const DmuxApp = ({ dmuxDir, panesFile, projectName, sessionName, settingsFile, a
1492
510
  setTimeout(() => setStatusMessage(''), 3000);
1493
511
  }
1494
512
  };
1495
- const monitorTestOutput = async (paneId, logFile) => {
1496
- try {
1497
- const content = await fs.readFile(logFile, 'utf-8');
1498
- // Look for common test result patterns
1499
- let status = 'running';
1500
- if (content.match(/(?:tests?|specs?) (?:passed|✓|succeeded)/i) ||
1501
- content.match(/\b0 fail(?:ing|ed|ures?)\b/i)) {
1502
- status = 'passed';
1503
- }
1504
- else if (content.match(/(?:tests?|specs?) (?:failed|✗|✖)/i) ||
1505
- content.match(/\d+ fail(?:ing|ed|ures?)/i) ||
1506
- content.match(/error:/i)) {
1507
- status = 'failed';
1508
- }
1509
- // Check if process is still running
1510
- const pane = panes.find(p => p.id === paneId);
1511
- if (pane?.testWindowId) {
1512
- try {
1513
- execSync(`tmux list-windows -F '#{window_id}' | grep -q '${pane.testWindowId}'`, { stdio: 'pipe' });
1514
- // Window still exists, check if command is done
1515
- const paneOutput = execSync(`tmux capture-pane -t '${pane.testWindowId}' -p | tail -5`, { encoding: 'utf-8' });
1516
- if (paneOutput.includes('$') || paneOutput.includes('#')) {
1517
- // Command prompt returned, test is done
1518
- if (status === 'running')
1519
- status = 'passed'; // Assume success if no errors found
1520
- }
1521
- }
1522
- catch {
1523
- // Window doesn't exist or command failed
1524
- if (status === 'running')
1525
- status = 'failed';
1526
- }
1527
- }
1528
- // Update pane status
1529
- const updatedPanes = panes.map(p => p.id === paneId
1530
- ? { ...p, testStatus: status, testOutput: content.slice(-5000) } // Keep last 5000 chars
1531
- : p);
1532
- await savePanes(updatedPanes);
1533
- // Continue monitoring if still running
1534
- if (status === 'running') {
1535
- setTimeout(() => monitorTestOutput(paneId, logFile), 2000);
1536
- }
1537
- }
1538
- catch { }
1539
- };
1540
- const monitorDevOutput = async (paneId, logFile) => {
1541
- try {
1542
- const content = await fs.readFile(logFile, 'utf-8');
1543
- // Look for dev server URLs
1544
- const urlMatch = content.match(/https?:\/\/(?:localhost|127\.0\.0\.1|0\.0\.0\.0):\d+/i) ||
1545
- content.match(/Local:\s+(https?:\/\/[^\s]+)/i) ||
1546
- content.match(/listening on port (\d+)/i);
1547
- let devUrl = '';
1548
- if (urlMatch) {
1549
- if (urlMatch[0].startsWith('http')) {
1550
- devUrl = urlMatch[0];
1551
- }
1552
- else if (urlMatch[1]) {
1553
- // If we just found a port number
1554
- devUrl = `http://localhost:${urlMatch[1]}`;
1555
- }
1556
- }
1557
- // Check if process is still running
1558
- const pane = panes.find(p => p.id === paneId);
1559
- let status = 'running';
1560
- if (pane?.devWindowId) {
1561
- try {
1562
- execSync(`tmux list-windows -F '#{window_id}' | grep -q '${pane.devWindowId}'`, { stdio: 'pipe' });
1563
- }
1564
- catch {
1565
- // Window doesn't exist
1566
- status = 'stopped';
1567
- }
1568
- }
1569
- // Update pane status
1570
- const updatedPanes = panes.map(p => p.id === paneId
1571
- ? { ...p, devStatus: status, devUrl: devUrl || p.devUrl }
1572
- : p);
1573
- await savePanes(updatedPanes);
1574
- // Continue monitoring if still running
1575
- if (status === 'running') {
1576
- setTimeout(() => monitorDevOutput(paneId, logFile), 2000);
1577
- }
1578
- }
1579
- catch { }
1580
- };
1581
- const attachBackgroundWindow = async (pane, type) => {
1582
- const windowId = type === 'test' ? pane.testWindowId : pane.devWindowId;
1583
- if (!windowId) {
1584
- setStatusMessage(`No ${type} window to attach`);
1585
- setTimeout(() => setStatusMessage(''), 2000);
1586
- return;
1587
- }
1588
- try {
1589
- // Join the window to current window as a pane
1590
- execSync(`tmux join-pane -h -s '${windowId}'`, { stdio: 'pipe' });
1591
- // Apply smart layout
1592
- const paneCount = parseInt(execSync('tmux list-panes | wc -l', { encoding: 'utf-8' }).trim());
1593
- applySmartLayout(paneCount);
1594
- // Focus on the newly attached pane
1595
- execSync(`tmux select-pane -t '{last}'`, { stdio: 'pipe' });
1596
- setStatusMessage(`Attached ${type} window`);
1597
- setTimeout(() => setStatusMessage(''), 2000);
1598
- }
1599
- catch (error) {
1600
- setStatusMessage(`Failed to attach ${type} window`);
1601
- setTimeout(() => setStatusMessage(''), 2000);
1602
- }
1603
- };
1604
- const mergeWorktree = async (pane) => {
1605
- if (!pane.worktreePath) {
1606
- setStatusMessage('No worktree to merge');
1607
- setTimeout(() => setStatusMessage(''), 2000);
1608
- return;
1609
- }
1610
- try {
1611
- setStatusMessage('Checking worktree status...');
1612
- // Get current branch
1613
- const mainBranch = execSync('git branch --show-current', { encoding: 'utf-8' }).trim();
1614
- // Check for uncommitted changes in the worktree
1615
- const statusOutput = execSync(`git -C "${pane.worktreePath}" status --porcelain`, { encoding: 'utf-8' });
1616
- if (statusOutput.trim()) {
1617
- setStatusMessage('Staging changes...');
1618
- // Stage all changes first (including untracked files)
1619
- execSync(`git -C "${pane.worktreePath}" add -A`, { stdio: 'pipe' });
1620
- setStatusMessage('Generating commit message...');
1621
- // Get the diff of staged changes (after adding files)
1622
- const diffOutput = execSync(`git -C "${pane.worktreePath}" diff --cached`, { encoding: 'utf-8' });
1623
- const statusDetails = execSync(`git -C "${pane.worktreePath}" status`, { encoding: 'utf-8' });
1624
- // Generate commit message using LLM
1625
- const commitMessage = await generateCommitMessage(`${statusDetails}\n\n${diffOutput}`);
1626
- setStatusMessage('Committing changes...');
1627
- // Escape the commit message for shell
1628
- const escapedMessage = commitMessage.replace(/'/g, "'\\''");
1629
- execSync(`git -C "${pane.worktreePath}" commit -m '${escapedMessage}'`, { stdio: 'pipe' });
1630
- }
1631
- setStatusMessage('Merging into main...');
1632
- // Try to merge the worktree branch
1633
- try {
1634
- execSync(`git merge ${pane.slug}`, { stdio: 'pipe' });
1635
- }
1636
- catch (mergeError) {
1637
- // Check if this is a merge conflict
1638
- const errorMessage = mergeError.message || mergeError.toString();
1639
- if (errorMessage.includes('CONFLICT') || errorMessage.includes('conflict')) {
1640
- // Merge conflict detected - exit dmux and inform user
1641
- // Merge conflict detected - exit dmux and inform user
1642
- process.stderr.write('\n\x1b[31m✗ Merge conflict detected!\x1b[0m\n');
1643
- process.stderr.write(`\nThere are merge conflicts when merging branch '${pane.slug}' into '${mainBranch}'.\n`);
1644
- process.stderr.write('\nTo resolve:\n');
1645
- process.stderr.write('1. Manually resolve the merge conflicts in your editor\n');
1646
- process.stderr.write('2. Stage the resolved files: git add <resolved-files>\n');
1647
- process.stderr.write('3. Complete the merge: git commit\n');
1648
- process.stderr.write('4. Run dmux again to continue managing your panes\n');
1649
- process.stderr.write('\nExiting dmux now...\n\n');
1650
- // Clean exit
1651
- process.stdout.write('\x1b[2J\x1b[H');
1652
- process.stdout.write('\x1b[3J');
1653
- try {
1654
- execSync('tmux clear-history', { stdio: 'pipe' });
1655
- }
1656
- catch { }
1657
- process.exit(1);
1658
- }
1659
- else {
1660
- // Some other merge error
1661
- throw mergeError;
1662
- }
1663
- }
1664
- // Remove worktree
1665
- execSync(`git worktree remove "${pane.worktreePath}"`, { stdio: 'pipe' });
1666
- // Delete branch
1667
- execSync(`git branch -d ${pane.slug}`, { stdio: 'pipe' });
1668
- setStatusMessage(`Merged ${pane.slug} into ${mainBranch}`);
1669
- setTimeout(() => setStatusMessage(''), 3000);
1670
- // Show confirmation dialog to close the pane
1671
- setMergedPane(pane);
1672
- setShowMergeConfirmation(true);
1673
- }
1674
- catch (error) {
1675
- setStatusMessage('Failed to merge - check git status');
1676
- setTimeout(() => setStatusMessage(''), 3000);
1677
- }
1678
- };
1679
- // Update handling functions
1680
- const performUpdate = async () => {
1681
- if (!autoUpdater || !updateInfo)
1682
- return;
1683
- try {
1684
- setIsUpdating(true);
1685
- setStatusMessage('Updating dmux...');
1686
- const success = await autoUpdater.performUpdate(updateInfo);
1687
- if (success) {
1688
- setStatusMessage('Update completed successfully! Please restart dmux.');
1689
- setTimeout(() => {
1690
- process.exit(0);
1691
- }, 3000);
1692
- }
1693
- else {
1694
- setStatusMessage('Update failed. Please update manually.');
1695
- setTimeout(() => setStatusMessage(''), 3000);
1696
- }
1697
- }
1698
- catch (error) {
1699
- setStatusMessage('Update failed. Please update manually.');
1700
- setTimeout(() => setStatusMessage(''), 3000);
1701
- }
1702
- finally {
1703
- setIsUpdating(false);
1704
- setShowUpdateDialog(false);
1705
- }
1706
- };
1707
- const skipUpdate = async () => {
1708
- if (!autoUpdater || !updateInfo)
1709
- return;
1710
- await autoUpdater.skipVersion(updateInfo.latestVersion);
1711
- setShowUpdateDialog(false);
1712
- setUpdateInfo(null);
1713
- };
1714
- const dismissUpdate = () => {
1715
- setShowUpdateDialog(false);
1716
- setUpdateInfo(null);
1717
- };
513
+ // Update handling moved to useAutoUpdater
1718
514
  // Cleanup function for exit
1719
515
  const cleanExit = () => {
1720
516
  // Clear screen multiple times to ensure no artifacts
@@ -1735,18 +531,6 @@ const DmuxApp = ({ dmuxDir, panesFile, projectName, sessionName, settingsFile, a
1735
531
  // Disable input while performing operations or loading
1736
532
  return;
1737
533
  }
1738
- if (showUpdateDialog && updateInfo) {
1739
- if (input === 'u' || input === 'U') {
1740
- await performUpdate();
1741
- }
1742
- else if (input === 's' || input === 'S') {
1743
- await skipUpdate();
1744
- }
1745
- else if (input === 'l' || input === 'L' || key.escape) {
1746
- dismissUpdate();
1747
- }
1748
- return;
1749
- }
1750
534
  if (showFileCopyPrompt) {
1751
535
  if (input === 'y' || input === 'Y') {
1752
536
  setShowFileCopyPrompt(false);
@@ -1854,7 +638,7 @@ const DmuxApp = ({ dmuxDir, panesFile, projectName, sessionName, settingsFile, a
1854
638
  }
1855
639
  else if (key.ctrl && input === 'o') {
1856
640
  // Open in external editor
1857
- openInEditor();
641
+ openEditor2(newPanePrompt, setNewPanePrompt);
1858
642
  }
1859
643
  // TextInput handles other input events
1860
644
  return;
@@ -1886,9 +670,18 @@ const DmuxApp = ({ dmuxDir, panesFile, projectName, sessionName, settingsFile, a
1886
670
  setSelectedCloseOption(Math.min(3, selectedCloseOption + 1));
1887
671
  }
1888
672
  else if (key.return && closingPane) {
1889
- handleCloseOption(selectedCloseOption, closingPane).catch(error => {
673
+ handleCloseOption(selectedCloseOption, closingPane).then(() => {
674
+ // Close the dialog after the action is performed
675
+ setShowCloseOptions(false);
676
+ setClosingPane(null);
677
+ setSelectedCloseOption(0);
678
+ }).catch(error => {
1890
679
  setStatusMessage('Failed to close pane');
1891
680
  setTimeout(() => setStatusMessage(''), 2000);
681
+ // Also close the dialog on error
682
+ setShowCloseOptions(false);
683
+ setClosingPane(null);
684
+ setSelectedCloseOption(0);
1892
685
  });
1893
686
  }
1894
687
  return;
@@ -1964,185 +757,55 @@ const DmuxApp = ({ dmuxDir, panesFile, projectName, sessionName, settingsFile, a
1964
757
  React.createElement(Text, { bold: true, color: "cyan" },
1965
758
  "dmux - ",
1966
759
  projectName)),
1967
- React.createElement(Box, { flexDirection: "row", flexWrap: "wrap", gap: 1 },
1968
- panes.map((pane, index) => {
1969
- // Determine border color based on status
1970
- let borderColor = 'gray';
1971
- if (selectedIndex === index) {
1972
- borderColor = 'cyan';
1973
- }
1974
- else if (pane.devStatus === 'running') {
1975
- borderColor = 'green';
1976
- }
1977
- else if (pane.testStatus === 'running') {
1978
- borderColor = 'yellow';
760
+ React.createElement(PanesGrid, { panes: panes, selectedIndex: selectedIndex, isLoading: isLoading, showNewPaneDialog: showNewPaneDialog }),
761
+ isLoading && (React.createElement(LoadingIndicator, null)),
762
+ showNewPaneDialog && !showAgentChoiceDialog && (React.createElement(NewPaneDialog, { value: newPanePrompt, onChange: setNewPanePrompt, onSubmit: (value) => {
763
+ const promptValue = value;
764
+ const agents = availableAgents;
765
+ if (agents.length === 0) {
766
+ setShowNewPaneDialog(false);
767
+ setNewPanePrompt('');
768
+ createNewPaneHook(promptValue);
769
+ }
770
+ else if (agents.length === 1) {
771
+ setShowNewPaneDialog(false);
772
+ setNewPanePrompt('');
773
+ createNewPaneHook(promptValue, agents[0]);
1979
774
  }
1980
- else if (pane.testStatus === 'failed') {
1981
- borderColor = 'red';
1982
- }
1983
- return (React.createElement(Box, { key: pane.id, paddingX: 1, borderStyle: "single", borderColor: borderColor, width: 35, flexShrink: 0 },
1984
- React.createElement(Box, { flexDirection: "column" },
1985
- React.createElement(Box, null,
1986
- React.createElement(Text, { color: selectedIndex === index ? 'cyan' : 'white', bold: true, wrap: "truncate" }, pane.slug),
1987
- pane.worktreePath && (React.createElement(Text, { color: "gray" }, " (wt)")),
1988
- pane.agent && (React.createElement(Text, { color: "gray" },
1989
- " (",
1990
- pane.agent === 'claude' ? 'cc' : 'oc',
1991
- ")"))),
1992
- React.createElement(Text, { color: "gray", dimColor: true, wrap: "truncate" }, pane.prompt.substring(0, 30)),
1993
- pane.agentStatus && (React.createElement(Box, null,
1994
- pane.agentStatus === 'working' && (React.createElement(Text, { color: "cyan" }, "\u273B Working...")),
1995
- pane.agentStatus === 'waiting' && (React.createElement(Text, { color: "yellow", bold: true }, "\u26A0 Needs attention")))),
1996
- (pane.testStatus || pane.devStatus) && (React.createElement(Box, null,
1997
- pane.testStatus === 'running' && (React.createElement(Text, { color: "yellow" }, "\u23F3 Test")),
1998
- pane.testStatus === 'passed' && (React.createElement(Text, { color: "green" }, "\u2713 Test")),
1999
- pane.testStatus === 'failed' && (React.createElement(Text, { color: "red" }, "\u2717 Test")),
2000
- pane.devStatus === 'running' && (React.createElement(Text, { color: "green" },
2001
- "\u25B6 Dev",
2002
- pane.devUrl && (React.createElement(Text, { color: "cyan", wrap: "truncate" },
2003
- " ",
2004
- pane.devUrl.replace(/https?:\/\//, '').substring(0, 15))))))))));
2005
- }),
2006
- !isLoading && !showNewPaneDialog && (React.createElement(Box, { paddingX: 1, borderStyle: "single", borderColor: selectedIndex === panes.length ? 'green' : 'gray', width: 35, flexShrink: 0 },
2007
- React.createElement(Text, { color: selectedIndex === panes.length ? 'green' : 'white' }, "+ New dmux pane")))),
2008
- isLoading && (React.createElement(Box, { borderStyle: "round", borderColor: "cyan", paddingX: 2, marginTop: 1 },
2009
- React.createElement(Box, { flexDirection: "row", gap: 1 },
2010
- React.createElement(Text, { color: "cyan" }, "\u23F3"),
2011
- React.createElement(Text, null, "Loading dmux sessions...")))),
2012
- showNewPaneDialog && !showAgentChoiceDialog && (React.createElement(Box, { flexDirection: "column", marginTop: 1 },
2013
- React.createElement(Text, null, "Enter initial prompt (ESC to cancel):"),
2014
- React.createElement(Box, { borderStyle: "round", borderColor: "#E67E22", paddingX: 1, marginTop: 1 },
2015
- React.createElement(CleanTextInput, { value: newPanePrompt, onChange: setNewPanePrompt, onSubmit: (expandedValue) => {
2016
- const promptValue = expandedValue || newPanePrompt;
2017
- const agents = availableAgents;
2018
- if (agents.length === 0) {
2019
- setShowNewPaneDialog(false);
2020
- setNewPanePrompt('');
2021
- createNewPane(promptValue);
2022
- }
2023
- else if (agents.length === 1) {
2024
- setShowNewPaneDialog(false);
2025
- setNewPanePrompt('');
2026
- createNewPane(promptValue, agents[0]);
2027
- }
2028
- else {
2029
- setPendingPrompt(promptValue);
2030
- setShowNewPaneDialog(false);
2031
- setNewPanePrompt('');
2032
- setShowAgentChoiceDialog(true);
2033
- setAgentChoice(agentChoice || 'claude');
2034
- }
2035
- } })),
2036
- React.createElement(Box, { marginTop: 1 },
2037
- React.createElement(Text, { dimColor: true, italic: true }, "Press Ctrl+O to open in $EDITOR for complex multi-line input")))),
2038
- showAgentChoiceDialog && (React.createElement(Box, { borderStyle: "round", borderColor: "cyan", paddingX: 1, marginTop: 1 },
2039
- React.createElement(Box, { flexDirection: "column" },
2040
- React.createElement(Text, null, "Select agent (\u2190/\u2192, 1/2, C/O, Enter, ESC):"),
2041
- React.createElement(Box, { marginTop: 1, gap: 3 },
2042
- React.createElement(Text, { color: agentChoice === 'claude' ? 'cyan' : 'white' }, agentChoice === 'claude' ? '▶ Claude Code' : ' Claude Code'),
2043
- React.createElement(Text, { color: agentChoice === 'opencode' ? 'cyan' : 'white' }, agentChoice === 'opencode' ? '▶ opencode' : ' opencode'))))),
2044
- isCreatingPane && (React.createElement(Box, { borderStyle: "single", borderColor: "yellow", paddingX: 1, marginTop: 1 },
2045
- React.createElement(Text, { color: "yellow" },
2046
- React.createElement(Text, { bold: true }, "\u23F3 Creating new pane... "),
2047
- statusMessage))),
2048
- showMergeConfirmation && mergedPane && (React.createElement(Box, { borderStyle: "double", borderColor: "yellow", paddingX: 1, marginTop: 1 },
2049
- React.createElement(Box, { flexDirection: "column" },
2050
- React.createElement(Text, { color: "yellow", bold: true }, "Worktree merged successfully!"),
2051
- React.createElement(Text, null,
2052
- "Close the pane \"",
2053
- mergedPane.slug,
2054
- "\"? (y/n)")))),
2055
- showCloseOptions && closingPane && (React.createElement(Box, { borderStyle: "double", borderColor: "red", paddingX: 1, marginTop: 1 },
2056
- React.createElement(Box, { flexDirection: "column" },
2057
- React.createElement(Text, { color: "red", bold: true },
2058
- "Close pane \"",
2059
- closingPane.slug,
2060
- "\"?"),
2061
- React.createElement(Text, { dimColor: true }, "Select an option (ESC to cancel):"),
2062
- React.createElement(Box, { flexDirection: "column", marginTop: 1 },
2063
- React.createElement(Box, null,
2064
- React.createElement(Text, { color: selectedCloseOption === 0 ? 'cyan' : 'white' },
2065
- selectedCloseOption === 0 ? '▶ ' : ' ',
2066
- "Merge & Prune - Merge worktree to main and close")),
2067
- React.createElement(Box, null,
2068
- React.createElement(Text, { color: selectedCloseOption === 1 ? 'cyan' : 'white' },
2069
- selectedCloseOption === 1 ? '▶ ' : ' ',
2070
- "Merge Only - Merge worktree but keep pane open")),
2071
- React.createElement(Box, null,
2072
- React.createElement(Text, { color: selectedCloseOption === 2 ? 'cyan' : 'white' },
2073
- selectedCloseOption === 2 ? '▶ ' : ' ',
2074
- "Delete Unsaved - Remove worktree (discard changes)")),
2075
- React.createElement(Box, null,
2076
- React.createElement(Text, { color: selectedCloseOption === 3 ? 'cyan' : 'white' },
2077
- selectedCloseOption === 3 ? '▶ ' : ' ',
2078
- "Just Close - Close pane only")))))),
2079
- showCommandPrompt && (React.createElement(Box, { borderStyle: "round", borderColor: "gray", paddingX: 1, marginTop: 1 },
2080
- React.createElement(Box, { flexDirection: "column" },
2081
- React.createElement(Text, { color: "magenta", bold: true },
2082
- "Configure ",
2083
- showCommandPrompt === 'test' ? 'Test' : 'Dev',
2084
- " Command"),
2085
- React.createElement(Text, { dimColor: true },
2086
- "Enter command to run ",
2087
- showCommandPrompt === 'test' ? 'tests' : 'dev server',
2088
- " in worktrees"),
2089
- React.createElement(Text, { dimColor: true }, "(Press Enter with empty input for suggested command, ESC to cancel)"),
2090
- React.createElement(Box, { marginTop: 1 },
2091
- React.createElement(StyledTextInput, { value: commandInput, onChange: setCommandInput, placeholder: showCommandPrompt === 'test' ? 'e.g., npm test, pnpm test' : 'e.g., npm run dev, pnpm dev' }))))),
2092
- showFileCopyPrompt && (React.createElement(Box, { borderStyle: "double", borderColor: "yellow", paddingX: 1, marginTop: 1 },
2093
- React.createElement(Box, { flexDirection: "column" },
2094
- React.createElement(Text, { color: "yellow", bold: true }, "First Run Setup"),
2095
- React.createElement(Text, null, "Copy non-git files (like .env, configs) from main to worktree?"),
2096
- React.createElement(Text, { dimColor: true }, "This includes files not tracked by git but excludes node_modules, dist, etc."),
2097
- React.createElement(Box, { marginTop: 1 },
2098
- React.createElement(Text, null, "(y/n):"))))),
2099
- runningCommand && (React.createElement(Box, { borderStyle: "single", borderColor: "blue", paddingX: 1, marginTop: 1 },
2100
- React.createElement(Text, { color: "blue" },
2101
- React.createElement(Text, { bold: true }, "\u25B6 Running command...")))),
2102
- isUpdating && (React.createElement(Box, { borderStyle: "single", borderColor: "yellow", paddingX: 1, marginTop: 1 },
2103
- React.createElement(Text, { color: "yellow" },
2104
- React.createElement(Text, { bold: true }, "\u2B07 Updating dmux...")))),
2105
- showUpdateDialog && updateInfo && (React.createElement(Box, { borderStyle: "double", borderColor: "green", paddingX: 1, marginTop: 1 },
2106
- React.createElement(Box, { flexDirection: "column" },
2107
- React.createElement(Text, { color: "green", bold: true }, "\uD83C\uDF89 dmux Update Available!"),
2108
- React.createElement(Text, null,
2109
- "Current version: ",
2110
- React.createElement(Text, { color: "cyan" }, updateInfo.currentVersion)),
2111
- React.createElement(Text, null,
2112
- "Latest version: ",
2113
- React.createElement(Text, { color: "green" }, updateInfo.latestVersion)),
2114
- updateInfo.installMethod === 'global' && updateInfo.packageManager && (React.createElement(Text, null,
2115
- "Detected global install via: ",
2116
- React.createElement(Text, { color: "yellow" }, updateInfo.packageManager))),
2117
- React.createElement(Box, { marginTop: 1 }, updateInfo.installMethod === 'global' && updateInfo.packageManager ? (React.createElement(Text, null, "[U]pdate now \u2022 [S]kip this version \u2022 [L]ater")) : (React.createElement(Text, null,
2118
- "Manual update required: ",
2119
- React.createElement(Text, { color: "cyan" },
2120
- updateInfo.packageManager || 'npm',
2121
- " update -g dmux"),
2122
- '\n',
2123
- "[S]kip this version \u2022 [L]ater")))))),
775
+ else {
776
+ setPendingPrompt(promptValue);
777
+ setShowNewPaneDialog(false);
778
+ setNewPanePrompt('');
779
+ setShowAgentChoiceDialog(true);
780
+ setAgentChoice(agentChoice || 'claude');
781
+ }
782
+ } })),
783
+ showAgentChoiceDialog && (React.createElement(AgentChoiceDialog, { agentChoice: agentChoice })),
784
+ isCreatingPane && (React.createElement(CreatingIndicator, { message: statusMessage })),
785
+ showMergeConfirmation && mergedPane && (React.createElement(MergeConfirmationDialog, { pane: mergedPane })),
786
+ showCloseOptions && closingPane && (React.createElement(CloseOptionsDialog, { pane: closingPane, selectedIndex: selectedCloseOption })),
787
+ showCommandPrompt && (React.createElement(CommandPromptDialog, { type: showCommandPrompt, value: commandInput, onChange: setCommandInput })),
788
+ showFileCopyPrompt && (React.createElement(FileCopyPrompt, null)),
789
+ runningCommand && (React.createElement(RunningIndicator, null)),
790
+ isUpdating && (React.createElement(UpdatingIndicator, null)),
2124
791
  statusMessage && (React.createElement(Box, { marginTop: 1 },
2125
792
  React.createElement(Text, { color: "green" }, statusMessage))),
2126
- !showNewPaneDialog && !showCommandPrompt && (React.createElement(Box, { marginTop: 1, flexDirection: "column" },
2127
- React.createElement(Text, { dimColor: true }, "Commands: [j]ump \u2022 [t]est \u2022 [d]ev \u2022 [o]pen \u2022 [x]close \u2022 [m]erge \u2022 [n]ew \u2022 [q]uit"),
2128
- React.createElement(Text, { dimColor: true }, "Use arrow keys (\u2191\u2193\u2190\u2192) for spatial navigation, Enter to select"),
2129
- process.env.DEBUG_DMUX && (React.createElement(Text, { dimColor: true },
2130
- "Grid: ",
2131
- Math.max(1, Math.floor(terminalWidth / 37)),
2132
- " cols \u00D7 ",
2133
- Math.ceil((panes.length + 1) / Math.max(1, Math.floor(terminalWidth / 37))),
2134
- " rows | Selected: ",
2135
- (() => {
2136
- const pos = getCardGridPosition(selectedIndex);
2137
- return ` row ${pos.row}, col ${pos.col}`;
2138
- })(),
2139
- " | Terminal: ",
2140
- terminalWidth,
2141
- "w")))),
793
+ React.createElement(FooterHelp, { show: !showNewPaneDialog && !showCommandPrompt, gridInfo: (() => {
794
+ if (!process.env.DEBUG_DMUX)
795
+ return undefined;
796
+ const cols = Math.max(1, Math.floor(terminalWidth / 37));
797
+ const rows = Math.ceil((panes.length + 1) / cols);
798
+ const pos = getCardGridPosition(selectedIndex);
799
+ return `Grid: ${cols} cols × ${rows} rows | Selected: row ${pos.row}, col ${pos.col} | Terminal: ${terminalWidth}w`;
800
+ })() }),
2142
801
  React.createElement(Box, { marginTop: 1 },
2143
802
  React.createElement(Text, { dimColor: true },
2144
803
  "dmux v",
2145
- packageJson.version))));
804
+ packageJson.version,
805
+ updateAvailable && updateInfo && (React.createElement(Text, { color: "yellow" },
806
+ " \u2022 New version ",
807
+ updateInfo.latestVersion,
808
+ " available! Run: npm i -g dmux@latest"))))));
2146
809
  };
2147
810
  export default DmuxApp;
2148
811
  //# sourceMappingURL=DmuxApp.js.map