dmux 1.2.0 → 1.3.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.
- package/dist/DmuxApp.d.ts.map +1 -1
- package/dist/DmuxApp.js +482 -61
- 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
|
@@ -28,11 +28,23 @@ const DmuxApp = ({ dmuxDir, panesFile, projectName, sessionName, settingsFile })
|
|
|
28
28
|
const [generatingCommand, setGeneratingCommand] = useState(false);
|
|
29
29
|
const [runningCommand, setRunningCommand] = useState(false);
|
|
30
30
|
const { exit } = useApp();
|
|
31
|
+
// Track terminal dimensions for responsive layout
|
|
32
|
+
const [terminalWidth, setTerminalWidth] = useState(process.stdout.columns || 80);
|
|
31
33
|
// Load panes and settings on mount and refresh periodically
|
|
32
34
|
useEffect(() => {
|
|
33
35
|
loadPanes();
|
|
34
36
|
loadSettings();
|
|
35
37
|
const interval = setInterval(loadPanes, 2000);
|
|
38
|
+
// Handle terminal resize
|
|
39
|
+
const handleResize = () => {
|
|
40
|
+
setTerminalWidth(process.stdout.columns || 80);
|
|
41
|
+
};
|
|
42
|
+
// Add resize listener
|
|
43
|
+
process.stdout.on('resize', handleResize);
|
|
44
|
+
// Add Claude status monitoring
|
|
45
|
+
const claudeInterval = setInterval(() => {
|
|
46
|
+
monitorClaudeStatus();
|
|
47
|
+
}, 200);
|
|
36
48
|
// Add cleanup handlers for process termination
|
|
37
49
|
const handleTermination = () => {
|
|
38
50
|
// Clear screen before exit
|
|
@@ -48,10 +60,89 @@ const DmuxApp = ({ dmuxDir, panesFile, projectName, sessionName, settingsFile })
|
|
|
48
60
|
process.on('SIGTERM', handleTermination);
|
|
49
61
|
return () => {
|
|
50
62
|
clearInterval(interval);
|
|
63
|
+
process.stdout.removeListener('resize', handleResize);
|
|
64
|
+
clearInterval(claudeInterval);
|
|
51
65
|
process.removeListener('SIGINT', handleTermination);
|
|
52
66
|
process.removeListener('SIGTERM', handleTermination);
|
|
53
67
|
};
|
|
54
68
|
}, []);
|
|
69
|
+
const monitorClaudeStatus = async () => {
|
|
70
|
+
// Monitor Claude Code status for all panes
|
|
71
|
+
const updatedPanes = await Promise.all(panes.map(async (pane) => {
|
|
72
|
+
try {
|
|
73
|
+
// Skip if recently checked (within 150ms to avoid overlapping checks)
|
|
74
|
+
if (pane.lastClaudeCheck && Date.now() - pane.lastClaudeCheck < 150) {
|
|
75
|
+
return pane;
|
|
76
|
+
}
|
|
77
|
+
// Capture the last 20 lines of the pane
|
|
78
|
+
const captureOutput = execSync(`tmux capture-pane -t '${pane.paneId}' -p -S -20`, { encoding: 'utf-8', stdio: 'pipe' });
|
|
79
|
+
// Pattern detection for Claude Code states
|
|
80
|
+
// Working patterns - Claude is processing
|
|
81
|
+
// Look for the loading symbols and "(esc to interrupt)"
|
|
82
|
+
const workingPatterns = [
|
|
83
|
+
/\(esc to interrupt\)/i, // Most reliable indicator
|
|
84
|
+
/✻|✢|✽|·|✳/, // Loading symbols you identified
|
|
85
|
+
/⠋|⠙|⠹|⠸|⠼|⠴|⠦|⠧|⠇|⠏/, // Braille spinner characters (might also be used)
|
|
86
|
+
];
|
|
87
|
+
// Permission/attention patterns - needs user input
|
|
88
|
+
const attentionPatterns = [
|
|
89
|
+
/Do you want to (allow|approve|grant|trust)/i,
|
|
90
|
+
/Permission required/i,
|
|
91
|
+
/\[y\/n\]/i,
|
|
92
|
+
/\(y\/n\)/i,
|
|
93
|
+
/Would you like to/i,
|
|
94
|
+
/Continue\?/i,
|
|
95
|
+
/Proceed\?/i,
|
|
96
|
+
/Accept.*\?/i,
|
|
97
|
+
/Allow.*\?/i,
|
|
98
|
+
/Approve.*\?/i,
|
|
99
|
+
/Grant.*\?/i,
|
|
100
|
+
/Trust.*\?/i,
|
|
101
|
+
/press.*enter.*continue/i,
|
|
102
|
+
/press.*enter.*accept/i,
|
|
103
|
+
/waiting for.*input/i,
|
|
104
|
+
/requires.*permission/i,
|
|
105
|
+
/needs.*approval/i
|
|
106
|
+
];
|
|
107
|
+
// Check if Claude is working
|
|
108
|
+
const isWorking = workingPatterns.some(pattern => pattern.test(captureOutput));
|
|
109
|
+
// Check if Claude needs attention
|
|
110
|
+
const needsAttention = attentionPatterns.some(pattern => pattern.test(captureOutput));
|
|
111
|
+
// Determine status
|
|
112
|
+
let newStatus = 'idle';
|
|
113
|
+
if (isWorking) {
|
|
114
|
+
newStatus = 'working';
|
|
115
|
+
}
|
|
116
|
+
else if (needsAttention) {
|
|
117
|
+
newStatus = 'waiting';
|
|
118
|
+
}
|
|
119
|
+
// Return updated pane if status changed
|
|
120
|
+
if (pane.claudeStatus !== newStatus) {
|
|
121
|
+
return {
|
|
122
|
+
...pane,
|
|
123
|
+
claudeStatus: newStatus,
|
|
124
|
+
lastClaudeCheck: Date.now()
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
// Just update timestamp
|
|
128
|
+
return {
|
|
129
|
+
...pane,
|
|
130
|
+
lastClaudeCheck: Date.now()
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
catch (error) {
|
|
134
|
+
// If we can't capture the pane, it might be dead
|
|
135
|
+
return pane;
|
|
136
|
+
}
|
|
137
|
+
}));
|
|
138
|
+
// Only update state if something changed
|
|
139
|
+
const hasChanges = updatedPanes.some((pane, index) => pane.claudeStatus !== panes[index]?.claudeStatus);
|
|
140
|
+
if (hasChanges) {
|
|
141
|
+
setPanes(updatedPanes);
|
|
142
|
+
// Save to file
|
|
143
|
+
await fs.writeFile(panesFile, JSON.stringify(updatedPanes, null, 2));
|
|
144
|
+
}
|
|
145
|
+
};
|
|
55
146
|
const loadSettings = async () => {
|
|
56
147
|
try {
|
|
57
148
|
const content = await fs.readFile(settingsFile, 'utf-8');
|
|
@@ -81,10 +172,9 @@ const DmuxApp = ({ dmuxDir, panesFile, projectName, sessionName, settingsFile })
|
|
|
81
172
|
}).trim().split('\n');
|
|
82
173
|
// Check if our pane ID exists in the list
|
|
83
174
|
if (paneIds.includes(pane.paneId)) {
|
|
84
|
-
// Update pane title
|
|
85
|
-
const paneTitle = pane.prompt ? pane.prompt.substring(0, 30) : pane.slug;
|
|
175
|
+
// Update pane title to match the slug
|
|
86
176
|
try {
|
|
87
|
-
execSync(`tmux select-pane -t '${pane.paneId}' -T "${
|
|
177
|
+
execSync(`tmux select-pane -t '${pane.paneId}' -T "${pane.slug}"`, { stdio: 'pipe' });
|
|
88
178
|
}
|
|
89
179
|
catch {
|
|
90
180
|
// Ignore if setting title fails
|
|
@@ -107,6 +197,163 @@ const DmuxApp = ({ dmuxDir, panesFile, projectName, sessionName, settingsFile })
|
|
|
107
197
|
setPanes([]);
|
|
108
198
|
}
|
|
109
199
|
};
|
|
200
|
+
const getPanePositions = () => {
|
|
201
|
+
try {
|
|
202
|
+
const output = execSync(`tmux list-panes -F '#{pane_id} #{pane_left} #{pane_top} #{pane_width} #{pane_height}'`, { encoding: 'utf-8', stdio: 'pipe' }).trim();
|
|
203
|
+
return output.split('\n').map(line => {
|
|
204
|
+
const [paneId, left, top, width, height] = line.split(' ');
|
|
205
|
+
return {
|
|
206
|
+
paneId,
|
|
207
|
+
left: parseInt(left),
|
|
208
|
+
top: parseInt(top),
|
|
209
|
+
width: parseInt(width),
|
|
210
|
+
height: parseInt(height)
|
|
211
|
+
};
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
catch {
|
|
215
|
+
return [];
|
|
216
|
+
}
|
|
217
|
+
};
|
|
218
|
+
// Calculate the visual grid position of cards based on their index
|
|
219
|
+
const getCardGridPosition = (index) => {
|
|
220
|
+
// Cards are displayed in a flexbox with wrapping
|
|
221
|
+
// With card width of 35 and typical terminal width of 80-120, we get 2-3 cards per row
|
|
222
|
+
const cardWidth = 35 + 2; // Card width plus gap
|
|
223
|
+
const cardsPerRow = Math.max(1, Math.floor(terminalWidth / cardWidth));
|
|
224
|
+
const row = Math.floor(index / cardsPerRow);
|
|
225
|
+
const col = index % cardsPerRow;
|
|
226
|
+
return { row, col };
|
|
227
|
+
};
|
|
228
|
+
const findCardInDirection = (currentIndex, direction) => {
|
|
229
|
+
const totalItems = panes.length + 1; // +1 for "New dmux pane" button
|
|
230
|
+
const currentPos = getCardGridPosition(currentIndex);
|
|
231
|
+
// Calculate cards per row based on current terminal width
|
|
232
|
+
const cardWidth = 35 + 2; // Card width plus gap
|
|
233
|
+
const cardsPerRow = Math.max(1, Math.floor(terminalWidth / cardWidth));
|
|
234
|
+
let targetIndex = null;
|
|
235
|
+
switch (direction) {
|
|
236
|
+
case 'up':
|
|
237
|
+
// Move up one row, same column
|
|
238
|
+
if (currentPos.row > 0) {
|
|
239
|
+
targetIndex = (currentPos.row - 1) * cardsPerRow + currentPos.col;
|
|
240
|
+
// Make sure target exists
|
|
241
|
+
if (targetIndex >= totalItems) {
|
|
242
|
+
// Try to find the last item in the row above
|
|
243
|
+
targetIndex = Math.min((currentPos.row - 1) * cardsPerRow + cardsPerRow - 1, totalItems - 1);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
break;
|
|
247
|
+
case 'down':
|
|
248
|
+
// Move down one row, same column
|
|
249
|
+
targetIndex = (currentPos.row + 1) * cardsPerRow + currentPos.col;
|
|
250
|
+
if (targetIndex >= totalItems) {
|
|
251
|
+
// If moving down from last row, try to go to "New dmux pane" if not already there
|
|
252
|
+
if (currentIndex < totalItems - 1) {
|
|
253
|
+
targetIndex = totalItems - 1;
|
|
254
|
+
}
|
|
255
|
+
else {
|
|
256
|
+
targetIndex = null;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
break;
|
|
260
|
+
case 'left':
|
|
261
|
+
// Move left one column, same row
|
|
262
|
+
if (currentPos.col > 0) {
|
|
263
|
+
targetIndex = currentIndex - 1;
|
|
264
|
+
}
|
|
265
|
+
else if (currentPos.row > 0) {
|
|
266
|
+
// Wrap to end of previous row
|
|
267
|
+
targetIndex = currentPos.row * cardsPerRow - 1;
|
|
268
|
+
if (targetIndex >= totalItems) {
|
|
269
|
+
targetIndex = totalItems - 1;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
break;
|
|
273
|
+
case 'right':
|
|
274
|
+
// Move right one column, same row
|
|
275
|
+
if (currentPos.col < cardsPerRow - 1 && currentIndex < totalItems - 1) {
|
|
276
|
+
targetIndex = currentIndex + 1;
|
|
277
|
+
}
|
|
278
|
+
else if ((currentPos.row + 1) * cardsPerRow < totalItems) {
|
|
279
|
+
// Wrap to start of next row
|
|
280
|
+
targetIndex = (currentPos.row + 1) * cardsPerRow;
|
|
281
|
+
}
|
|
282
|
+
break;
|
|
283
|
+
}
|
|
284
|
+
// Validate target index
|
|
285
|
+
if (targetIndex !== null && targetIndex >= 0 && targetIndex < totalItems) {
|
|
286
|
+
return targetIndex;
|
|
287
|
+
}
|
|
288
|
+
return null;
|
|
289
|
+
};
|
|
290
|
+
const findPaneInDirection = (currentPane, direction) => {
|
|
291
|
+
const positions = getPanePositions();
|
|
292
|
+
const currentPos = positions.find(p => p.paneId === currentPane.paneId);
|
|
293
|
+
if (!currentPos)
|
|
294
|
+
return null;
|
|
295
|
+
// Calculate center point of current pane
|
|
296
|
+
const currentCenterX = currentPos.left + currentPos.width / 2;
|
|
297
|
+
const currentCenterY = currentPos.top + currentPos.height / 2;
|
|
298
|
+
// Filter panes based on direction
|
|
299
|
+
const candidatePanes = panes.filter(pane => {
|
|
300
|
+
if (pane.paneId === currentPane.paneId)
|
|
301
|
+
return false;
|
|
302
|
+
const pos = positions.find(p => p.paneId === pane.paneId);
|
|
303
|
+
if (!pos)
|
|
304
|
+
return false;
|
|
305
|
+
const centerX = pos.left + pos.width / 2;
|
|
306
|
+
const centerY = pos.top + pos.height / 2;
|
|
307
|
+
switch (direction) {
|
|
308
|
+
case 'up':
|
|
309
|
+
return centerY < currentCenterY;
|
|
310
|
+
case 'down':
|
|
311
|
+
return centerY > currentCenterY;
|
|
312
|
+
case 'left':
|
|
313
|
+
return centerX < currentCenterX;
|
|
314
|
+
case 'right':
|
|
315
|
+
return centerX > currentCenterX;
|
|
316
|
+
default:
|
|
317
|
+
return false;
|
|
318
|
+
}
|
|
319
|
+
});
|
|
320
|
+
if (candidatePanes.length === 0)
|
|
321
|
+
return null;
|
|
322
|
+
// Find the closest pane in the given direction
|
|
323
|
+
let closestPane = candidatePanes[0];
|
|
324
|
+
let minDistance = Infinity;
|
|
325
|
+
for (const pane of candidatePanes) {
|
|
326
|
+
const pos = positions.find(p => p.paneId === pane.paneId);
|
|
327
|
+
if (!pos)
|
|
328
|
+
continue;
|
|
329
|
+
const centerX = pos.left + pos.width / 2;
|
|
330
|
+
const centerY = pos.top + pos.height / 2;
|
|
331
|
+
let distance;
|
|
332
|
+
switch (direction) {
|
|
333
|
+
case 'up':
|
|
334
|
+
case 'down':
|
|
335
|
+
// For vertical movement, prioritize vertical alignment, then distance
|
|
336
|
+
const xDiff = Math.abs(centerX - currentCenterX);
|
|
337
|
+
const yDiff = Math.abs(centerY - currentCenterY);
|
|
338
|
+
// Weight horizontal difference less than vertical
|
|
339
|
+
distance = yDiff + xDiff * 0.3;
|
|
340
|
+
break;
|
|
341
|
+
case 'left':
|
|
342
|
+
case 'right':
|
|
343
|
+
// For horizontal movement, prioritize horizontal alignment, then distance
|
|
344
|
+
const xDiff2 = Math.abs(centerX - currentCenterX);
|
|
345
|
+
const yDiff2 = Math.abs(centerY - currentCenterY);
|
|
346
|
+
// Weight vertical difference less than horizontal
|
|
347
|
+
distance = xDiff2 + yDiff2 * 0.3;
|
|
348
|
+
break;
|
|
349
|
+
}
|
|
350
|
+
if (distance < minDistance) {
|
|
351
|
+
minDistance = distance;
|
|
352
|
+
closestPane = pane;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
return closestPane;
|
|
356
|
+
};
|
|
110
357
|
const savePanes = async (newPanes) => {
|
|
111
358
|
await fs.writeFile(panesFile, JSON.stringify(newPanes, null, 2));
|
|
112
359
|
setPanes(newPanes);
|
|
@@ -314,10 +561,8 @@ const DmuxApp = ({ dmuxDir, panesFile, projectName, sessionName, settingsFile })
|
|
|
314
561
|
execSync('tmux send-keys C-l', { stdio: 'pipe' });
|
|
315
562
|
}
|
|
316
563
|
catch { }
|
|
317
|
-
//
|
|
318
|
-
|
|
319
|
-
// Wait for exit to complete
|
|
320
|
-
await new Promise(resolve => setTimeout(resolve, 500));
|
|
564
|
+
// Wait a bit for clearing to settle
|
|
565
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
321
566
|
// 4. Force tmux to refresh the display
|
|
322
567
|
try {
|
|
323
568
|
execSync('tmux refresh-client', { stdio: 'pipe' });
|
|
@@ -325,14 +570,20 @@ const DmuxApp = ({ dmuxDir, panesFile, projectName, sessionName, settingsFile })
|
|
|
325
570
|
catch { }
|
|
326
571
|
// Get current pane count to determine layout
|
|
327
572
|
const paneCount = parseInt(execSync('tmux list-panes | wc -l', { encoding: 'utf-8' }).trim());
|
|
573
|
+
// Enable pane borders to show titles
|
|
574
|
+
try {
|
|
575
|
+
execSync(`tmux set-option -g pane-border-status top`, { stdio: 'pipe' });
|
|
576
|
+
}
|
|
577
|
+
catch {
|
|
578
|
+
// Ignore if already set or fails
|
|
579
|
+
}
|
|
328
580
|
// Create new pane
|
|
329
581
|
const paneInfo = execSync(`tmux split-window -h -P -F '#{pane_id}'`, { encoding: 'utf-8' }).trim();
|
|
330
582
|
// Wait for pane creation to settle
|
|
331
583
|
await new Promise(resolve => setTimeout(resolve, 500));
|
|
332
|
-
// Set pane title
|
|
333
|
-
const paneTitle = prompt ? prompt.substring(0, 30) : slug;
|
|
584
|
+
// Set pane title to match the slug
|
|
334
585
|
try {
|
|
335
|
-
execSync(`tmux select-pane -t '${paneInfo}' -T "${
|
|
586
|
+
execSync(`tmux select-pane -t '${paneInfo}' -T "${slug}"`, { stdio: 'pipe' });
|
|
336
587
|
}
|
|
337
588
|
catch {
|
|
338
589
|
// Ignore if setting title fails
|
|
@@ -370,47 +621,117 @@ const DmuxApp = ({ dmuxDir, panesFile, projectName, sessionName, settingsFile })
|
|
|
370
621
|
execSync(`tmux send-keys -t '${paneInfo}' Enter`, { stdio: 'pipe' });
|
|
371
622
|
// Monitor for Claude Code trust prompt and auto-respond
|
|
372
623
|
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
|
-
|
|
624
|
+
// Wait for Claude to start up before checking for prompts
|
|
625
|
+
await new Promise(resolve => setTimeout(resolve, 800));
|
|
626
|
+
const maxChecks = 100; // 100 checks * 100ms = 10 seconds total
|
|
627
|
+
const checkInterval = 100; // Check every 100ms
|
|
628
|
+
let lastContent = '';
|
|
629
|
+
let stableContentCount = 0;
|
|
630
|
+
let promptHandled = false;
|
|
631
|
+
// More comprehensive trust prompt patterns
|
|
632
|
+
const trustPromptPatterns = [
|
|
633
|
+
/Do you trust the files in this folder\?/i,
|
|
634
|
+
/Trust the files in this workspace\?/i,
|
|
635
|
+
/Do you trust the authors of the files/i,
|
|
636
|
+
/Do you want to trust this workspace\?/i,
|
|
637
|
+
/trust.*files.*folder/i,
|
|
638
|
+
/trust.*workspace/i,
|
|
639
|
+
/Do you trust/i,
|
|
640
|
+
/Trust this folder/i,
|
|
641
|
+
/trust.*directory/i,
|
|
642
|
+
/permission.*grant/i,
|
|
643
|
+
/allow.*access/i,
|
|
644
|
+
/workspace.*trust/i,
|
|
645
|
+
/accept.*edits/i, // Claude's accept edits prompt
|
|
646
|
+
/permission.*mode/i, // Permission mode prompt
|
|
647
|
+
/allow.*claude/i, // Allow Claude prompt
|
|
648
|
+
/\[y\/n\]/i, // Common yes/no prompt pattern
|
|
649
|
+
/\(y\/n\)/i,
|
|
650
|
+
/Yes\/No/i,
|
|
651
|
+
/\[Y\/n\]/i, // Default yes pattern
|
|
652
|
+
/press.*enter.*accept/i, // Press enter to accept
|
|
653
|
+
/press.*enter.*continue/i // Press enter to continue
|
|
654
|
+
];
|
|
655
|
+
for (let i = 0; i < maxChecks; i++) {
|
|
656
|
+
await new Promise(resolve => setTimeout(resolve, checkInterval));
|
|
657
|
+
try {
|
|
658
|
+
// Capture the pane content
|
|
659
|
+
const paneContent = execSync(`tmux capture-pane -t '${paneInfo}' -p -S -30`, // Capture last 30 lines
|
|
660
|
+
{ encoding: 'utf-8', stdio: 'pipe' });
|
|
661
|
+
// Check if content has stabilized (same for 3 checks = prompt is waiting)
|
|
662
|
+
if (paneContent === lastContent) {
|
|
663
|
+
stableContentCount++;
|
|
664
|
+
}
|
|
665
|
+
else {
|
|
666
|
+
stableContentCount = 0;
|
|
667
|
+
lastContent = paneContent;
|
|
668
|
+
}
|
|
669
|
+
// Look for trust prompt in the current content
|
|
670
|
+
const hasTrustPrompt = trustPromptPatterns.some(pattern => pattern.test(paneContent));
|
|
671
|
+
// Also check if we see specific Claude permission text
|
|
672
|
+
const hasClaudePermissionPrompt = paneContent.includes('Do you trust') ||
|
|
673
|
+
paneContent.includes('trust the files') ||
|
|
674
|
+
paneContent.includes('permission') ||
|
|
675
|
+
paneContent.includes('allow') ||
|
|
676
|
+
(paneContent.includes('folder') && paneContent.includes('?'));
|
|
677
|
+
if ((hasTrustPrompt || hasClaudePermissionPrompt) && !promptHandled) {
|
|
678
|
+
// Log what we detected for debugging
|
|
679
|
+
// Detected trust prompt in pane, content stable
|
|
680
|
+
// Content is stable and we found a prompt
|
|
681
|
+
if (stableContentCount >= 2) {
|
|
682
|
+
// Attempting to auto-approve trust prompt
|
|
683
|
+
// Try multiple response methods to ensure it works
|
|
684
|
+
// Method 1: Send 'y' followed by Enter (most explicit)
|
|
685
|
+
// Sending 'y' + Enter
|
|
686
|
+
execSync(`tmux send-keys -t '${paneInfo}' 'y'`, { stdio: 'pipe' });
|
|
687
|
+
await new Promise(resolve => setTimeout(resolve, 50));
|
|
688
|
+
execSync(`tmux send-keys -t '${paneInfo}' Enter`, { stdio: 'pipe' });
|
|
689
|
+
// Method 2: Just Enter (if it's a yes/no with default yes)
|
|
690
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
691
|
+
// Sending additional Enter
|
|
403
692
|
execSync(`tmux send-keys -t '${paneInfo}' Enter`, { stdio: 'pipe' });
|
|
693
|
+
// Mark as handled to avoid duplicate responses
|
|
694
|
+
promptHandled = true;
|
|
695
|
+
// Wait and check if prompt is gone
|
|
696
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
697
|
+
// Verify the prompt is gone
|
|
698
|
+
const updatedContent = execSync(`tmux capture-pane -t '${paneInfo}' -p -S -10`, { encoding: 'utf-8', stdio: 'pipe' });
|
|
699
|
+
// If trust prompt is gone, check if we need to resend the Claude command
|
|
700
|
+
const promptGone = !trustPromptPatterns.some(p => p.test(updatedContent));
|
|
701
|
+
if (promptGone) {
|
|
702
|
+
// Check if Claude is running or if we need to restart it
|
|
703
|
+
const claudeRunning = updatedContent.includes('Claude') ||
|
|
704
|
+
updatedContent.includes('claude') ||
|
|
705
|
+
updatedContent.includes('Assistant') ||
|
|
706
|
+
(prompt && updatedContent.includes(prompt.substring(0, Math.min(20, prompt.length))));
|
|
707
|
+
if (!claudeRunning && !updatedContent.includes('$')) {
|
|
708
|
+
// Claude might have exited after permission was granted, restart it
|
|
709
|
+
// Claude not running after trust approval, restarting
|
|
710
|
+
await new Promise(resolve => setTimeout(resolve, 300));
|
|
711
|
+
execSync(`tmux send-keys -t '${paneInfo}' '${escapedCmd}'`, { stdio: 'pipe' });
|
|
712
|
+
execSync(`tmux send-keys -t '${paneInfo}' Enter`, { stdio: 'pipe' });
|
|
713
|
+
}
|
|
714
|
+
// Successfully handled the prompt
|
|
715
|
+
break;
|
|
716
|
+
}
|
|
404
717
|
}
|
|
405
718
|
}
|
|
719
|
+
// If we see Claude is already running without prompts, we're done
|
|
720
|
+
if (!hasTrustPrompt && !hasClaudePermissionPrompt &&
|
|
721
|
+
(paneContent.includes('Claude') || paneContent.includes('Assistant'))) {
|
|
722
|
+
break;
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
catch (error) {
|
|
726
|
+
// Continue checking, errors are non-fatal
|
|
727
|
+
// Error checking for trust prompt
|
|
406
728
|
}
|
|
407
|
-
}
|
|
408
|
-
catch (error) {
|
|
409
|
-
// Ignore errors in auto-approval, it's a best-effort feature
|
|
410
729
|
}
|
|
411
730
|
};
|
|
412
731
|
// Start monitoring for trust prompt in background
|
|
413
|
-
autoApproveTrust()
|
|
732
|
+
autoApproveTrust().catch(err => {
|
|
733
|
+
// Error in autoApproveTrust
|
|
734
|
+
});
|
|
414
735
|
// Keep focus on the new pane
|
|
415
736
|
execSync(`tmux select-pane -t '${paneInfo}'`, { stdio: 'pipe' });
|
|
416
737
|
// Save pane info
|
|
@@ -423,7 +744,7 @@ const DmuxApp = ({ dmuxDir, panesFile, projectName, sessionName, settingsFile })
|
|
|
423
744
|
};
|
|
424
745
|
const updatedPanes = [...panes, newPane];
|
|
425
746
|
await fs.writeFile(panesFile, JSON.stringify(updatedPanes, null, 2));
|
|
426
|
-
// Switch back to the original pane (where dmux
|
|
747
|
+
// Switch back to the original pane (where dmux is running)
|
|
427
748
|
execSync(`tmux select-pane -t '${originalPaneId}'`, { stdio: 'pipe' });
|
|
428
749
|
// Re-set the title for the dmux pane
|
|
429
750
|
try {
|
|
@@ -432,13 +753,23 @@ const DmuxApp = ({ dmuxDir, panesFile, projectName, sessionName, settingsFile })
|
|
|
432
753
|
catch {
|
|
433
754
|
// Ignore if setting title fails
|
|
434
755
|
}
|
|
435
|
-
//
|
|
436
|
-
|
|
437
|
-
//
|
|
438
|
-
|
|
756
|
+
// Clear the screen and redraw the UI
|
|
757
|
+
process.stdout.write('\x1b[2J\x1b[H');
|
|
758
|
+
// Reset the creating pane flag and refresh
|
|
759
|
+
setIsCreatingPane(false);
|
|
760
|
+
setStatusMessage('');
|
|
761
|
+
// Force a reload of panes to ensure UI is up to date
|
|
762
|
+
await loadPanes();
|
|
439
763
|
};
|
|
440
764
|
const jumpToPane = (paneId) => {
|
|
441
765
|
try {
|
|
766
|
+
// Enable pane borders to show titles (if not already enabled)
|
|
767
|
+
try {
|
|
768
|
+
execSync(`tmux set-option -g pane-border-status top`, { stdio: 'pipe' });
|
|
769
|
+
}
|
|
770
|
+
catch {
|
|
771
|
+
// Ignore if already set or fails
|
|
772
|
+
}
|
|
442
773
|
execSync(`tmux select-pane -t '${paneId}'`, { stdio: 'pipe' });
|
|
443
774
|
setStatusMessage('Jumped to pane');
|
|
444
775
|
setTimeout(() => setStatusMessage(''), 2000);
|
|
@@ -527,8 +858,38 @@ const DmuxApp = ({ dmuxDir, panesFile, projectName, sessionName, settingsFile })
|
|
|
527
858
|
execSync(`git -C "${pane.worktreePath}" commit -m '${escapedMessage}'`, { stdio: 'pipe' });
|
|
528
859
|
}
|
|
529
860
|
setStatusMessage('Merging into main...');
|
|
530
|
-
//
|
|
531
|
-
|
|
861
|
+
// Try to merge the worktree branch
|
|
862
|
+
try {
|
|
863
|
+
execSync(`git merge ${pane.slug}`, { stdio: 'pipe' });
|
|
864
|
+
}
|
|
865
|
+
catch (mergeError) {
|
|
866
|
+
// Check if this is a merge conflict
|
|
867
|
+
const errorMessage = mergeError.message || mergeError.toString();
|
|
868
|
+
if (errorMessage.includes('CONFLICT') || errorMessage.includes('conflict')) {
|
|
869
|
+
// Merge conflict detected - exit dmux and inform user
|
|
870
|
+
// Merge conflict detected - exit dmux and inform user
|
|
871
|
+
process.stderr.write('\n\x1b[31m✗ Merge conflict detected!\x1b[0m\n');
|
|
872
|
+
process.stderr.write(`\nThere are merge conflicts when merging branch '${pane.slug}' into '${mainBranch}'.\n`);
|
|
873
|
+
process.stderr.write('\nTo resolve:\n');
|
|
874
|
+
process.stderr.write('1. Manually resolve the merge conflicts in your editor\n');
|
|
875
|
+
process.stderr.write('2. Stage the resolved files: git add <resolved-files>\n');
|
|
876
|
+
process.stderr.write('3. Complete the merge: git commit\n');
|
|
877
|
+
process.stderr.write('4. Run dmux again to continue managing your panes\n');
|
|
878
|
+
process.stderr.write('\nExiting dmux now...\n\n');
|
|
879
|
+
// Clean exit
|
|
880
|
+
process.stdout.write('\x1b[2J\x1b[H');
|
|
881
|
+
process.stdout.write('\x1b[3J');
|
|
882
|
+
try {
|
|
883
|
+
execSync('tmux clear-history', { stdio: 'pipe' });
|
|
884
|
+
}
|
|
885
|
+
catch { }
|
|
886
|
+
process.exit(1);
|
|
887
|
+
}
|
|
888
|
+
else {
|
|
889
|
+
// Some other merge error
|
|
890
|
+
throw mergeError;
|
|
891
|
+
}
|
|
892
|
+
}
|
|
532
893
|
// Remove worktree
|
|
533
894
|
execSync(`git worktree remove "${pane.worktreePath}"`, { stdio: 'pipe' });
|
|
534
895
|
// Delete branch
|
|
@@ -759,7 +1120,7 @@ OR ` : ''}To provide the final command:
|
|
|
759
1120
|
response = actualResult;
|
|
760
1121
|
}
|
|
761
1122
|
catch (parseError) {
|
|
762
|
-
|
|
1123
|
+
// Failed to parse Claude response
|
|
763
1124
|
throw new Error(`Failed to parse AI response: ${parseError}`);
|
|
764
1125
|
}
|
|
765
1126
|
// Handle the response
|
|
@@ -997,8 +1358,38 @@ OR ` : ''}To provide the final command:
|
|
|
997
1358
|
execSync(`git -C "${pane.worktreePath}" commit -m '${escapedMessage}'`, { stdio: 'pipe' });
|
|
998
1359
|
}
|
|
999
1360
|
setStatusMessage('Merging into main...');
|
|
1000
|
-
//
|
|
1001
|
-
|
|
1361
|
+
// Try to merge the worktree branch
|
|
1362
|
+
try {
|
|
1363
|
+
execSync(`git merge ${pane.slug}`, { stdio: 'pipe' });
|
|
1364
|
+
}
|
|
1365
|
+
catch (mergeError) {
|
|
1366
|
+
// Check if this is a merge conflict
|
|
1367
|
+
const errorMessage = mergeError.message || mergeError.toString();
|
|
1368
|
+
if (errorMessage.includes('CONFLICT') || errorMessage.includes('conflict')) {
|
|
1369
|
+
// Merge conflict detected - exit dmux and inform user
|
|
1370
|
+
// Merge conflict detected - exit dmux and inform user
|
|
1371
|
+
process.stderr.write('\n\x1b[31m✗ Merge conflict detected!\x1b[0m\n');
|
|
1372
|
+
process.stderr.write(`\nThere are merge conflicts when merging branch '${pane.slug}' into '${mainBranch}'.\n`);
|
|
1373
|
+
process.stderr.write('\nTo resolve:\n');
|
|
1374
|
+
process.stderr.write('1. Manually resolve the merge conflicts in your editor\n');
|
|
1375
|
+
process.stderr.write('2. Stage the resolved files: git add <resolved-files>\n');
|
|
1376
|
+
process.stderr.write('3. Complete the merge: git commit\n');
|
|
1377
|
+
process.stderr.write('4. Run dmux again to continue managing your panes\n');
|
|
1378
|
+
process.stderr.write('\nExiting dmux now...\n\n');
|
|
1379
|
+
// Clean exit
|
|
1380
|
+
process.stdout.write('\x1b[2J\x1b[H');
|
|
1381
|
+
process.stdout.write('\x1b[3J');
|
|
1382
|
+
try {
|
|
1383
|
+
execSync('tmux clear-history', { stdio: 'pipe' });
|
|
1384
|
+
}
|
|
1385
|
+
catch { }
|
|
1386
|
+
process.exit(1);
|
|
1387
|
+
}
|
|
1388
|
+
else {
|
|
1389
|
+
// Some other merge error
|
|
1390
|
+
throw mergeError;
|
|
1391
|
+
}
|
|
1392
|
+
}
|
|
1002
1393
|
// Remove worktree
|
|
1003
1394
|
execSync(`git worktree remove "${pane.worktreePath}"`, { stdio: 'pipe' });
|
|
1004
1395
|
// Delete branch
|
|
@@ -1132,13 +1523,27 @@ OR ` : ''}To provide the final command:
|
|
|
1132
1523
|
}
|
|
1133
1524
|
return;
|
|
1134
1525
|
}
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1526
|
+
// Handle directional navigation with spatial awareness based on card grid layout
|
|
1527
|
+
if (key.upArrow || key.downArrow || key.leftArrow || key.rightArrow) {
|
|
1528
|
+
let targetIndex = null;
|
|
1529
|
+
if (key.upArrow) {
|
|
1530
|
+
targetIndex = findCardInDirection(selectedIndex, 'up');
|
|
1531
|
+
}
|
|
1532
|
+
else if (key.downArrow) {
|
|
1533
|
+
targetIndex = findCardInDirection(selectedIndex, 'down');
|
|
1534
|
+
}
|
|
1535
|
+
else if (key.leftArrow) {
|
|
1536
|
+
targetIndex = findCardInDirection(selectedIndex, 'left');
|
|
1537
|
+
}
|
|
1538
|
+
else if (key.rightArrow) {
|
|
1539
|
+
targetIndex = findCardInDirection(selectedIndex, 'right');
|
|
1540
|
+
}
|
|
1541
|
+
if (targetIndex !== null) {
|
|
1542
|
+
setSelectedIndex(targetIndex);
|
|
1543
|
+
}
|
|
1544
|
+
return;
|
|
1140
1545
|
}
|
|
1141
|
-
|
|
1546
|
+
if (input === 'q') {
|
|
1142
1547
|
cleanExit();
|
|
1143
1548
|
}
|
|
1144
1549
|
else if (input === 'n' || (key.return && selectedIndex === panes.length)) {
|
|
@@ -1208,6 +1613,9 @@ OR ` : ''}To provide the final command:
|
|
|
1208
1613
|
React.createElement(Text, { color: selectedIndex === index ? 'cyan' : 'white', bold: true, wrap: "truncate" }, pane.slug),
|
|
1209
1614
|
pane.worktreePath && (React.createElement(Text, { color: "gray" }, " (wt)"))),
|
|
1210
1615
|
React.createElement(Text, { color: "gray", dimColor: true, wrap: "truncate" }, pane.prompt.substring(0, 30)),
|
|
1616
|
+
pane.claudeStatus && (React.createElement(Box, null,
|
|
1617
|
+
pane.claudeStatus === 'working' && (React.createElement(Text, { color: "cyan" }, "\u273B Working...")),
|
|
1618
|
+
pane.claudeStatus === 'waiting' && (React.createElement(Text, { color: "yellow", bold: true }, "\u26A0 Needs attention")))),
|
|
1211
1619
|
(pane.testStatus || pane.devStatus) && (React.createElement(Box, null,
|
|
1212
1620
|
pane.testStatus === 'running' && (React.createElement(Text, { color: "yellow" }, "\u23F3 Test")),
|
|
1213
1621
|
pane.testStatus === 'passed' && (React.createElement(Text, { color: "green" }, "\u2713 Test")),
|
|
@@ -1302,7 +1710,20 @@ OR ` : ''}To provide the final command:
|
|
|
1302
1710
|
React.createElement(Text, { color: "green" }, statusMessage))),
|
|
1303
1711
|
!showNewPaneDialog && !showCommandPrompt && (React.createElement(Box, { marginTop: 1, flexDirection: "column" },
|
|
1304
1712
|
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
|
|
1713
|
+
React.createElement(Text, { dimColor: true }, "Use arrow keys (\u2191\u2193\u2190\u2192) for spatial navigation, Enter to select"),
|
|
1714
|
+
process.env.DEBUG_DMUX && (React.createElement(Text, { dimColor: true },
|
|
1715
|
+
"Grid: ",
|
|
1716
|
+
Math.max(1, Math.floor(terminalWidth / 37)),
|
|
1717
|
+
" cols \u00D7 ",
|
|
1718
|
+
Math.ceil((panes.length + 1) / Math.max(1, Math.floor(terminalWidth / 37))),
|
|
1719
|
+
" rows | Selected: ",
|
|
1720
|
+
(() => {
|
|
1721
|
+
const pos = getCardGridPosition(selectedIndex);
|
|
1722
|
+
return ` row ${pos.row}, col ${pos.col}`;
|
|
1723
|
+
})(),
|
|
1724
|
+
" | Terminal: ",
|
|
1725
|
+
terminalWidth,
|
|
1726
|
+
"w")))),
|
|
1306
1727
|
React.createElement(Box, { marginTop: 1 },
|
|
1307
1728
|
React.createElement(Text, { dimColor: true },
|
|
1308
1729
|
"dmux v",
|