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.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 [showAIPrompt, setShowAIPrompt] = useState(false);
27
- const [aiChangeRequest, setAIChangeRequest] = useState('');
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 while we're at it
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 "${paneTitle}"`, { stdio: 'pipe' });
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
- // 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));
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 based on prompt or slug
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 "${paneTitle}"`, { stdio: 'pipe' });
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
- // 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' });
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 was running)
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
- // 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' });
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
- // Merge the worktree branch
531
- execSync(`git merge ${pane.slug}`, { stdio: 'pipe' });
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 generateCommand = async (type, changeRequest) => {
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
- // Pre-fetch directory listings
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
- packageJsonContent = await fs.readFile(path.join(projectRoot, 'package.json'), 'utf-8');
670
- }
671
- catch { }
672
- let requestedFile = '';
673
- let maxAttempts = 2; // Allow one file request, then must generate command
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
- catch { }
735
- // Parse the response
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
- // Handle the response
766
- if (response.type === 'cat' && !isLastAttempt) {
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 if (response.type === 'command') {
782
- // Got the command!
783
- setGeneratingCommand(false);
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
- else if (isLastAttempt) {
788
- // Last attempt must provide a command
789
- throw new Error('AI did not provide a command on final attempt');
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
- throw new Error('Failed to generate command after maximum attempts');
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
- setGeneratingCommand(false);
796
- setStatusMessage(`Failed to generate command: ${error}`);
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
- // Merge the worktree branch
1001
- execSync(`git merge ${pane.slug}`, { stdio: 'pipe' });
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 || generatingCommand || runningCommand) {
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 && !showAIPrompt) {
1480
+ else if (key.return) {
1045
1481
  if (commandInput.trim() === '') {
1046
- // User wants AI to generate
1047
- const generated = await generateCommand(showCommandPrompt);
1048
- if (generated) {
1049
- setCommandInput(generated);
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
- await runCommand(showCommandPrompt, selectedPane);
1063
- }
1064
- setShowCommandPrompt(null);
1065
- setCommandInput('');
1066
- setShowAIPrompt(false);
1067
- }
1068
- }
1069
- else if (key.return && showAIPrompt) {
1070
- if (aiChangeRequest.trim() === '') {
1071
- // User accepts AI generated command
1072
- const newSettings = {
1073
- ...projectSettings,
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
- setShowCommandPrompt(null);
1082
- setCommandInput('');
1083
- setShowAIPrompt(false);
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
- if (key.upArrow) {
1136
- setSelectedIndex(Math.max(0, selectedIndex - 1));
1137
- }
1138
- else if (key.downArrow) {
1139
- setSelectedIndex(Math.min(panes.length, selectedIndex + 1));
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
- else if (input === 'q') {
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 && !showAIPrompt && (React.createElement(Box, { borderStyle: "double", borderColor: "magenta", paddingX: 1, marginTop: 1 },
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 to generate with AI, ESC to cancel)"),
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
- showCommandPrompt && showAIPrompt && (React.createElement(Box, { borderStyle: "double", borderColor: "magenta", paddingX: 1, marginTop: 1 },
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: "magenta", bold: true },
1286
- "AI Generated ",
1287
- showCommandPrompt === 'test' ? 'Test' : 'Dev',
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(TextInput, { value: aiChangeRequest, onChange: setAIChangeRequest, placeholder: "e.g., 'also copy .env file' or press Enter to accept" }))))),
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 arrows to navigate, Enter to select"))),
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",