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.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 while we're at it
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 "${paneTitle}"`, { stdio: 'pipe' });
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
- // Exit Ink app cleanly before creating tmux pane (no cleanup needed here as we're re-launching)
318
- exit();
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 based on prompt or slug
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 "${paneTitle}"`, { stdio: 'pipe' });
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
- // Give Claude time to start and potentially show the trust prompt
374
- await new Promise(resolve => setTimeout(resolve, 500));
375
- try {
376
- // Capture the pane content to check for trust prompt
377
- const paneContent = execSync(`tmux capture-pane -t '${paneInfo}' -p`, { encoding: 'utf-8', stdio: 'pipe' });
378
- // Check for various versions of the trust prompt
379
- const trustPromptPatterns = [
380
- /Do you trust the files in this folder\?/i,
381
- /Trust the files in this workspace\?/i,
382
- /Do you trust the authors of the files/i,
383
- /Do you want to trust this workspace\?/i,
384
- /trust.*files.*folder/i,
385
- /trust.*workspace/i
386
- ];
387
- const hasTrustPrompt = trustPromptPatterns.some(pattern => pattern.test(paneContent));
388
- if (hasTrustPrompt) {
389
- // Auto-respond with Enter (yes) to the trust prompt
390
- execSync(`tmux send-keys -t '${paneInfo}' Enter`, { stdio: 'pipe' });
391
- // Optionally wait a bit more and check if we need to send the command again
392
- await new Promise(resolve => setTimeout(resolve, 300));
393
- // Check if Claude is now ready (no longer showing trust prompt)
394
- const updatedContent = execSync(`tmux capture-pane -t '${paneInfo}' -p`, { encoding: 'utf-8', stdio: 'pipe' });
395
- // If the trust prompt is gone but Claude hasn't started processing the prompt,
396
- // we might need to resend the command
397
- if (!trustPromptPatterns.some(p => p.test(updatedContent)) &&
398
- !updatedContent.includes(prompt.substring(0, 20))) {
399
- // Claude might need the command again after trust approval
400
- // This handles cases where the trust dialog cleared the initial command
401
- if (prompt && prompt.trim()) {
402
- execSync(`tmux send-keys -t '${paneInfo}' '${escapedCmd}'`, { stdio: 'pipe' });
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 was running)
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
- // Small delay to ensure pane is fully established before re-launching
436
- await new Promise(resolve => setTimeout(resolve, 100));
437
- // Re-launch dmux in the original pane
438
- execSync(`tmux send-keys -t '${originalPaneId}' 'dmux' Enter`, { stdio: 'pipe' });
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
- // Merge the worktree branch
531
- execSync(`git merge ${pane.slug}`, { stdio: 'pipe' });
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
- console.error('Failed to parse Claude response:', result);
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
- // Merge the worktree branch
1001
- execSync(`git merge ${pane.slug}`, { stdio: 'pipe' });
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
- if (key.upArrow) {
1136
- setSelectedIndex(Math.max(0, selectedIndex - 1));
1137
- }
1138
- else if (key.downArrow) {
1139
- setSelectedIndex(Math.min(panes.length, selectedIndex + 1));
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
- else if (input === 'q') {
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 arrows to navigate, Enter to select"))),
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",