dmux 1.2.0 → 1.3.1
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.
- package/dist/DmuxApp.d.ts.map +1 -1
- package/dist/DmuxApp.js +700 -257
- package/dist/DmuxApp.js.map +1 -1
- package/dist/SimpleEnhancedInput.d.ts.map +1 -1
- package/dist/SimpleEnhancedInput.js +290 -75
- package/dist/SimpleEnhancedInput.js.map +1 -1
- package/dist/index.js +12 -4
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/DmuxApp.js
CHANGED
|
@@ -23,16 +23,23 @@ const DmuxApp = ({ dmuxDir, panesFile, projectName, sessionName, settingsFile })
|
|
|
23
23
|
const [projectSettings, setProjectSettings] = useState({});
|
|
24
24
|
const [showCommandPrompt, setShowCommandPrompt] = useState(null);
|
|
25
25
|
const [commandInput, setCommandInput] = useState('');
|
|
26
|
-
const [
|
|
27
|
-
const [
|
|
28
|
-
const [generatingCommand, setGeneratingCommand] = useState(false);
|
|
26
|
+
const [showFileCopyPrompt, setShowFileCopyPrompt] = useState(false);
|
|
27
|
+
const [currentCommandType, setCurrentCommandType] = useState(null);
|
|
29
28
|
const [runningCommand, setRunningCommand] = useState(false);
|
|
30
29
|
const { exit } = useApp();
|
|
30
|
+
// Track terminal dimensions for responsive layout
|
|
31
|
+
const [terminalWidth, setTerminalWidth] = useState(process.stdout.columns || 80);
|
|
31
32
|
// Load panes and settings on mount and refresh periodically
|
|
32
33
|
useEffect(() => {
|
|
33
34
|
loadPanes();
|
|
34
35
|
loadSettings();
|
|
35
36
|
const interval = setInterval(loadPanes, 2000);
|
|
37
|
+
// Handle terminal resize
|
|
38
|
+
const handleResize = () => {
|
|
39
|
+
setTerminalWidth(process.stdout.columns || 80);
|
|
40
|
+
};
|
|
41
|
+
// Add resize listener
|
|
42
|
+
process.stdout.on('resize', handleResize);
|
|
36
43
|
// Add cleanup handlers for process termination
|
|
37
44
|
const handleTermination = () => {
|
|
38
45
|
// Clear screen before exit
|
|
@@ -48,10 +55,129 @@ const DmuxApp = ({ dmuxDir, panesFile, projectName, sessionName, settingsFile })
|
|
|
48
55
|
process.on('SIGTERM', handleTermination);
|
|
49
56
|
return () => {
|
|
50
57
|
clearInterval(interval);
|
|
58
|
+
process.stdout.removeListener('resize', handleResize);
|
|
51
59
|
process.removeListener('SIGINT', handleTermination);
|
|
52
60
|
process.removeListener('SIGTERM', handleTermination);
|
|
53
61
|
};
|
|
54
62
|
}, []);
|
|
63
|
+
// Monitor Claude status in all panes with proper dependency tracking
|
|
64
|
+
useEffect(() => {
|
|
65
|
+
if (panes.length === 0)
|
|
66
|
+
return;
|
|
67
|
+
const monitorClaudeStatus = async () => {
|
|
68
|
+
// Monitor Claude Code status for all panes
|
|
69
|
+
const updatedPanes = await Promise.all(panes.map(async (pane) => {
|
|
70
|
+
try {
|
|
71
|
+
// Skip if recently checked (within 500ms to avoid overlapping checks)
|
|
72
|
+
if (pane.lastClaudeCheck && Date.now() - pane.lastClaudeCheck < 500) {
|
|
73
|
+
return pane;
|
|
74
|
+
}
|
|
75
|
+
// Capture the last 30 lines of the pane for better detection
|
|
76
|
+
const captureOutput = execSync(`tmux capture-pane -t '${pane.paneId}' -p -S -30`, { encoding: 'utf-8', stdio: 'pipe' });
|
|
77
|
+
// Pattern detection for Claude Code states
|
|
78
|
+
// Working patterns - Claude is processing
|
|
79
|
+
// The ONLY reliable indicator is "(esc to interrupt)"
|
|
80
|
+
const workingPatterns = [
|
|
81
|
+
/esc to interrupt/i, // The ONLY reliable indicator that Claude is actively working
|
|
82
|
+
];
|
|
83
|
+
// Extract last few lines to check for patterns
|
|
84
|
+
const lines = captureOutput.split('\n');
|
|
85
|
+
const lastLines = lines.slice(-10).join('\n');
|
|
86
|
+
// Check if Claude's input box is present (waiting for input)
|
|
87
|
+
const hasInputBox = /╭─+╮/.test(lastLines) && /╰─+╯/.test(lastLines) && /│\s+>\s+.*│/.test(lastLines);
|
|
88
|
+
// Permission/attention patterns - needs user input (very forgiving)
|
|
89
|
+
const attentionPatterns = [
|
|
90
|
+
/\?\s*$/m, // Any line ending with a question mark
|
|
91
|
+
/y\/n/i, // Any y/n prompt
|
|
92
|
+
/yes.*no/i, // Yes or no prompts
|
|
93
|
+
/\ballow\b.*\?/i,
|
|
94
|
+
/\bapprove\b.*\?/i,
|
|
95
|
+
/\bgrant\b.*\?/i,
|
|
96
|
+
/\btrust\b.*\?/i,
|
|
97
|
+
/\baccept\b.*\?/i,
|
|
98
|
+
/\bcontinue\b.*\?/i,
|
|
99
|
+
/\bproceed\b.*\?/i,
|
|
100
|
+
/permission/i,
|
|
101
|
+
/confirmation/i,
|
|
102
|
+
/press.*enter/i,
|
|
103
|
+
/waiting for/i,
|
|
104
|
+
/are you sure/i,
|
|
105
|
+
/would you like/i,
|
|
106
|
+
/do you want/i,
|
|
107
|
+
/please confirm/i,
|
|
108
|
+
/requires.*approval/i,
|
|
109
|
+
/needs.*input/i,
|
|
110
|
+
/⏵⏵\s*accept edits/i, // Claude's accept edits mode
|
|
111
|
+
/shift\+tab to cycle/i, // Claude's interface hints
|
|
112
|
+
];
|
|
113
|
+
// Check if Claude is working
|
|
114
|
+
const isWorking = workingPatterns.some(pattern => pattern.test(captureOutput));
|
|
115
|
+
// Check if Claude needs attention
|
|
116
|
+
const needsAttention = attentionPatterns.some(pattern => pattern.test(captureOutput)) || hasInputBox;
|
|
117
|
+
// Determine status - working takes precedence
|
|
118
|
+
let newStatus = 'idle';
|
|
119
|
+
if (isWorking) {
|
|
120
|
+
newStatus = 'working';
|
|
121
|
+
}
|
|
122
|
+
else if (needsAttention && !isWorking) {
|
|
123
|
+
// Only show as waiting if NOT working (working takes precedence)
|
|
124
|
+
newStatus = 'waiting';
|
|
125
|
+
}
|
|
126
|
+
// Additional checks for specific Claude states
|
|
127
|
+
// If we see "accept edits" without other working indicators, it's waiting
|
|
128
|
+
if (/accept edits/i.test(captureOutput) && !/esc to interrupt/i.test(captureOutput)) {
|
|
129
|
+
newStatus = 'waiting';
|
|
130
|
+
}
|
|
131
|
+
// If Claude's input box is visible and no working indicators, it's waiting for input
|
|
132
|
+
if (hasInputBox && !isWorking) {
|
|
133
|
+
newStatus = 'waiting';
|
|
134
|
+
}
|
|
135
|
+
// Check for specific Claude question patterns that might not end with ?
|
|
136
|
+
const claudeQuestionPatterns = [
|
|
137
|
+
/I (can|could|should|would|will|may|might)/i,
|
|
138
|
+
/Let me know/i,
|
|
139
|
+
/Please (tell|let|inform|advise)/i,
|
|
140
|
+
/Would you prefer/i,
|
|
141
|
+
/Should I (proceed|continue|go ahead)/i,
|
|
142
|
+
];
|
|
143
|
+
if (claudeQuestionPatterns.some(pattern => pattern.test(lastLines)) && !isWorking) {
|
|
144
|
+
newStatus = 'waiting';
|
|
145
|
+
}
|
|
146
|
+
// Return updated pane if status changed
|
|
147
|
+
if (pane.claudeStatus !== newStatus) {
|
|
148
|
+
return {
|
|
149
|
+
...pane,
|
|
150
|
+
claudeStatus: newStatus,
|
|
151
|
+
lastClaudeCheck: Date.now()
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
// Just update timestamp
|
|
155
|
+
return {
|
|
156
|
+
...pane,
|
|
157
|
+
lastClaudeCheck: Date.now()
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
catch (error) {
|
|
161
|
+
// If we can't capture the pane, it might be dead
|
|
162
|
+
return pane;
|
|
163
|
+
}
|
|
164
|
+
}));
|
|
165
|
+
// Only update state if something changed
|
|
166
|
+
const hasChanges = updatedPanes.some((pane, index) => pane.claudeStatus !== panes[index]?.claudeStatus);
|
|
167
|
+
if (hasChanges) {
|
|
168
|
+
setPanes(updatedPanes);
|
|
169
|
+
// Save to file
|
|
170
|
+
await fs.writeFile(panesFile, JSON.stringify(updatedPanes, null, 2));
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
// Run monitoring immediately
|
|
174
|
+
monitorClaudeStatus();
|
|
175
|
+
// Set up interval for continuous monitoring
|
|
176
|
+
const claudeInterval = setInterval(monitorClaudeStatus, 1000); // Check every second
|
|
177
|
+
return () => {
|
|
178
|
+
clearInterval(claudeInterval);
|
|
179
|
+
};
|
|
180
|
+
}, [panes, panesFile]); // Re-run when panes change
|
|
55
181
|
const loadSettings = async () => {
|
|
56
182
|
try {
|
|
57
183
|
const content = await fs.readFile(settingsFile, 'utf-8');
|
|
@@ -81,10 +207,9 @@ const DmuxApp = ({ dmuxDir, panesFile, projectName, sessionName, settingsFile })
|
|
|
81
207
|
}).trim().split('\n');
|
|
82
208
|
// Check if our pane ID exists in the list
|
|
83
209
|
if (paneIds.includes(pane.paneId)) {
|
|
84
|
-
// Update pane title
|
|
85
|
-
const paneTitle = pane.prompt ? pane.prompt.substring(0, 30) : pane.slug;
|
|
210
|
+
// Update pane title to match the slug
|
|
86
211
|
try {
|
|
87
|
-
execSync(`tmux select-pane -t '${pane.paneId}' -T "${
|
|
212
|
+
execSync(`tmux select-pane -t '${pane.paneId}' -T "${pane.slug}"`, { stdio: 'pipe' });
|
|
88
213
|
}
|
|
89
214
|
catch {
|
|
90
215
|
// Ignore if setting title fails
|
|
@@ -107,6 +232,163 @@ const DmuxApp = ({ dmuxDir, panesFile, projectName, sessionName, settingsFile })
|
|
|
107
232
|
setPanes([]);
|
|
108
233
|
}
|
|
109
234
|
};
|
|
235
|
+
const getPanePositions = () => {
|
|
236
|
+
try {
|
|
237
|
+
const output = execSync(`tmux list-panes -F '#{pane_id} #{pane_left} #{pane_top} #{pane_width} #{pane_height}'`, { encoding: 'utf-8', stdio: 'pipe' }).trim();
|
|
238
|
+
return output.split('\n').map(line => {
|
|
239
|
+
const [paneId, left, top, width, height] = line.split(' ');
|
|
240
|
+
return {
|
|
241
|
+
paneId,
|
|
242
|
+
left: parseInt(left),
|
|
243
|
+
top: parseInt(top),
|
|
244
|
+
width: parseInt(width),
|
|
245
|
+
height: parseInt(height)
|
|
246
|
+
};
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
catch {
|
|
250
|
+
return [];
|
|
251
|
+
}
|
|
252
|
+
};
|
|
253
|
+
// Calculate the visual grid position of cards based on their index
|
|
254
|
+
const getCardGridPosition = (index) => {
|
|
255
|
+
// Cards are displayed in a flexbox with wrapping
|
|
256
|
+
// With card width of 35 and typical terminal width of 80-120, we get 2-3 cards per row
|
|
257
|
+
const cardWidth = 35 + 2; // Card width plus gap
|
|
258
|
+
const cardsPerRow = Math.max(1, Math.floor(terminalWidth / cardWidth));
|
|
259
|
+
const row = Math.floor(index / cardsPerRow);
|
|
260
|
+
const col = index % cardsPerRow;
|
|
261
|
+
return { row, col };
|
|
262
|
+
};
|
|
263
|
+
const findCardInDirection = (currentIndex, direction) => {
|
|
264
|
+
const totalItems = panes.length + 1; // +1 for "New dmux pane" button
|
|
265
|
+
const currentPos = getCardGridPosition(currentIndex);
|
|
266
|
+
// Calculate cards per row based on current terminal width
|
|
267
|
+
const cardWidth = 35 + 2; // Card width plus gap
|
|
268
|
+
const cardsPerRow = Math.max(1, Math.floor(terminalWidth / cardWidth));
|
|
269
|
+
let targetIndex = null;
|
|
270
|
+
switch (direction) {
|
|
271
|
+
case 'up':
|
|
272
|
+
// Move up one row, same column
|
|
273
|
+
if (currentPos.row > 0) {
|
|
274
|
+
targetIndex = (currentPos.row - 1) * cardsPerRow + currentPos.col;
|
|
275
|
+
// Make sure target exists
|
|
276
|
+
if (targetIndex >= totalItems) {
|
|
277
|
+
// Try to find the last item in the row above
|
|
278
|
+
targetIndex = Math.min((currentPos.row - 1) * cardsPerRow + cardsPerRow - 1, totalItems - 1);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
break;
|
|
282
|
+
case 'down':
|
|
283
|
+
// Move down one row, same column
|
|
284
|
+
targetIndex = (currentPos.row + 1) * cardsPerRow + currentPos.col;
|
|
285
|
+
if (targetIndex >= totalItems) {
|
|
286
|
+
// If moving down from last row, try to go to "New dmux pane" if not already there
|
|
287
|
+
if (currentIndex < totalItems - 1) {
|
|
288
|
+
targetIndex = totalItems - 1;
|
|
289
|
+
}
|
|
290
|
+
else {
|
|
291
|
+
targetIndex = null;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
break;
|
|
295
|
+
case 'left':
|
|
296
|
+
// Move left one column, same row
|
|
297
|
+
if (currentPos.col > 0) {
|
|
298
|
+
targetIndex = currentIndex - 1;
|
|
299
|
+
}
|
|
300
|
+
else if (currentPos.row > 0) {
|
|
301
|
+
// Wrap to end of previous row
|
|
302
|
+
targetIndex = currentPos.row * cardsPerRow - 1;
|
|
303
|
+
if (targetIndex >= totalItems) {
|
|
304
|
+
targetIndex = totalItems - 1;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
break;
|
|
308
|
+
case 'right':
|
|
309
|
+
// Move right one column, same row
|
|
310
|
+
if (currentPos.col < cardsPerRow - 1 && currentIndex < totalItems - 1) {
|
|
311
|
+
targetIndex = currentIndex + 1;
|
|
312
|
+
}
|
|
313
|
+
else if ((currentPos.row + 1) * cardsPerRow < totalItems) {
|
|
314
|
+
// Wrap to start of next row
|
|
315
|
+
targetIndex = (currentPos.row + 1) * cardsPerRow;
|
|
316
|
+
}
|
|
317
|
+
break;
|
|
318
|
+
}
|
|
319
|
+
// Validate target index
|
|
320
|
+
if (targetIndex !== null && targetIndex >= 0 && targetIndex < totalItems) {
|
|
321
|
+
return targetIndex;
|
|
322
|
+
}
|
|
323
|
+
return null;
|
|
324
|
+
};
|
|
325
|
+
const findPaneInDirection = (currentPane, direction) => {
|
|
326
|
+
const positions = getPanePositions();
|
|
327
|
+
const currentPos = positions.find(p => p.paneId === currentPane.paneId);
|
|
328
|
+
if (!currentPos)
|
|
329
|
+
return null;
|
|
330
|
+
// Calculate center point of current pane
|
|
331
|
+
const currentCenterX = currentPos.left + currentPos.width / 2;
|
|
332
|
+
const currentCenterY = currentPos.top + currentPos.height / 2;
|
|
333
|
+
// Filter panes based on direction
|
|
334
|
+
const candidatePanes = panes.filter(pane => {
|
|
335
|
+
if (pane.paneId === currentPane.paneId)
|
|
336
|
+
return false;
|
|
337
|
+
const pos = positions.find(p => p.paneId === pane.paneId);
|
|
338
|
+
if (!pos)
|
|
339
|
+
return false;
|
|
340
|
+
const centerX = pos.left + pos.width / 2;
|
|
341
|
+
const centerY = pos.top + pos.height / 2;
|
|
342
|
+
switch (direction) {
|
|
343
|
+
case 'up':
|
|
344
|
+
return centerY < currentCenterY;
|
|
345
|
+
case 'down':
|
|
346
|
+
return centerY > currentCenterY;
|
|
347
|
+
case 'left':
|
|
348
|
+
return centerX < currentCenterX;
|
|
349
|
+
case 'right':
|
|
350
|
+
return centerX > currentCenterX;
|
|
351
|
+
default:
|
|
352
|
+
return false;
|
|
353
|
+
}
|
|
354
|
+
});
|
|
355
|
+
if (candidatePanes.length === 0)
|
|
356
|
+
return null;
|
|
357
|
+
// Find the closest pane in the given direction
|
|
358
|
+
let closestPane = candidatePanes[0];
|
|
359
|
+
let minDistance = Infinity;
|
|
360
|
+
for (const pane of candidatePanes) {
|
|
361
|
+
const pos = positions.find(p => p.paneId === pane.paneId);
|
|
362
|
+
if (!pos)
|
|
363
|
+
continue;
|
|
364
|
+
const centerX = pos.left + pos.width / 2;
|
|
365
|
+
const centerY = pos.top + pos.height / 2;
|
|
366
|
+
let distance;
|
|
367
|
+
switch (direction) {
|
|
368
|
+
case 'up':
|
|
369
|
+
case 'down':
|
|
370
|
+
// For vertical movement, prioritize vertical alignment, then distance
|
|
371
|
+
const xDiff = Math.abs(centerX - currentCenterX);
|
|
372
|
+
const yDiff = Math.abs(centerY - currentCenterY);
|
|
373
|
+
// Weight horizontal difference less than vertical
|
|
374
|
+
distance = yDiff + xDiff * 0.3;
|
|
375
|
+
break;
|
|
376
|
+
case 'left':
|
|
377
|
+
case 'right':
|
|
378
|
+
// For horizontal movement, prioritize horizontal alignment, then distance
|
|
379
|
+
const xDiff2 = Math.abs(centerX - currentCenterX);
|
|
380
|
+
const yDiff2 = Math.abs(centerY - currentCenterY);
|
|
381
|
+
// Weight vertical difference less than horizontal
|
|
382
|
+
distance = xDiff2 + yDiff2 * 0.3;
|
|
383
|
+
break;
|
|
384
|
+
}
|
|
385
|
+
if (distance < minDistance) {
|
|
386
|
+
minDistance = distance;
|
|
387
|
+
closestPane = pane;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
return closestPane;
|
|
391
|
+
};
|
|
110
392
|
const savePanes = async (newPanes) => {
|
|
111
393
|
await fs.writeFile(panesFile, JSON.stringify(newPanes, null, 2));
|
|
112
394
|
setPanes(newPanes);
|
|
@@ -314,10 +596,8 @@ const DmuxApp = ({ dmuxDir, panesFile, projectName, sessionName, settingsFile })
|
|
|
314
596
|
execSync('tmux send-keys C-l', { stdio: 'pipe' });
|
|
315
597
|
}
|
|
316
598
|
catch { }
|
|
317
|
-
//
|
|
318
|
-
|
|
319
|
-
// Wait for exit to complete
|
|
320
|
-
await new Promise(resolve => setTimeout(resolve, 500));
|
|
599
|
+
// Wait a bit for clearing to settle
|
|
600
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
321
601
|
// 4. Force tmux to refresh the display
|
|
322
602
|
try {
|
|
323
603
|
execSync('tmux refresh-client', { stdio: 'pipe' });
|
|
@@ -325,14 +605,20 @@ const DmuxApp = ({ dmuxDir, panesFile, projectName, sessionName, settingsFile })
|
|
|
325
605
|
catch { }
|
|
326
606
|
// Get current pane count to determine layout
|
|
327
607
|
const paneCount = parseInt(execSync('tmux list-panes | wc -l', { encoding: 'utf-8' }).trim());
|
|
608
|
+
// Enable pane borders to show titles
|
|
609
|
+
try {
|
|
610
|
+
execSync(`tmux set-option -g pane-border-status top`, { stdio: 'pipe' });
|
|
611
|
+
}
|
|
612
|
+
catch {
|
|
613
|
+
// Ignore if already set or fails
|
|
614
|
+
}
|
|
328
615
|
// Create new pane
|
|
329
616
|
const paneInfo = execSync(`tmux split-window -h -P -F '#{pane_id}'`, { encoding: 'utf-8' }).trim();
|
|
330
617
|
// Wait for pane creation to settle
|
|
331
618
|
await new Promise(resolve => setTimeout(resolve, 500));
|
|
332
|
-
// Set pane title
|
|
333
|
-
const paneTitle = prompt ? prompt.substring(0, 30) : slug;
|
|
619
|
+
// Set pane title to match the slug
|
|
334
620
|
try {
|
|
335
|
-
execSync(`tmux select-pane -t '${paneInfo}' -T "${
|
|
621
|
+
execSync(`tmux select-pane -t '${paneInfo}' -T "${slug}"`, { stdio: 'pipe' });
|
|
336
622
|
}
|
|
337
623
|
catch {
|
|
338
624
|
// Ignore if setting title fails
|
|
@@ -370,47 +656,117 @@ const DmuxApp = ({ dmuxDir, panesFile, projectName, sessionName, settingsFile })
|
|
|
370
656
|
execSync(`tmux send-keys -t '${paneInfo}' Enter`, { stdio: 'pipe' });
|
|
371
657
|
// Monitor for Claude Code trust prompt and auto-respond
|
|
372
658
|
const autoApproveTrust = async () => {
|
|
373
|
-
//
|
|
374
|
-
await new Promise(resolve => setTimeout(resolve,
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
659
|
+
// Wait for Claude to start up before checking for prompts
|
|
660
|
+
await new Promise(resolve => setTimeout(resolve, 800));
|
|
661
|
+
const maxChecks = 100; // 100 checks * 100ms = 10 seconds total
|
|
662
|
+
const checkInterval = 100; // Check every 100ms
|
|
663
|
+
let lastContent = '';
|
|
664
|
+
let stableContentCount = 0;
|
|
665
|
+
let promptHandled = false;
|
|
666
|
+
// More comprehensive trust prompt patterns
|
|
667
|
+
const trustPromptPatterns = [
|
|
668
|
+
/Do you trust the files in this folder\?/i,
|
|
669
|
+
/Trust the files in this workspace\?/i,
|
|
670
|
+
/Do you trust the authors of the files/i,
|
|
671
|
+
/Do you want to trust this workspace\?/i,
|
|
672
|
+
/trust.*files.*folder/i,
|
|
673
|
+
/trust.*workspace/i,
|
|
674
|
+
/Do you trust/i,
|
|
675
|
+
/Trust this folder/i,
|
|
676
|
+
/trust.*directory/i,
|
|
677
|
+
/permission.*grant/i,
|
|
678
|
+
/allow.*access/i,
|
|
679
|
+
/workspace.*trust/i,
|
|
680
|
+
/accept.*edits/i, // Claude's accept edits prompt
|
|
681
|
+
/permission.*mode/i, // Permission mode prompt
|
|
682
|
+
/allow.*claude/i, // Allow Claude prompt
|
|
683
|
+
/\[y\/n\]/i, // Common yes/no prompt pattern
|
|
684
|
+
/\(y\/n\)/i,
|
|
685
|
+
/Yes\/No/i,
|
|
686
|
+
/\[Y\/n\]/i, // Default yes pattern
|
|
687
|
+
/press.*enter.*accept/i, // Press enter to accept
|
|
688
|
+
/press.*enter.*continue/i // Press enter to continue
|
|
689
|
+
];
|
|
690
|
+
for (let i = 0; i < maxChecks; i++) {
|
|
691
|
+
await new Promise(resolve => setTimeout(resolve, checkInterval));
|
|
692
|
+
try {
|
|
693
|
+
// Capture the pane content
|
|
694
|
+
const paneContent = execSync(`tmux capture-pane -t '${paneInfo}' -p -S -30`, // Capture last 30 lines
|
|
695
|
+
{ encoding: 'utf-8', stdio: 'pipe' });
|
|
696
|
+
// Check if content has stabilized (same for 3 checks = prompt is waiting)
|
|
697
|
+
if (paneContent === lastContent) {
|
|
698
|
+
stableContentCount++;
|
|
699
|
+
}
|
|
700
|
+
else {
|
|
701
|
+
stableContentCount = 0;
|
|
702
|
+
lastContent = paneContent;
|
|
703
|
+
}
|
|
704
|
+
// Look for trust prompt in the current content
|
|
705
|
+
const hasTrustPrompt = trustPromptPatterns.some(pattern => pattern.test(paneContent));
|
|
706
|
+
// Also check if we see specific Claude permission text
|
|
707
|
+
const hasClaudePermissionPrompt = paneContent.includes('Do you trust') ||
|
|
708
|
+
paneContent.includes('trust the files') ||
|
|
709
|
+
paneContent.includes('permission') ||
|
|
710
|
+
paneContent.includes('allow') ||
|
|
711
|
+
(paneContent.includes('folder') && paneContent.includes('?'));
|
|
712
|
+
if ((hasTrustPrompt || hasClaudePermissionPrompt) && !promptHandled) {
|
|
713
|
+
// Log what we detected for debugging
|
|
714
|
+
// Detected trust prompt in pane, content stable
|
|
715
|
+
// Content is stable and we found a prompt
|
|
716
|
+
if (stableContentCount >= 2) {
|
|
717
|
+
// Attempting to auto-approve trust prompt
|
|
718
|
+
// Try multiple response methods to ensure it works
|
|
719
|
+
// Method 1: Send 'y' followed by Enter (most explicit)
|
|
720
|
+
// Sending 'y' + Enter
|
|
721
|
+
execSync(`tmux send-keys -t '${paneInfo}' 'y'`, { stdio: 'pipe' });
|
|
722
|
+
await new Promise(resolve => setTimeout(resolve, 50));
|
|
403
723
|
execSync(`tmux send-keys -t '${paneInfo}' Enter`, { stdio: 'pipe' });
|
|
724
|
+
// Method 2: Just Enter (if it's a yes/no with default yes)
|
|
725
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
726
|
+
// Sending additional Enter
|
|
727
|
+
execSync(`tmux send-keys -t '${paneInfo}' Enter`, { stdio: 'pipe' });
|
|
728
|
+
// Mark as handled to avoid duplicate responses
|
|
729
|
+
promptHandled = true;
|
|
730
|
+
// Wait and check if prompt is gone
|
|
731
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
732
|
+
// Verify the prompt is gone
|
|
733
|
+
const updatedContent = execSync(`tmux capture-pane -t '${paneInfo}' -p -S -10`, { encoding: 'utf-8', stdio: 'pipe' });
|
|
734
|
+
// If trust prompt is gone, check if we need to resend the Claude command
|
|
735
|
+
const promptGone = !trustPromptPatterns.some(p => p.test(updatedContent));
|
|
736
|
+
if (promptGone) {
|
|
737
|
+
// Check if Claude is running or if we need to restart it
|
|
738
|
+
const claudeRunning = updatedContent.includes('Claude') ||
|
|
739
|
+
updatedContent.includes('claude') ||
|
|
740
|
+
updatedContent.includes('Assistant') ||
|
|
741
|
+
(prompt && updatedContent.includes(prompt.substring(0, Math.min(20, prompt.length))));
|
|
742
|
+
if (!claudeRunning && !updatedContent.includes('$')) {
|
|
743
|
+
// Claude might have exited after permission was granted, restart it
|
|
744
|
+
// Claude not running after trust approval, restarting
|
|
745
|
+
await new Promise(resolve => setTimeout(resolve, 300));
|
|
746
|
+
execSync(`tmux send-keys -t '${paneInfo}' '${escapedCmd}'`, { stdio: 'pipe' });
|
|
747
|
+
execSync(`tmux send-keys -t '${paneInfo}' Enter`, { stdio: 'pipe' });
|
|
748
|
+
}
|
|
749
|
+
// Successfully handled the prompt
|
|
750
|
+
break;
|
|
751
|
+
}
|
|
404
752
|
}
|
|
405
753
|
}
|
|
754
|
+
// If we see Claude is already running without prompts, we're done
|
|
755
|
+
if (!hasTrustPrompt && !hasClaudePermissionPrompt &&
|
|
756
|
+
(paneContent.includes('Claude') || paneContent.includes('Assistant'))) {
|
|
757
|
+
break;
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
catch (error) {
|
|
761
|
+
// Continue checking, errors are non-fatal
|
|
762
|
+
// Error checking for trust prompt
|
|
406
763
|
}
|
|
407
|
-
}
|
|
408
|
-
catch (error) {
|
|
409
|
-
// Ignore errors in auto-approval, it's a best-effort feature
|
|
410
764
|
}
|
|
411
765
|
};
|
|
412
766
|
// Start monitoring for trust prompt in background
|
|
413
|
-
autoApproveTrust()
|
|
767
|
+
autoApproveTrust().catch(err => {
|
|
768
|
+
// Error in autoApproveTrust
|
|
769
|
+
});
|
|
414
770
|
// Keep focus on the new pane
|
|
415
771
|
execSync(`tmux select-pane -t '${paneInfo}'`, { stdio: 'pipe' });
|
|
416
772
|
// Save pane info
|
|
@@ -423,7 +779,7 @@ const DmuxApp = ({ dmuxDir, panesFile, projectName, sessionName, settingsFile })
|
|
|
423
779
|
};
|
|
424
780
|
const updatedPanes = [...panes, newPane];
|
|
425
781
|
await fs.writeFile(panesFile, JSON.stringify(updatedPanes, null, 2));
|
|
426
|
-
// Switch back to the original pane (where dmux
|
|
782
|
+
// Switch back to the original pane (where dmux is running)
|
|
427
783
|
execSync(`tmux select-pane -t '${originalPaneId}'`, { stdio: 'pipe' });
|
|
428
784
|
// Re-set the title for the dmux pane
|
|
429
785
|
try {
|
|
@@ -432,13 +788,23 @@ const DmuxApp = ({ dmuxDir, panesFile, projectName, sessionName, settingsFile })
|
|
|
432
788
|
catch {
|
|
433
789
|
// Ignore if setting title fails
|
|
434
790
|
}
|
|
435
|
-
//
|
|
436
|
-
|
|
437
|
-
//
|
|
438
|
-
|
|
791
|
+
// Clear the screen and redraw the UI
|
|
792
|
+
process.stdout.write('\x1b[2J\x1b[H');
|
|
793
|
+
// Reset the creating pane flag and refresh
|
|
794
|
+
setIsCreatingPane(false);
|
|
795
|
+
setStatusMessage('');
|
|
796
|
+
// Force a reload of panes to ensure UI is up to date
|
|
797
|
+
await loadPanes();
|
|
439
798
|
};
|
|
440
799
|
const jumpToPane = (paneId) => {
|
|
441
800
|
try {
|
|
801
|
+
// Enable pane borders to show titles (if not already enabled)
|
|
802
|
+
try {
|
|
803
|
+
execSync(`tmux set-option -g pane-border-status top`, { stdio: 'pipe' });
|
|
804
|
+
}
|
|
805
|
+
catch {
|
|
806
|
+
// Ignore if already set or fails
|
|
807
|
+
}
|
|
442
808
|
execSync(`tmux select-pane -t '${paneId}'`, { stdio: 'pipe' });
|
|
443
809
|
setStatusMessage('Jumped to pane');
|
|
444
810
|
setTimeout(() => setStatusMessage(''), 2000);
|
|
@@ -527,8 +893,38 @@ const DmuxApp = ({ dmuxDir, panesFile, projectName, sessionName, settingsFile })
|
|
|
527
893
|
execSync(`git -C "${pane.worktreePath}" commit -m '${escapedMessage}'`, { stdio: 'pipe' });
|
|
528
894
|
}
|
|
529
895
|
setStatusMessage('Merging into main...');
|
|
530
|
-
//
|
|
531
|
-
|
|
896
|
+
// Try to merge the worktree branch
|
|
897
|
+
try {
|
|
898
|
+
execSync(`git merge ${pane.slug}`, { stdio: 'pipe' });
|
|
899
|
+
}
|
|
900
|
+
catch (mergeError) {
|
|
901
|
+
// Check if this is a merge conflict
|
|
902
|
+
const errorMessage = mergeError.message || mergeError.toString();
|
|
903
|
+
if (errorMessage.includes('CONFLICT') || errorMessage.includes('conflict')) {
|
|
904
|
+
// Merge conflict detected - exit dmux and inform user
|
|
905
|
+
// Merge conflict detected - exit dmux and inform user
|
|
906
|
+
process.stderr.write('\n\x1b[31m✗ Merge conflict detected!\x1b[0m\n');
|
|
907
|
+
process.stderr.write(`\nThere are merge conflicts when merging branch '${pane.slug}' into '${mainBranch}'.\n`);
|
|
908
|
+
process.stderr.write('\nTo resolve:\n');
|
|
909
|
+
process.stderr.write('1. Manually resolve the merge conflicts in your editor\n');
|
|
910
|
+
process.stderr.write('2. Stage the resolved files: git add <resolved-files>\n');
|
|
911
|
+
process.stderr.write('3. Complete the merge: git commit\n');
|
|
912
|
+
process.stderr.write('4. Run dmux again to continue managing your panes\n');
|
|
913
|
+
process.stderr.write('\nExiting dmux now...\n\n');
|
|
914
|
+
// Clean exit
|
|
915
|
+
process.stdout.write('\x1b[2J\x1b[H');
|
|
916
|
+
process.stdout.write('\x1b[3J');
|
|
917
|
+
try {
|
|
918
|
+
execSync('tmux clear-history', { stdio: 'pipe' });
|
|
919
|
+
}
|
|
920
|
+
catch { }
|
|
921
|
+
process.exit(1);
|
|
922
|
+
}
|
|
923
|
+
else {
|
|
924
|
+
// Some other merge error
|
|
925
|
+
throw mergeError;
|
|
926
|
+
}
|
|
927
|
+
}
|
|
532
928
|
// Remove worktree
|
|
533
929
|
execSync(`git worktree remove "${pane.worktreePath}"`, { stdio: 'pipe' });
|
|
534
930
|
// Delete branch
|
|
@@ -639,163 +1035,131 @@ const DmuxApp = ({ dmuxDir, panesFile, projectName, sessionName, settingsFile })
|
|
|
639
1035
|
// Final fallback
|
|
640
1036
|
return 'chore: merge worktree changes';
|
|
641
1037
|
};
|
|
642
|
-
const
|
|
643
|
-
setGeneratingCommand(true);
|
|
644
|
-
setStatusMessage(`Generating ${type} command with AI...`);
|
|
1038
|
+
const detectPackageManager = async () => {
|
|
645
1039
|
try {
|
|
646
|
-
// Find claude command
|
|
647
|
-
const claudeCmd = await findClaudeCommand();
|
|
648
|
-
if (!claudeCmd) {
|
|
649
|
-
throw new Error('Claude Code not found. Please ensure Claude Code is installed and accessible');
|
|
650
|
-
}
|
|
651
1040
|
// Get project root
|
|
652
1041
|
const projectRoot = execSync('git rev-parse --show-toplevel', {
|
|
653
1042
|
encoding: 'utf-8',
|
|
654
1043
|
stdio: 'pipe'
|
|
655
1044
|
}).trim();
|
|
656
|
-
//
|
|
657
|
-
setStatusMessage('Analyzing project structure...');
|
|
658
|
-
// Get main directory listing
|
|
659
|
-
const mainFiles = execSync(`ls -la "${projectRoot}"`, {
|
|
660
|
-
encoding: 'utf-8',
|
|
661
|
-
stdio: 'pipe'
|
|
662
|
-
});
|
|
663
|
-
// Get worktree listing (simulate what it would look like)
|
|
664
|
-
// For now we'll use main, but in reality worktrees start with the same files
|
|
665
|
-
const worktreeFiles = mainFiles;
|
|
666
|
-
// Check for package.json to understand the project type
|
|
667
|
-
let packageJsonContent = '';
|
|
1045
|
+
// Check if package.json exists
|
|
668
1046
|
try {
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
675
|
-
const isLastAttempt = attempt === maxAttempts - 1;
|
|
676
|
-
// Build the prompt
|
|
677
|
-
let prompt = `You are generating a ${type === 'test' ? 'test' : 'development server'} command for a git worktree.
|
|
678
|
-
|
|
679
|
-
CRITICAL CONTEXT:
|
|
680
|
-
- Main project directory: ${projectRoot}
|
|
681
|
-
- Worktrees are siblings: ${path.dirname(projectRoot)}/{project-name}-{branch}
|
|
682
|
-
- Command runs INSIDE the worktree (not main)
|
|
683
|
-
- Files like .env, .wrangler, node_modules are NOT shared between worktrees
|
|
684
|
-
|
|
685
|
-
Files in MAIN directory:
|
|
686
|
-
${mainFiles}
|
|
687
|
-
|
|
688
|
-
Files in WORKTREE (initially same as main):
|
|
689
|
-
${worktreeFiles}
|
|
690
|
-
|
|
691
|
-
${packageJsonContent ? `package.json contents:\n${packageJsonContent.substring(0, 3000)}\n` : ''}
|
|
692
|
-
|
|
693
|
-
${requestedFile}
|
|
694
|
-
|
|
695
|
-
${changeRequest ? `User's requested change: ${changeRequest}\n` : ''}
|
|
696
|
-
|
|
697
|
-
Your task: Generate a command to ${type === 'test' ? 'run tests' : 'start a dev server'} in the worktree.
|
|
698
|
-
|
|
699
|
-
Consider:
|
|
700
|
-
1. Copy needed files from main (e.g., cp ../${path.basename(projectRoot)}/.env .)
|
|
701
|
-
2. Install dependencies (npm/pnpm/yarn install)
|
|
702
|
-
3. Build if needed
|
|
703
|
-
4. Run the ${type} command
|
|
704
|
-
|
|
705
|
-
${isLastAttempt ? 'YOU MUST PROVIDE THE FINAL COMMAND NOW. No more file requests allowed.' : 'You can request ONE file to read, then you must provide the command.'}
|
|
706
|
-
|
|
707
|
-
Respond with ONLY a JSON object:
|
|
708
|
-
|
|
709
|
-
${!isLastAttempt ? `To read ONE file (only one chance):
|
|
710
|
-
{
|
|
711
|
-
"type": "cat",
|
|
712
|
-
"path": "path/to/file"
|
|
713
|
-
}
|
|
714
|
-
|
|
715
|
-
OR ` : ''}To provide the final command:
|
|
716
|
-
{
|
|
717
|
-
"type": "command",
|
|
718
|
-
"command": "cp ../${path.basename(projectRoot)}/.env . && npm install && npm run ${type}",
|
|
719
|
-
"description": "Copy env, install deps, run ${type}"
|
|
720
|
-
}`;
|
|
721
|
-
// Write prompt to a temporary file
|
|
722
|
-
const tmpFile = `/tmp/dmux-prompt-${Date.now()}.txt`;
|
|
723
|
-
await fs.writeFile(tmpFile, prompt);
|
|
724
|
-
// Use Claude to generate the response
|
|
725
|
-
const result = execSync(`${claudeCmd} -p "$(cat ${tmpFile})" --output-format json`, {
|
|
726
|
-
encoding: 'utf-8',
|
|
727
|
-
stdio: 'pipe',
|
|
728
|
-
maxBuffer: 1024 * 1024 * 10
|
|
729
|
-
});
|
|
730
|
-
// Clean up temp file
|
|
731
|
-
try {
|
|
732
|
-
await fs.unlink(tmpFile);
|
|
1047
|
+
await fs.access(path.join(projectRoot, 'package.json'));
|
|
1048
|
+
// Check for lock files to determine package manager
|
|
1049
|
+
const files = await fs.readdir(projectRoot);
|
|
1050
|
+
if (files.includes('pnpm-lock.yaml')) {
|
|
1051
|
+
return { manager: 'pnpm', hasPackageJson: true };
|
|
733
1052
|
}
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
let response;
|
|
737
|
-
try {
|
|
738
|
-
const wrapper = JSON.parse(result);
|
|
739
|
-
let actualResult = wrapper.result || wrapper;
|
|
740
|
-
if (typeof actualResult === 'string') {
|
|
741
|
-
actualResult = actualResult.trim();
|
|
742
|
-
// Extract JSON from the response
|
|
743
|
-
const jsonMatch = actualResult.match(/\{[\s\S]*\}/);
|
|
744
|
-
if (jsonMatch) {
|
|
745
|
-
let jsonStr = jsonMatch[0];
|
|
746
|
-
// Clean up markdown code blocks if present
|
|
747
|
-
if (actualResult.includes('```')) {
|
|
748
|
-
const codeBlockMatch = actualResult.match(/```(?:json)?\s*([\s\S]*?)```/);
|
|
749
|
-
if (codeBlockMatch) {
|
|
750
|
-
jsonStr = codeBlockMatch[1].trim();
|
|
751
|
-
}
|
|
752
|
-
}
|
|
753
|
-
actualResult = JSON.parse(jsonStr);
|
|
754
|
-
}
|
|
755
|
-
else {
|
|
756
|
-
throw new Error('No JSON object found in response');
|
|
757
|
-
}
|
|
758
|
-
}
|
|
759
|
-
response = actualResult;
|
|
760
|
-
}
|
|
761
|
-
catch (parseError) {
|
|
762
|
-
console.error('Failed to parse Claude response:', result);
|
|
763
|
-
throw new Error(`Failed to parse AI response: ${parseError}`);
|
|
1053
|
+
else if (files.includes('yarn.lock')) {
|
|
1054
|
+
return { manager: 'yarn', hasPackageJson: true };
|
|
764
1055
|
}
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
// Read the requested file
|
|
768
|
-
const targetPath = path.join(projectRoot, response.path);
|
|
769
|
-
try {
|
|
770
|
-
const content = await fs.readFile(targetPath, 'utf-8');
|
|
771
|
-
const truncated = content.length > 4000
|
|
772
|
-
? content.substring(0, 4000) + '\n... [truncated]'
|
|
773
|
-
: content;
|
|
774
|
-
requestedFile = `\nContents of ${response.path}:\n${truncated}\n`;
|
|
775
|
-
}
|
|
776
|
-
catch (err) {
|
|
777
|
-
requestedFile = `\nError reading ${response.path}: File not found\n`;
|
|
778
|
-
}
|
|
779
|
-
// Continue to next iteration to get the command
|
|
1056
|
+
else if (files.includes('package-lock.json')) {
|
|
1057
|
+
return { manager: 'npm', hasPackageJson: true };
|
|
780
1058
|
}
|
|
781
|
-
else
|
|
782
|
-
//
|
|
783
|
-
|
|
784
|
-
setStatusMessage('');
|
|
785
|
-
return response.command;
|
|
1059
|
+
else {
|
|
1060
|
+
// Default to npm if no lock file found
|
|
1061
|
+
return { manager: 'npm', hasPackageJson: true };
|
|
786
1062
|
}
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
1063
|
+
}
|
|
1064
|
+
catch {
|
|
1065
|
+
// No package.json found
|
|
1066
|
+
return { manager: null, hasPackageJson: false };
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
catch {
|
|
1070
|
+
return { manager: null, hasPackageJson: false };
|
|
1071
|
+
}
|
|
1072
|
+
};
|
|
1073
|
+
const suggestCommand = async (type) => {
|
|
1074
|
+
const { manager, hasPackageJson } = await detectPackageManager();
|
|
1075
|
+
if (!hasPackageJson) {
|
|
1076
|
+
return null;
|
|
1077
|
+
}
|
|
1078
|
+
// Suggest standard commands based on package manager
|
|
1079
|
+
if (type === 'test') {
|
|
1080
|
+
return `${manager} run test`;
|
|
1081
|
+
}
|
|
1082
|
+
else {
|
|
1083
|
+
return `${manager} run dev`;
|
|
1084
|
+
}
|
|
1085
|
+
};
|
|
1086
|
+
const copyNonGitFiles = async (worktreePath) => {
|
|
1087
|
+
try {
|
|
1088
|
+
setStatusMessage('Copying non-git files from main...');
|
|
1089
|
+
// Get project root
|
|
1090
|
+
const projectRoot = execSync('git rev-parse --show-toplevel', {
|
|
1091
|
+
encoding: 'utf-8',
|
|
1092
|
+
stdio: 'pipe'
|
|
1093
|
+
}).trim();
|
|
1094
|
+
// Use rsync to copy non-tracked files
|
|
1095
|
+
// This copies everything except git-tracked files, .git, and common build directories
|
|
1096
|
+
const rsyncCmd = `rsync -avz --exclude='.git' --exclude='node_modules' --exclude='dist' --exclude='build' --exclude='.next' --exclude='.turbo' "${projectRoot}/" "${worktreePath}/"`;
|
|
1097
|
+
execSync(rsyncCmd, { stdio: 'pipe' });
|
|
1098
|
+
setStatusMessage('Non-git files copied successfully');
|
|
1099
|
+
setTimeout(() => setStatusMessage(''), 2000);
|
|
1100
|
+
}
|
|
1101
|
+
catch (error) {
|
|
1102
|
+
setStatusMessage('Failed to copy non-git files');
|
|
1103
|
+
setTimeout(() => setStatusMessage(''), 2000);
|
|
1104
|
+
}
|
|
1105
|
+
};
|
|
1106
|
+
const runCommandInternal = async (type, pane) => {
|
|
1107
|
+
if (!pane.worktreePath) {
|
|
1108
|
+
setStatusMessage('No worktree path for this pane');
|
|
1109
|
+
setTimeout(() => setStatusMessage(''), 2000);
|
|
1110
|
+
return;
|
|
1111
|
+
}
|
|
1112
|
+
const command = type === 'test' ? projectSettings.testCommand : projectSettings.devCommand;
|
|
1113
|
+
if (!command) {
|
|
1114
|
+
setStatusMessage('No command configured');
|
|
1115
|
+
setTimeout(() => setStatusMessage(''), 2000);
|
|
1116
|
+
return;
|
|
1117
|
+
}
|
|
1118
|
+
try {
|
|
1119
|
+
setRunningCommand(true);
|
|
1120
|
+
setStatusMessage(`Starting ${type} in background window...`);
|
|
1121
|
+
// Kill existing window if present
|
|
1122
|
+
const existingWindowId = type === 'test' ? pane.testWindowId : pane.devWindowId;
|
|
1123
|
+
if (existingWindowId) {
|
|
1124
|
+
try {
|
|
1125
|
+
execSync(`tmux kill-window -t '${existingWindowId}'`, { stdio: 'pipe' });
|
|
790
1126
|
}
|
|
1127
|
+
catch { }
|
|
791
1128
|
}
|
|
792
|
-
|
|
1129
|
+
// Create a new background window for the command
|
|
1130
|
+
const windowName = `${pane.slug}-${type}`;
|
|
1131
|
+
const windowId = execSync(`tmux new-window -d -n '${windowName}' -P -F '#{window_id}'`, { encoding: 'utf-8', stdio: 'pipe' }).trim();
|
|
1132
|
+
// Create a log file to capture output
|
|
1133
|
+
const logFile = `/tmp/dmux-${pane.id}-${type}.log`;
|
|
1134
|
+
// Build the command with output capture
|
|
1135
|
+
const fullCommand = `cd "${pane.worktreePath}" && ${command} 2>&1 | tee ${logFile}`;
|
|
1136
|
+
// Send the command to the new window
|
|
1137
|
+
execSync(`tmux send-keys -t '${windowId}' '${fullCommand.replace(/'/g, "'\\''")}' Enter`, { stdio: 'pipe' });
|
|
1138
|
+
// Update pane with window info
|
|
1139
|
+
const updatedPane = {
|
|
1140
|
+
...pane,
|
|
1141
|
+
[type === 'test' ? 'testWindowId' : 'devWindowId']: windowId,
|
|
1142
|
+
[type === 'test' ? 'testStatus' : 'devStatus']: 'running'
|
|
1143
|
+
};
|
|
1144
|
+
const updatedPanes = panes.map(p => p.id === pane.id ? updatedPane : p);
|
|
1145
|
+
await savePanes(updatedPanes);
|
|
1146
|
+
// Start monitoring the output
|
|
1147
|
+
if (type === 'test') {
|
|
1148
|
+
// For tests, monitor for completion
|
|
1149
|
+
setTimeout(() => monitorTestOutput(pane.id, logFile), 2000);
|
|
1150
|
+
}
|
|
1151
|
+
else {
|
|
1152
|
+
// For dev, monitor for server URL
|
|
1153
|
+
setTimeout(() => monitorDevOutput(pane.id, logFile), 2000);
|
|
1154
|
+
}
|
|
1155
|
+
setRunningCommand(false);
|
|
1156
|
+
setStatusMessage(`${type === 'test' ? 'Test' : 'Dev server'} started in background`);
|
|
1157
|
+
setTimeout(() => setStatusMessage(''), 3000);
|
|
793
1158
|
}
|
|
794
1159
|
catch (error) {
|
|
795
|
-
|
|
796
|
-
setStatusMessage(`Failed to
|
|
1160
|
+
setRunningCommand(false);
|
|
1161
|
+
setStatusMessage(`Failed to run ${type} command`);
|
|
797
1162
|
setTimeout(() => setStatusMessage(''), 3000);
|
|
798
|
-
return null;
|
|
799
1163
|
}
|
|
800
1164
|
};
|
|
801
1165
|
const runCommand = async (type, pane) => {
|
|
@@ -805,11 +1169,21 @@ OR ` : ''}To provide the final command:
|
|
|
805
1169
|
return;
|
|
806
1170
|
}
|
|
807
1171
|
const command = type === 'test' ? projectSettings.testCommand : projectSettings.devCommand;
|
|
1172
|
+
const isFirstRun = type === 'test' ? !projectSettings.firstTestRun : !projectSettings.firstDevRun;
|
|
808
1173
|
if (!command) {
|
|
809
1174
|
// No command configured, prompt user
|
|
810
1175
|
setShowCommandPrompt(type);
|
|
811
1176
|
return;
|
|
812
1177
|
}
|
|
1178
|
+
// Check if this is the first run and offer to copy non-git files
|
|
1179
|
+
if (isFirstRun) {
|
|
1180
|
+
// Show file copy prompt and wait for response
|
|
1181
|
+
setShowFileCopyPrompt(true);
|
|
1182
|
+
setCurrentCommandType(type);
|
|
1183
|
+
setStatusMessage(`First time running ${type} command...`);
|
|
1184
|
+
// Return here - the actual command will be run after user responds to prompt
|
|
1185
|
+
return;
|
|
1186
|
+
}
|
|
813
1187
|
try {
|
|
814
1188
|
setRunningCommand(true);
|
|
815
1189
|
setStatusMessage(`Starting ${type} in background window...`);
|
|
@@ -997,8 +1371,38 @@ OR ` : ''}To provide the final command:
|
|
|
997
1371
|
execSync(`git -C "${pane.worktreePath}" commit -m '${escapedMessage}'`, { stdio: 'pipe' });
|
|
998
1372
|
}
|
|
999
1373
|
setStatusMessage('Merging into main...');
|
|
1000
|
-
//
|
|
1001
|
-
|
|
1374
|
+
// Try to merge the worktree branch
|
|
1375
|
+
try {
|
|
1376
|
+
execSync(`git merge ${pane.slug}`, { stdio: 'pipe' });
|
|
1377
|
+
}
|
|
1378
|
+
catch (mergeError) {
|
|
1379
|
+
// Check if this is a merge conflict
|
|
1380
|
+
const errorMessage = mergeError.message || mergeError.toString();
|
|
1381
|
+
if (errorMessage.includes('CONFLICT') || errorMessage.includes('conflict')) {
|
|
1382
|
+
// Merge conflict detected - exit dmux and inform user
|
|
1383
|
+
// Merge conflict detected - exit dmux and inform user
|
|
1384
|
+
process.stderr.write('\n\x1b[31m✗ Merge conflict detected!\x1b[0m\n');
|
|
1385
|
+
process.stderr.write(`\nThere are merge conflicts when merging branch '${pane.slug}' into '${mainBranch}'.\n`);
|
|
1386
|
+
process.stderr.write('\nTo resolve:\n');
|
|
1387
|
+
process.stderr.write('1. Manually resolve the merge conflicts in your editor\n');
|
|
1388
|
+
process.stderr.write('2. Stage the resolved files: git add <resolved-files>\n');
|
|
1389
|
+
process.stderr.write('3. Complete the merge: git commit\n');
|
|
1390
|
+
process.stderr.write('4. Run dmux again to continue managing your panes\n');
|
|
1391
|
+
process.stderr.write('\nExiting dmux now...\n\n');
|
|
1392
|
+
// Clean exit
|
|
1393
|
+
process.stdout.write('\x1b[2J\x1b[H');
|
|
1394
|
+
process.stdout.write('\x1b[3J');
|
|
1395
|
+
try {
|
|
1396
|
+
execSync('tmux clear-history', { stdio: 'pipe' });
|
|
1397
|
+
}
|
|
1398
|
+
catch { }
|
|
1399
|
+
process.exit(1);
|
|
1400
|
+
}
|
|
1401
|
+
else {
|
|
1402
|
+
// Some other merge error
|
|
1403
|
+
throw mergeError;
|
|
1404
|
+
}
|
|
1405
|
+
}
|
|
1002
1406
|
// Remove worktree
|
|
1003
1407
|
execSync(`git worktree remove "${pane.worktreePath}"`, { stdio: 'pipe' });
|
|
1004
1408
|
// Delete branch
|
|
@@ -1030,24 +1434,55 @@ OR ` : ''}To provide the final command:
|
|
|
1030
1434
|
exit();
|
|
1031
1435
|
};
|
|
1032
1436
|
useInput(async (input, key) => {
|
|
1033
|
-
if (isCreatingPane ||
|
|
1437
|
+
if (isCreatingPane || runningCommand) {
|
|
1034
1438
|
// Disable input while performing operations
|
|
1035
1439
|
return;
|
|
1036
1440
|
}
|
|
1441
|
+
if (showFileCopyPrompt) {
|
|
1442
|
+
if (input === 'y' || input === 'Y') {
|
|
1443
|
+
setShowFileCopyPrompt(false);
|
|
1444
|
+
const selectedPane = panes[selectedIndex];
|
|
1445
|
+
if (selectedPane && selectedPane.worktreePath && currentCommandType) {
|
|
1446
|
+
await copyNonGitFiles(selectedPane.worktreePath);
|
|
1447
|
+
// Mark as not first run and continue with command
|
|
1448
|
+
const newSettings = {
|
|
1449
|
+
...projectSettings,
|
|
1450
|
+
[currentCommandType === 'test' ? 'firstTestRun' : 'firstDevRun']: true
|
|
1451
|
+
};
|
|
1452
|
+
await saveSettings(newSettings);
|
|
1453
|
+
// Now run the actual command
|
|
1454
|
+
await runCommandInternal(currentCommandType, selectedPane);
|
|
1455
|
+
}
|
|
1456
|
+
setCurrentCommandType(null);
|
|
1457
|
+
}
|
|
1458
|
+
else if (input === 'n' || input === 'N' || key.escape) {
|
|
1459
|
+
setShowFileCopyPrompt(false);
|
|
1460
|
+
const selectedPane = panes[selectedIndex];
|
|
1461
|
+
if (selectedPane && currentCommandType) {
|
|
1462
|
+
// Mark as not first run and continue without copying
|
|
1463
|
+
const newSettings = {
|
|
1464
|
+
...projectSettings,
|
|
1465
|
+
[currentCommandType === 'test' ? 'firstTestRun' : 'firstDevRun']: true
|
|
1466
|
+
};
|
|
1467
|
+
await saveSettings(newSettings);
|
|
1468
|
+
// Now run the actual command
|
|
1469
|
+
await runCommandInternal(currentCommandType, selectedPane);
|
|
1470
|
+
}
|
|
1471
|
+
setCurrentCommandType(null);
|
|
1472
|
+
}
|
|
1473
|
+
return;
|
|
1474
|
+
}
|
|
1037
1475
|
if (showCommandPrompt) {
|
|
1038
1476
|
if (key.escape) {
|
|
1039
1477
|
setShowCommandPrompt(null);
|
|
1040
1478
|
setCommandInput('');
|
|
1041
|
-
setShowAIPrompt(false);
|
|
1042
|
-
setAIChangeRequest('');
|
|
1043
1479
|
}
|
|
1044
|
-
else if (key.return
|
|
1480
|
+
else if (key.return) {
|
|
1045
1481
|
if (commandInput.trim() === '') {
|
|
1046
|
-
//
|
|
1047
|
-
const
|
|
1048
|
-
if (
|
|
1049
|
-
setCommandInput(
|
|
1050
|
-
setShowAIPrompt(true);
|
|
1482
|
+
// If empty, suggest a default command based on package manager
|
|
1483
|
+
const suggested = await suggestCommand(showCommandPrompt);
|
|
1484
|
+
if (suggested) {
|
|
1485
|
+
setCommandInput(suggested);
|
|
1051
1486
|
}
|
|
1052
1487
|
}
|
|
1053
1488
|
else {
|
|
@@ -1059,36 +1494,22 @@ OR ` : ''}To provide the final command:
|
|
|
1059
1494
|
await saveSettings(newSettings);
|
|
1060
1495
|
const selectedPane = panes[selectedIndex];
|
|
1061
1496
|
if (selectedPane) {
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
[showCommandPrompt === 'test' ? 'testCommand' : 'devCommand']: commandInput.trim()
|
|
1075
|
-
};
|
|
1076
|
-
await saveSettings(newSettings);
|
|
1077
|
-
const selectedPane = panes[selectedIndex];
|
|
1078
|
-
if (selectedPane) {
|
|
1079
|
-
await runCommand(showCommandPrompt, selectedPane);
|
|
1497
|
+
// Check if first run
|
|
1498
|
+
const isFirstRun = showCommandPrompt === 'test' ? !projectSettings.firstTestRun : !projectSettings.firstDevRun;
|
|
1499
|
+
if (isFirstRun) {
|
|
1500
|
+
setCurrentCommandType(showCommandPrompt);
|
|
1501
|
+
setShowCommandPrompt(null);
|
|
1502
|
+
setShowFileCopyPrompt(true);
|
|
1503
|
+
}
|
|
1504
|
+
else {
|
|
1505
|
+
await runCommandInternal(showCommandPrompt, selectedPane);
|
|
1506
|
+
setShowCommandPrompt(null);
|
|
1507
|
+
setCommandInput('');
|
|
1508
|
+
}
|
|
1080
1509
|
}
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
setAIChangeRequest('');
|
|
1085
|
-
}
|
|
1086
|
-
else {
|
|
1087
|
-
// User wants changes, regenerate
|
|
1088
|
-
const generated = await generateCommand(showCommandPrompt, aiChangeRequest);
|
|
1089
|
-
if (generated) {
|
|
1090
|
-
setCommandInput(generated);
|
|
1091
|
-
setAIChangeRequest('');
|
|
1510
|
+
else {
|
|
1511
|
+
setShowCommandPrompt(null);
|
|
1512
|
+
setCommandInput('');
|
|
1092
1513
|
}
|
|
1093
1514
|
}
|
|
1094
1515
|
}
|
|
@@ -1132,13 +1553,27 @@ OR ` : ''}To provide the final command:
|
|
|
1132
1553
|
}
|
|
1133
1554
|
return;
|
|
1134
1555
|
}
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1556
|
+
// Handle directional navigation with spatial awareness based on card grid layout
|
|
1557
|
+
if (key.upArrow || key.downArrow || key.leftArrow || key.rightArrow) {
|
|
1558
|
+
let targetIndex = null;
|
|
1559
|
+
if (key.upArrow) {
|
|
1560
|
+
targetIndex = findCardInDirection(selectedIndex, 'up');
|
|
1561
|
+
}
|
|
1562
|
+
else if (key.downArrow) {
|
|
1563
|
+
targetIndex = findCardInDirection(selectedIndex, 'down');
|
|
1564
|
+
}
|
|
1565
|
+
else if (key.leftArrow) {
|
|
1566
|
+
targetIndex = findCardInDirection(selectedIndex, 'left');
|
|
1567
|
+
}
|
|
1568
|
+
else if (key.rightArrow) {
|
|
1569
|
+
targetIndex = findCardInDirection(selectedIndex, 'right');
|
|
1570
|
+
}
|
|
1571
|
+
if (targetIndex !== null) {
|
|
1572
|
+
setSelectedIndex(targetIndex);
|
|
1573
|
+
}
|
|
1574
|
+
return;
|
|
1140
1575
|
}
|
|
1141
|
-
|
|
1576
|
+
if (input === 'q') {
|
|
1142
1577
|
cleanExit();
|
|
1143
1578
|
}
|
|
1144
1579
|
else if (input === 'n' || (key.return && selectedIndex === panes.length)) {
|
|
@@ -1208,6 +1643,9 @@ OR ` : ''}To provide the final command:
|
|
|
1208
1643
|
React.createElement(Text, { color: selectedIndex === index ? 'cyan' : 'white', bold: true, wrap: "truncate" }, pane.slug),
|
|
1209
1644
|
pane.worktreePath && (React.createElement(Text, { color: "gray" }, " (wt)"))),
|
|
1210
1645
|
React.createElement(Text, { color: "gray", dimColor: true, wrap: "truncate" }, pane.prompt.substring(0, 30)),
|
|
1646
|
+
pane.claudeStatus && (React.createElement(Box, null,
|
|
1647
|
+
pane.claudeStatus === 'working' && (React.createElement(Text, { color: "cyan" }, "\u273B Working...")),
|
|
1648
|
+
pane.claudeStatus === 'waiting' && (React.createElement(Text, { color: "yellow", bold: true }, "\u26A0 Needs attention")))),
|
|
1211
1649
|
(pane.testStatus || pane.devStatus) && (React.createElement(Box, null,
|
|
1212
1650
|
pane.testStatus === 'running' && (React.createElement(Text, { color: "yellow" }, "\u23F3 Test")),
|
|
1213
1651
|
pane.testStatus === 'passed' && (React.createElement(Text, { color: "green" }, "\u2713 Test")),
|
|
@@ -1267,7 +1705,7 @@ OR ` : ''}To provide the final command:
|
|
|
1267
1705
|
React.createElement(Text, { color: selectedCloseOption === 3 ? 'cyan' : 'white' },
|
|
1268
1706
|
selectedCloseOption === 3 ? '▶ ' : ' ',
|
|
1269
1707
|
"Just Close - Close pane only")))))),
|
|
1270
|
-
showCommandPrompt &&
|
|
1708
|
+
showCommandPrompt && (React.createElement(Box, { borderStyle: "double", borderColor: "magenta", paddingX: 1, marginTop: 1 },
|
|
1271
1709
|
React.createElement(Box, { flexDirection: "column" },
|
|
1272
1710
|
React.createElement(Text, { color: "magenta", bold: true },
|
|
1273
1711
|
"Configure ",
|
|
@@ -1277,24 +1715,16 @@ OR ` : ''}To provide the final command:
|
|
|
1277
1715
|
"Enter command to run ",
|
|
1278
1716
|
showCommandPrompt === 'test' ? 'tests' : 'dev server',
|
|
1279
1717
|
" in worktrees"),
|
|
1280
|
-
React.createElement(Text, { dimColor: true }, "(Press Enter with empty input
|
|
1718
|
+
React.createElement(Text, { dimColor: true }, "(Press Enter with empty input for suggested command, ESC to cancel)"),
|
|
1281
1719
|
React.createElement(Box, { marginTop: 1 },
|
|
1282
1720
|
React.createElement(TextInput, { value: commandInput, onChange: setCommandInput, placeholder: showCommandPrompt === 'test' ? 'e.g., npm test, pnpm test' : 'e.g., npm run dev, pnpm dev' }))))),
|
|
1283
|
-
|
|
1721
|
+
showFileCopyPrompt && (React.createElement(Box, { borderStyle: "double", borderColor: "yellow", paddingX: 1, marginTop: 1 },
|
|
1284
1722
|
React.createElement(Box, { flexDirection: "column" },
|
|
1285
|
-
React.createElement(Text, { color: "
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
" Command"),
|
|
1289
|
-
React.createElement(Box, { marginTop: 1, borderStyle: "single", borderColor: "gray", paddingX: 1 },
|
|
1290
|
-
React.createElement(Text, null, commandInput)),
|
|
1291
|
-
React.createElement(Box, { marginTop: 1 },
|
|
1292
|
-
React.createElement(Text, { dimColor: true }, "Press Enter to accept, or describe changes needed:")),
|
|
1723
|
+
React.createElement(Text, { color: "yellow", bold: true }, "First Run Setup"),
|
|
1724
|
+
React.createElement(Text, null, "Copy non-git files (like .env, configs) from main to worktree?"),
|
|
1725
|
+
React.createElement(Text, { dimColor: true }, "This includes files not tracked by git but excludes node_modules, dist, etc."),
|
|
1293
1726
|
React.createElement(Box, { marginTop: 1 },
|
|
1294
|
-
React.createElement(
|
|
1295
|
-
generatingCommand && (React.createElement(Box, { borderStyle: "single", borderColor: "yellow", paddingX: 1, marginTop: 1 },
|
|
1296
|
-
React.createElement(Text, { color: "yellow" },
|
|
1297
|
-
React.createElement(Text, { bold: true }, "\u23F3 Generating command with AI...")))),
|
|
1727
|
+
React.createElement(Text, null, "(y/n):"))))),
|
|
1298
1728
|
runningCommand && (React.createElement(Box, { borderStyle: "single", borderColor: "blue", paddingX: 1, marginTop: 1 },
|
|
1299
1729
|
React.createElement(Text, { color: "blue" },
|
|
1300
1730
|
React.createElement(Text, { bold: true }, "\u25B6 Running command...")))),
|
|
@@ -1302,7 +1732,20 @@ OR ` : ''}To provide the final command:
|
|
|
1302
1732
|
React.createElement(Text, { color: "green" }, statusMessage))),
|
|
1303
1733
|
!showNewPaneDialog && !showCommandPrompt && (React.createElement(Box, { marginTop: 1, flexDirection: "column" },
|
|
1304
1734
|
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"),
|
|
1305
|
-
React.createElement(Text, { dimColor: true }, "Use \u2191\u2193
|
|
1735
|
+
React.createElement(Text, { dimColor: true }, "Use arrow keys (\u2191\u2193\u2190\u2192) for spatial navigation, Enter to select"),
|
|
1736
|
+
process.env.DEBUG_DMUX && (React.createElement(Text, { dimColor: true },
|
|
1737
|
+
"Grid: ",
|
|
1738
|
+
Math.max(1, Math.floor(terminalWidth / 37)),
|
|
1739
|
+
" cols \u00D7 ",
|
|
1740
|
+
Math.ceil((panes.length + 1) / Math.max(1, Math.floor(terminalWidth / 37))),
|
|
1741
|
+
" rows | Selected: ",
|
|
1742
|
+
(() => {
|
|
1743
|
+
const pos = getCardGridPosition(selectedIndex);
|
|
1744
|
+
return ` row ${pos.row}, col ${pos.col}`;
|
|
1745
|
+
})(),
|
|
1746
|
+
" | Terminal: ",
|
|
1747
|
+
terminalWidth,
|
|
1748
|
+
"w")))),
|
|
1306
1749
|
React.createElement(Box, { marginTop: 1 },
|
|
1307
1750
|
React.createElement(Text, { dimColor: true },
|
|
1308
1751
|
"dmux v",
|