dmux 1.3.0 → 1.3.2

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
@@ -1,7 +1,6 @@
1
1
  import React, { useState, useEffect } from 'react';
2
2
  import { Box, Text, useInput, useApp } from 'ink';
3
3
  import TextInput from 'ink-text-input';
4
- import SimpleEnhancedInput from './SimpleEnhancedInput.js';
5
4
  import { execSync } from 'child_process';
6
5
  import fs from 'fs/promises';
7
6
  import path from 'path';
@@ -23,9 +22,8 @@ const DmuxApp = ({ dmuxDir, panesFile, projectName, sessionName, settingsFile })
23
22
  const [projectSettings, setProjectSettings] = useState({});
24
23
  const [showCommandPrompt, setShowCommandPrompt] = useState(null);
25
24
  const [commandInput, setCommandInput] = useState('');
26
- const [showAIPrompt, setShowAIPrompt] = useState(false);
27
- const [aiChangeRequest, setAIChangeRequest] = useState('');
28
- const [generatingCommand, setGeneratingCommand] = useState(false);
25
+ const [showFileCopyPrompt, setShowFileCopyPrompt] = useState(false);
26
+ const [currentCommandType, setCurrentCommandType] = useState(null);
29
27
  const [runningCommand, setRunningCommand] = useState(false);
30
28
  const { exit } = useApp();
31
29
  // Track terminal dimensions for responsive layout
@@ -41,10 +39,6 @@ const DmuxApp = ({ dmuxDir, panesFile, projectName, sessionName, settingsFile })
41
39
  };
42
40
  // Add resize listener
43
41
  process.stdout.on('resize', handleResize);
44
- // Add Claude status monitoring
45
- const claudeInterval = setInterval(() => {
46
- monitorClaudeStatus();
47
- }, 200);
48
42
  // Add cleanup handlers for process termination
49
43
  const handleTermination = () => {
50
44
  // Clear screen before exit
@@ -61,88 +55,140 @@ const DmuxApp = ({ dmuxDir, panesFile, projectName, sessionName, settingsFile })
61
55
  return () => {
62
56
  clearInterval(interval);
63
57
  process.stdout.removeListener('resize', handleResize);
64
- clearInterval(claudeInterval);
65
58
  process.removeListener('SIGINT', handleTermination);
66
59
  process.removeListener('SIGTERM', handleTermination);
67
60
  };
68
61
  }, []);
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) {
62
+ // Monitor Claude status in all panes with proper dependency tracking
63
+ useEffect(() => {
64
+ if (panes.length === 0)
65
+ return;
66
+ const monitorClaudeStatus = async () => {
67
+ // Monitor Claude Code status for all panes
68
+ const updatedPanesWithNulls = await Promise.all(panes.map(async (pane) => {
69
+ try {
70
+ // Skip if recently checked (within 500ms to avoid overlapping checks)
71
+ if (pane.lastClaudeCheck && Date.now() - pane.lastClaudeCheck < 500) {
72
+ return pane;
73
+ }
74
+ // First check if pane exists before trying to capture
75
+ const paneIds = execSync(`tmux list-panes -F '#{pane_id}'`, {
76
+ encoding: 'utf-8',
77
+ stdio: 'pipe'
78
+ }).trim().split('\n');
79
+ if (!paneIds.includes(pane.paneId)) {
80
+ // Pane doesn't exist anymore, return unchanged
81
+ return pane;
82
+ }
83
+ // Capture the last 30 lines of the pane for better detection
84
+ const captureOutput = execSync(`tmux capture-pane -t '${pane.paneId}' -p -S -30`, { encoding: 'utf-8', stdio: 'pipe' });
85
+ // Pattern detection for Claude Code states
86
+ // Working patterns - Claude is processing
87
+ // The ONLY reliable indicator is "(esc to interrupt)"
88
+ const workingPatterns = [
89
+ /esc to interrupt/i, // The ONLY reliable indicator that Claude is actively working
90
+ ];
91
+ // Extract last few lines to check for patterns
92
+ const lines = captureOutput.split('\n');
93
+ const lastLines = lines.slice(-10).join('\n');
94
+ // Check if Claude's input box is present (waiting for input)
95
+ const hasInputBox = /╭─+╮/.test(lastLines) && /╰─+╯/.test(lastLines) && /│\s+>\s+.*│/.test(lastLines);
96
+ // Permission/attention patterns - needs user input (very forgiving)
97
+ const attentionPatterns = [
98
+ /\?\s*$/m, // Any line ending with a question mark
99
+ /y\/n/i, // Any y/n prompt
100
+ /yes.*no/i, // Yes or no prompts
101
+ /\ballow\b.*\?/i,
102
+ /\bapprove\b.*\?/i,
103
+ /\bgrant\b.*\?/i,
104
+ /\btrust\b.*\?/i,
105
+ /\baccept\b.*\?/i,
106
+ /\bcontinue\b.*\?/i,
107
+ /\bproceed\b.*\?/i,
108
+ /permission/i,
109
+ /confirmation/i,
110
+ /press.*enter/i,
111
+ /waiting for/i,
112
+ /are you sure/i,
113
+ /would you like/i,
114
+ /do you want/i,
115
+ /please confirm/i,
116
+ /requires.*approval/i,
117
+ /needs.*input/i,
118
+ /⏵⏵\s*accept edits/i, // Claude's accept edits mode
119
+ /shift\+tab to cycle/i, // Claude's interface hints
120
+ ];
121
+ // Check if Claude is working
122
+ const isWorking = workingPatterns.some(pattern => pattern.test(captureOutput));
123
+ // Check if Claude needs attention
124
+ const needsAttention = attentionPatterns.some(pattern => pattern.test(captureOutput)) || hasInputBox;
125
+ // Determine status - working takes precedence
126
+ let newStatus = 'idle';
127
+ if (isWorking) {
128
+ newStatus = 'working';
129
+ }
130
+ else if (needsAttention && !isWorking) {
131
+ // Only show as waiting if NOT working (working takes precedence)
132
+ newStatus = 'waiting';
133
+ }
134
+ // Additional checks for specific Claude states
135
+ // If we see "accept edits" without other working indicators, it's waiting
136
+ if (/accept edits/i.test(captureOutput) && !/esc to interrupt/i.test(captureOutput)) {
137
+ newStatus = 'waiting';
138
+ }
139
+ // If Claude's input box is visible and no working indicators, it's waiting for input
140
+ if (hasInputBox && !isWorking) {
141
+ newStatus = 'waiting';
142
+ }
143
+ // Check for specific Claude question patterns that might not end with ?
144
+ const claudeQuestionPatterns = [
145
+ /I (can|could|should|would|will|may|might)/i,
146
+ /Let me know/i,
147
+ /Please (tell|let|inform|advise)/i,
148
+ /Would you prefer/i,
149
+ /Should I (proceed|continue|go ahead)/i,
150
+ ];
151
+ if (claudeQuestionPatterns.some(pattern => pattern.test(lastLines)) && !isWorking) {
152
+ newStatus = 'waiting';
153
+ }
154
+ // Return updated pane if status changed
155
+ if (pane.claudeStatus !== newStatus) {
156
+ return {
157
+ ...pane,
158
+ claudeStatus: newStatus,
159
+ lastClaudeCheck: Date.now()
160
+ };
161
+ }
162
+ // Just update timestamp
121
163
  return {
122
164
  ...pane,
123
- claudeStatus: newStatus,
124
165
  lastClaudeCheck: Date.now()
125
166
  };
126
167
  }
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;
168
+ catch (error) {
169
+ // If we can't capture the pane, it might be dead - mark it for removal
170
+ return null; // Will be filtered out below
171
+ }
172
+ }));
173
+ // Filter out null values (dead panes) and keep only valid panes
174
+ const updatedPanes = updatedPanesWithNulls.filter((pane) => pane !== null);
175
+ // Check if we have changes (including pane removals)
176
+ const hasChanges = updatedPanes.length !== panes.length ||
177
+ updatedPanes.some((pane, index) => pane.claudeStatus !== panes[index]?.claudeStatus);
178
+ if (hasChanges) {
179
+ setPanes(updatedPanes);
180
+ // Save to file
181
+ await fs.writeFile(panesFile, JSON.stringify(updatedPanes, null, 2));
136
182
  }
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
- };
183
+ };
184
+ // Run monitoring immediately
185
+ monitorClaudeStatus();
186
+ // Set up interval for continuous monitoring
187
+ const claudeInterval = setInterval(monitorClaudeStatus, 1000); // Check every second
188
+ return () => {
189
+ clearInterval(claudeInterval);
190
+ };
191
+ }, [panes, panesFile]); // Re-run when panes change
146
192
  const loadSettings = async () => {
147
193
  try {
148
194
  const content = await fs.readFile(settingsFile, 'utf-8');
@@ -529,11 +575,49 @@ const DmuxApp = ({ dmuxDir, panesFile, projectName, sessionName, settingsFile })
529
575
  // Final fallback
530
576
  return `dmux-${Date.now()}`;
531
577
  };
578
+ const openInEditor = async () => {
579
+ try {
580
+ const os = require('os');
581
+ const fs = require('fs');
582
+ const tmpFile = path.join(os.tmpdir(), `dmux-prompt-${Date.now()}.md`);
583
+ // Write current prompt to temp file
584
+ fs.writeFileSync(tmpFile, newPanePrompt || '# Enter your Claude prompt here\n\n');
585
+ // Get editor from environment or use default
586
+ const editor = process.env.EDITOR || process.env.VISUAL || 'nano';
587
+ // Clear screen and open editor
588
+ process.stdout.write('\x1b[2J\x1b[H');
589
+ // Use spawn to open editor in foreground
590
+ const { spawn } = require('child_process');
591
+ const editorProcess = spawn(editor, [tmpFile], {
592
+ stdio: 'inherit',
593
+ shell: true
594
+ });
595
+ editorProcess.on('close', (code) => {
596
+ // Read the file back
597
+ try {
598
+ const content = fs.readFileSync(tmpFile, 'utf8')
599
+ .replace(/^# Enter your Claude prompt here\s*\n*/m, '')
600
+ .trim();
601
+ setNewPanePrompt(content);
602
+ // Clean up temp file
603
+ fs.unlinkSync(tmpFile);
604
+ // Clear screen and return to dmux
605
+ process.stdout.write('\x1b[2J\x1b[H');
606
+ }
607
+ catch (error) {
608
+ // If file read fails, just continue
609
+ }
610
+ });
611
+ }
612
+ catch (error) {
613
+ // If editor fails, just continue with inline input
614
+ }
615
+ };
532
616
  const createNewPane = async (prompt) => {
533
617
  setIsCreatingPane(true);
534
618
  setStatusMessage('Generating slug...');
535
619
  const slug = await generateSlug(prompt);
536
- setStatusMessage('Creating new pane...');
620
+ setStatusMessage(`Creating worktree: ${slug}...`);
537
621
  // Get git root directory for consistent worktree placement
538
622
  let projectRoot;
539
623
  try {
@@ -592,17 +676,29 @@ const DmuxApp = ({ dmuxDir, panesFile, projectName, sessionName, settingsFile })
592
676
  const newPaneCount = paneCount + 1;
593
677
  applySmartLayout(newPaneCount);
594
678
  // Create git worktree and cd into it
679
+ // This MUST happen before launching Claude to ensure we're in the right directory
595
680
  try {
596
- // Send the git worktree command
597
- execSync(`tmux send-keys -t '${paneInfo}' 'git worktree add "${worktreePath}" -b ${slug} && cd "${worktreePath}"' Enter`, { stdio: 'pipe' });
598
- // Wait for worktree creation to complete
599
- await new Promise(resolve => setTimeout(resolve, 1500));
681
+ // First, create the worktree and cd into it as a single command
682
+ // Use ; instead of && to ensure cd runs even if worktree already exists
683
+ const worktreeCmd = `git worktree add "${worktreePath}" -b ${slug} 2>/dev/null ; cd "${worktreePath}"`;
684
+ execSync(`tmux send-keys -t '${paneInfo}' '${worktreeCmd}' Enter`, { stdio: 'pipe' });
685
+ // Wait longer for worktree creation and cd to complete
686
+ // This is critical - if we don't wait long enough, Claude will start in the wrong directory
687
+ await new Promise(resolve => setTimeout(resolve, 2500));
688
+ // Verify we're in the worktree directory by sending pwd command
689
+ execSync(`tmux send-keys -t '${paneInfo}' 'echo "Worktree created at:" && pwd' Enter`, { stdio: 'pipe' });
690
+ await new Promise(resolve => setTimeout(resolve, 500));
691
+ setStatusMessage('Worktree created, launching Claude...');
600
692
  }
601
693
  catch (error) {
602
- // Log error but continue
603
- setStatusMessage(`Warning: Could not create worktree: ${error}`);
604
- }
605
- // Prepare the Claude command
694
+ // Log error but continue - worktree creation is essential
695
+ setStatusMessage(`Warning: Worktree issue: ${error}`);
696
+ // Even if worktree creation failed, try to cd to the directory in case it exists
697
+ execSync(`tmux send-keys -t '${paneInfo}' 'cd "${worktreePath}" 2>/dev/null || (echo "ERROR: Failed to create/enter worktree ${slug}" && pwd)' Enter`, { stdio: 'pipe' });
698
+ await new Promise(resolve => setTimeout(resolve, 1000));
699
+ }
700
+ // NOW prepare and send the Claude command
701
+ // Claude should always be launched AFTER we're in the worktree directory
606
702
  let claudeCmd;
607
703
  if (prompt && prompt.trim()) {
608
704
  const escapedPrompt = prompt
@@ -615,7 +711,7 @@ const DmuxApp = ({ dmuxDir, panesFile, projectName, sessionName, settingsFile })
615
711
  else {
616
712
  claudeCmd = `claude --permission-mode=acceptEdits`;
617
713
  }
618
- // Send command to new pane
714
+ // Send Claude command to new pane
619
715
  const escapedCmd = claudeCmd.replace(/'/g, "'\\''");
620
716
  execSync(`tmux send-keys -t '${paneInfo}' '${escapedCmd}'`, { stdio: 'pipe' });
621
717
  execSync(`tmux send-keys -t '${paneInfo}' Enter`, { stdio: 'pipe' });
@@ -1000,163 +1096,131 @@ const DmuxApp = ({ dmuxDir, panesFile, projectName, sessionName, settingsFile })
1000
1096
  // Final fallback
1001
1097
  return 'chore: merge worktree changes';
1002
1098
  };
1003
- const generateCommand = async (type, changeRequest) => {
1004
- setGeneratingCommand(true);
1005
- setStatusMessage(`Generating ${type} command with AI...`);
1099
+ const detectPackageManager = async () => {
1006
1100
  try {
1007
- // Find claude command
1008
- const claudeCmd = await findClaudeCommand();
1009
- if (!claudeCmd) {
1010
- throw new Error('Claude Code not found. Please ensure Claude Code is installed and accessible');
1011
- }
1012
1101
  // Get project root
1013
1102
  const projectRoot = execSync('git rev-parse --show-toplevel', {
1014
1103
  encoding: 'utf-8',
1015
1104
  stdio: 'pipe'
1016
1105
  }).trim();
1017
- // Pre-fetch directory listings
1018
- setStatusMessage('Analyzing project structure...');
1019
- // Get main directory listing
1020
- const mainFiles = execSync(`ls -la "${projectRoot}"`, {
1021
- encoding: 'utf-8',
1022
- stdio: 'pipe'
1023
- });
1024
- // Get worktree listing (simulate what it would look like)
1025
- // For now we'll use main, but in reality worktrees start with the same files
1026
- const worktreeFiles = mainFiles;
1027
- // Check for package.json to understand the project type
1028
- let packageJsonContent = '';
1106
+ // Check if package.json exists
1029
1107
  try {
1030
- packageJsonContent = await fs.readFile(path.join(projectRoot, 'package.json'), 'utf-8');
1031
- }
1032
- catch { }
1033
- let requestedFile = '';
1034
- let maxAttempts = 2; // Allow one file request, then must generate command
1035
- for (let attempt = 0; attempt < maxAttempts; attempt++) {
1036
- const isLastAttempt = attempt === maxAttempts - 1;
1037
- // Build the prompt
1038
- let prompt = `You are generating a ${type === 'test' ? 'test' : 'development server'} command for a git worktree.
1039
-
1040
- CRITICAL CONTEXT:
1041
- - Main project directory: ${projectRoot}
1042
- - Worktrees are siblings: ${path.dirname(projectRoot)}/{project-name}-{branch}
1043
- - Command runs INSIDE the worktree (not main)
1044
- - Files like .env, .wrangler, node_modules are NOT shared between worktrees
1045
-
1046
- Files in MAIN directory:
1047
- ${mainFiles}
1048
-
1049
- Files in WORKTREE (initially same as main):
1050
- ${worktreeFiles}
1051
-
1052
- ${packageJsonContent ? `package.json contents:\n${packageJsonContent.substring(0, 3000)}\n` : ''}
1053
-
1054
- ${requestedFile}
1055
-
1056
- ${changeRequest ? `User's requested change: ${changeRequest}\n` : ''}
1057
-
1058
- Your task: Generate a command to ${type === 'test' ? 'run tests' : 'start a dev server'} in the worktree.
1059
-
1060
- Consider:
1061
- 1. Copy needed files from main (e.g., cp ../${path.basename(projectRoot)}/.env .)
1062
- 2. Install dependencies (npm/pnpm/yarn install)
1063
- 3. Build if needed
1064
- 4. Run the ${type} command
1065
-
1066
- ${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.'}
1067
-
1068
- Respond with ONLY a JSON object:
1069
-
1070
- ${!isLastAttempt ? `To read ONE file (only one chance):
1071
- {
1072
- "type": "cat",
1073
- "path": "path/to/file"
1074
- }
1075
-
1076
- OR ` : ''}To provide the final command:
1077
- {
1078
- "type": "command",
1079
- "command": "cp ../${path.basename(projectRoot)}/.env . && npm install && npm run ${type}",
1080
- "description": "Copy env, install deps, run ${type}"
1081
- }`;
1082
- // Write prompt to a temporary file
1083
- const tmpFile = `/tmp/dmux-prompt-${Date.now()}.txt`;
1084
- await fs.writeFile(tmpFile, prompt);
1085
- // Use Claude to generate the response
1086
- const result = execSync(`${claudeCmd} -p "$(cat ${tmpFile})" --output-format json`, {
1087
- encoding: 'utf-8',
1088
- stdio: 'pipe',
1089
- maxBuffer: 1024 * 1024 * 10
1090
- });
1091
- // Clean up temp file
1092
- try {
1093
- await fs.unlink(tmpFile);
1108
+ await fs.access(path.join(projectRoot, 'package.json'));
1109
+ // Check for lock files to determine package manager
1110
+ const files = await fs.readdir(projectRoot);
1111
+ if (files.includes('pnpm-lock.yaml')) {
1112
+ return { manager: 'pnpm', hasPackageJson: true };
1094
1113
  }
1095
- catch { }
1096
- // Parse the response
1097
- let response;
1098
- try {
1099
- const wrapper = JSON.parse(result);
1100
- let actualResult = wrapper.result || wrapper;
1101
- if (typeof actualResult === 'string') {
1102
- actualResult = actualResult.trim();
1103
- // Extract JSON from the response
1104
- const jsonMatch = actualResult.match(/\{[\s\S]*\}/);
1105
- if (jsonMatch) {
1106
- let jsonStr = jsonMatch[0];
1107
- // Clean up markdown code blocks if present
1108
- if (actualResult.includes('```')) {
1109
- const codeBlockMatch = actualResult.match(/```(?:json)?\s*([\s\S]*?)```/);
1110
- if (codeBlockMatch) {
1111
- jsonStr = codeBlockMatch[1].trim();
1112
- }
1113
- }
1114
- actualResult = JSON.parse(jsonStr);
1115
- }
1116
- else {
1117
- throw new Error('No JSON object found in response');
1118
- }
1119
- }
1120
- response = actualResult;
1114
+ else if (files.includes('yarn.lock')) {
1115
+ return { manager: 'yarn', hasPackageJson: true };
1121
1116
  }
1122
- catch (parseError) {
1123
- // Failed to parse Claude response
1124
- throw new Error(`Failed to parse AI response: ${parseError}`);
1117
+ else if (files.includes('package-lock.json')) {
1118
+ return { manager: 'npm', hasPackageJson: true };
1125
1119
  }
1126
- // Handle the response
1127
- if (response.type === 'cat' && !isLastAttempt) {
1128
- // Read the requested file
1129
- const targetPath = path.join(projectRoot, response.path);
1130
- try {
1131
- const content = await fs.readFile(targetPath, 'utf-8');
1132
- const truncated = content.length > 4000
1133
- ? content.substring(0, 4000) + '\n... [truncated]'
1134
- : content;
1135
- requestedFile = `\nContents of ${response.path}:\n${truncated}\n`;
1136
- }
1137
- catch (err) {
1138
- requestedFile = `\nError reading ${response.path}: File not found\n`;
1139
- }
1140
- // Continue to next iteration to get the command
1141
- }
1142
- else if (response.type === 'command') {
1143
- // Got the command!
1144
- setGeneratingCommand(false);
1145
- setStatusMessage('');
1146
- return response.command;
1120
+ else {
1121
+ // Default to npm if no lock file found
1122
+ return { manager: 'npm', hasPackageJson: true };
1147
1123
  }
1148
- else if (isLastAttempt) {
1149
- // Last attempt must provide a command
1150
- throw new Error('AI did not provide a command on final attempt');
1124
+ }
1125
+ catch {
1126
+ // No package.json found
1127
+ return { manager: null, hasPackageJson: false };
1128
+ }
1129
+ }
1130
+ catch {
1131
+ return { manager: null, hasPackageJson: false };
1132
+ }
1133
+ };
1134
+ const suggestCommand = async (type) => {
1135
+ const { manager, hasPackageJson } = await detectPackageManager();
1136
+ if (!hasPackageJson) {
1137
+ return null;
1138
+ }
1139
+ // Suggest standard commands based on package manager
1140
+ if (type === 'test') {
1141
+ return `${manager} run test`;
1142
+ }
1143
+ else {
1144
+ return `${manager} run dev`;
1145
+ }
1146
+ };
1147
+ const copyNonGitFiles = async (worktreePath) => {
1148
+ try {
1149
+ setStatusMessage('Copying non-git files from main...');
1150
+ // Get project root
1151
+ const projectRoot = execSync('git rev-parse --show-toplevel', {
1152
+ encoding: 'utf-8',
1153
+ stdio: 'pipe'
1154
+ }).trim();
1155
+ // Use rsync to copy non-tracked files
1156
+ // This copies everything except git-tracked files, .git, and common build directories
1157
+ const rsyncCmd = `rsync -avz --exclude='.git' --exclude='node_modules' --exclude='dist' --exclude='build' --exclude='.next' --exclude='.turbo' "${projectRoot}/" "${worktreePath}/"`;
1158
+ execSync(rsyncCmd, { stdio: 'pipe' });
1159
+ setStatusMessage('Non-git files copied successfully');
1160
+ setTimeout(() => setStatusMessage(''), 2000);
1161
+ }
1162
+ catch (error) {
1163
+ setStatusMessage('Failed to copy non-git files');
1164
+ setTimeout(() => setStatusMessage(''), 2000);
1165
+ }
1166
+ };
1167
+ const runCommandInternal = async (type, pane) => {
1168
+ if (!pane.worktreePath) {
1169
+ setStatusMessage('No worktree path for this pane');
1170
+ setTimeout(() => setStatusMessage(''), 2000);
1171
+ return;
1172
+ }
1173
+ const command = type === 'test' ? projectSettings.testCommand : projectSettings.devCommand;
1174
+ if (!command) {
1175
+ setStatusMessage('No command configured');
1176
+ setTimeout(() => setStatusMessage(''), 2000);
1177
+ return;
1178
+ }
1179
+ try {
1180
+ setRunningCommand(true);
1181
+ setStatusMessage(`Starting ${type} in background window...`);
1182
+ // Kill existing window if present
1183
+ const existingWindowId = type === 'test' ? pane.testWindowId : pane.devWindowId;
1184
+ if (existingWindowId) {
1185
+ try {
1186
+ execSync(`tmux kill-window -t '${existingWindowId}'`, { stdio: 'pipe' });
1151
1187
  }
1188
+ catch { }
1189
+ }
1190
+ // Create a new background window for the command
1191
+ const windowName = `${pane.slug}-${type}`;
1192
+ const windowId = execSync(`tmux new-window -d -n '${windowName}' -P -F '#{window_id}'`, { encoding: 'utf-8', stdio: 'pipe' }).trim();
1193
+ // Create a log file to capture output
1194
+ const logFile = `/tmp/dmux-${pane.id}-${type}.log`;
1195
+ // Build the command with output capture
1196
+ const fullCommand = `cd "${pane.worktreePath}" && ${command} 2>&1 | tee ${logFile}`;
1197
+ // Send the command to the new window
1198
+ execSync(`tmux send-keys -t '${windowId}' '${fullCommand.replace(/'/g, "'\\''")}' Enter`, { stdio: 'pipe' });
1199
+ // Update pane with window info
1200
+ const updatedPane = {
1201
+ ...pane,
1202
+ [type === 'test' ? 'testWindowId' : 'devWindowId']: windowId,
1203
+ [type === 'test' ? 'testStatus' : 'devStatus']: 'running'
1204
+ };
1205
+ const updatedPanes = panes.map(p => p.id === pane.id ? updatedPane : p);
1206
+ await savePanes(updatedPanes);
1207
+ // Start monitoring the output
1208
+ if (type === 'test') {
1209
+ // For tests, monitor for completion
1210
+ setTimeout(() => monitorTestOutput(pane.id, logFile), 2000);
1152
1211
  }
1153
- throw new Error('Failed to generate command after maximum attempts');
1212
+ else {
1213
+ // For dev, monitor for server URL
1214
+ setTimeout(() => monitorDevOutput(pane.id, logFile), 2000);
1215
+ }
1216
+ setRunningCommand(false);
1217
+ setStatusMessage(`${type === 'test' ? 'Test' : 'Dev server'} started in background`);
1218
+ setTimeout(() => setStatusMessage(''), 3000);
1154
1219
  }
1155
1220
  catch (error) {
1156
- setGeneratingCommand(false);
1157
- setStatusMessage(`Failed to generate command: ${error}`);
1221
+ setRunningCommand(false);
1222
+ setStatusMessage(`Failed to run ${type} command`);
1158
1223
  setTimeout(() => setStatusMessage(''), 3000);
1159
- return null;
1160
1224
  }
1161
1225
  };
1162
1226
  const runCommand = async (type, pane) => {
@@ -1166,11 +1230,21 @@ OR ` : ''}To provide the final command:
1166
1230
  return;
1167
1231
  }
1168
1232
  const command = type === 'test' ? projectSettings.testCommand : projectSettings.devCommand;
1233
+ const isFirstRun = type === 'test' ? !projectSettings.firstTestRun : !projectSettings.firstDevRun;
1169
1234
  if (!command) {
1170
1235
  // No command configured, prompt user
1171
1236
  setShowCommandPrompt(type);
1172
1237
  return;
1173
1238
  }
1239
+ // Check if this is the first run and offer to copy non-git files
1240
+ if (isFirstRun) {
1241
+ // Show file copy prompt and wait for response
1242
+ setShowFileCopyPrompt(true);
1243
+ setCurrentCommandType(type);
1244
+ setStatusMessage(`First time running ${type} command...`);
1245
+ // Return here - the actual command will be run after user responds to prompt
1246
+ return;
1247
+ }
1174
1248
  try {
1175
1249
  setRunningCommand(true);
1176
1250
  setStatusMessage(`Starting ${type} in background window...`);
@@ -1421,24 +1495,55 @@ OR ` : ''}To provide the final command:
1421
1495
  exit();
1422
1496
  };
1423
1497
  useInput(async (input, key) => {
1424
- if (isCreatingPane || generatingCommand || runningCommand) {
1498
+ if (isCreatingPane || runningCommand) {
1425
1499
  // Disable input while performing operations
1426
1500
  return;
1427
1501
  }
1502
+ if (showFileCopyPrompt) {
1503
+ if (input === 'y' || input === 'Y') {
1504
+ setShowFileCopyPrompt(false);
1505
+ const selectedPane = panes[selectedIndex];
1506
+ if (selectedPane && selectedPane.worktreePath && currentCommandType) {
1507
+ await copyNonGitFiles(selectedPane.worktreePath);
1508
+ // Mark as not first run and continue with command
1509
+ const newSettings = {
1510
+ ...projectSettings,
1511
+ [currentCommandType === 'test' ? 'firstTestRun' : 'firstDevRun']: true
1512
+ };
1513
+ await saveSettings(newSettings);
1514
+ // Now run the actual command
1515
+ await runCommandInternal(currentCommandType, selectedPane);
1516
+ }
1517
+ setCurrentCommandType(null);
1518
+ }
1519
+ else if (input === 'n' || input === 'N' || key.escape) {
1520
+ setShowFileCopyPrompt(false);
1521
+ const selectedPane = panes[selectedIndex];
1522
+ if (selectedPane && currentCommandType) {
1523
+ // Mark as not first run and continue without copying
1524
+ const newSettings = {
1525
+ ...projectSettings,
1526
+ [currentCommandType === 'test' ? 'firstTestRun' : 'firstDevRun']: true
1527
+ };
1528
+ await saveSettings(newSettings);
1529
+ // Now run the actual command
1530
+ await runCommandInternal(currentCommandType, selectedPane);
1531
+ }
1532
+ setCurrentCommandType(null);
1533
+ }
1534
+ return;
1535
+ }
1428
1536
  if (showCommandPrompt) {
1429
1537
  if (key.escape) {
1430
1538
  setShowCommandPrompt(null);
1431
1539
  setCommandInput('');
1432
- setShowAIPrompt(false);
1433
- setAIChangeRequest('');
1434
1540
  }
1435
- else if (key.return && !showAIPrompt) {
1541
+ else if (key.return) {
1436
1542
  if (commandInput.trim() === '') {
1437
- // User wants AI to generate
1438
- const generated = await generateCommand(showCommandPrompt);
1439
- if (generated) {
1440
- setCommandInput(generated);
1441
- setShowAIPrompt(true);
1543
+ // If empty, suggest a default command based on package manager
1544
+ const suggested = await suggestCommand(showCommandPrompt);
1545
+ if (suggested) {
1546
+ setCommandInput(suggested);
1442
1547
  }
1443
1548
  }
1444
1549
  else {
@@ -1450,43 +1555,37 @@ OR ` : ''}To provide the final command:
1450
1555
  await saveSettings(newSettings);
1451
1556
  const selectedPane = panes[selectedIndex];
1452
1557
  if (selectedPane) {
1453
- await runCommand(showCommandPrompt, selectedPane);
1454
- }
1455
- setShowCommandPrompt(null);
1456
- setCommandInput('');
1457
- setShowAIPrompt(false);
1458
- }
1459
- }
1460
- else if (key.return && showAIPrompt) {
1461
- if (aiChangeRequest.trim() === '') {
1462
- // User accepts AI generated command
1463
- const newSettings = {
1464
- ...projectSettings,
1465
- [showCommandPrompt === 'test' ? 'testCommand' : 'devCommand']: commandInput.trim()
1466
- };
1467
- await saveSettings(newSettings);
1468
- const selectedPane = panes[selectedIndex];
1469
- if (selectedPane) {
1470
- await runCommand(showCommandPrompt, selectedPane);
1558
+ // Check if first run
1559
+ const isFirstRun = showCommandPrompt === 'test' ? !projectSettings.firstTestRun : !projectSettings.firstDevRun;
1560
+ if (isFirstRun) {
1561
+ setCurrentCommandType(showCommandPrompt);
1562
+ setShowCommandPrompt(null);
1563
+ setShowFileCopyPrompt(true);
1564
+ }
1565
+ else {
1566
+ await runCommandInternal(showCommandPrompt, selectedPane);
1567
+ setShowCommandPrompt(null);
1568
+ setCommandInput('');
1569
+ }
1471
1570
  }
1472
- setShowCommandPrompt(null);
1473
- setCommandInput('');
1474
- setShowAIPrompt(false);
1475
- setAIChangeRequest('');
1476
- }
1477
- else {
1478
- // User wants changes, regenerate
1479
- const generated = await generateCommand(showCommandPrompt, aiChangeRequest);
1480
- if (generated) {
1481
- setCommandInput(generated);
1482
- setAIChangeRequest('');
1571
+ else {
1572
+ setShowCommandPrompt(null);
1573
+ setCommandInput('');
1483
1574
  }
1484
1575
  }
1485
1576
  }
1486
1577
  return;
1487
1578
  }
1488
1579
  if (showNewPaneDialog) {
1489
- // EnhancedTextInput handles its own input events
1580
+ if (key.escape) {
1581
+ setShowNewPaneDialog(false);
1582
+ setNewPanePrompt('');
1583
+ }
1584
+ else if (key.ctrl && input === 'o') {
1585
+ // Open in external editor
1586
+ openInEditor();
1587
+ }
1588
+ // TextInput handles other input events
1490
1589
  return;
1491
1590
  }
1492
1591
  if (showMergeConfirmation) {
@@ -1628,18 +1727,18 @@ OR ` : ''}To provide the final command:
1628
1727
  }),
1629
1728
  React.createElement(Box, { paddingX: 1, borderStyle: "single", borderColor: selectedIndex === panes.length ? 'green' : 'gray', width: 35, flexShrink: 0 },
1630
1729
  React.createElement(Text, { color: selectedIndex === panes.length ? 'green' : 'white' }, "+ New dmux pane"))),
1631
- showNewPaneDialog && (React.createElement(Box, { borderStyle: "double", borderColor: "cyan", paddingX: 1 },
1730
+ showNewPaneDialog && (React.createElement(Box, { borderStyle: "round", borderColor: "gray", paddingX: 1 },
1632
1731
  React.createElement(Box, { flexDirection: "column" },
1633
1732
  React.createElement(Text, null, "Enter initial Claude prompt (ESC to cancel):"),
1634
1733
  React.createElement(Box, { marginTop: 1 },
1635
- React.createElement(SimpleEnhancedInput, { value: newPanePrompt, onChange: setNewPanePrompt, placeholder: "Optional prompt... (@ to reference files)", onSubmit: () => {
1636
- createNewPane(newPanePrompt);
1637
- setShowNewPaneDialog(false);
1638
- setNewPanePrompt('');
1639
- }, onCancel: () => {
1640
- setShowNewPaneDialog(false);
1641
- setNewPanePrompt('');
1642
- }, isActive: showNewPaneDialog, workingDirectory: process.cwd() }))))),
1734
+ React.createElement(Box, { flexDirection: "column" },
1735
+ React.createElement(TextInput, { value: newPanePrompt, onChange: setNewPanePrompt, placeholder: "Optional prompt... (@ to reference files)", onSubmit: () => {
1736
+ createNewPane(newPanePrompt);
1737
+ setShowNewPaneDialog(false);
1738
+ setNewPanePrompt('');
1739
+ } }),
1740
+ React.createElement(Box, { marginTop: 1 },
1741
+ React.createElement(Text, { dimColor: true, italic: true }, "Press Ctrl+O to open in $EDITOR for complex multi-line input"))))))),
1643
1742
  isCreatingPane && (React.createElement(Box, { borderStyle: "single", borderColor: "yellow", paddingX: 1, marginTop: 1 },
1644
1743
  React.createElement(Text, { color: "yellow" },
1645
1744
  React.createElement(Text, { bold: true }, "\u23F3 Creating new pane... "),
@@ -1675,7 +1774,7 @@ OR ` : ''}To provide the final command:
1675
1774
  React.createElement(Text, { color: selectedCloseOption === 3 ? 'cyan' : 'white' },
1676
1775
  selectedCloseOption === 3 ? '▶ ' : ' ',
1677
1776
  "Just Close - Close pane only")))))),
1678
- showCommandPrompt && !showAIPrompt && (React.createElement(Box, { borderStyle: "double", borderColor: "magenta", paddingX: 1, marginTop: 1 },
1777
+ showCommandPrompt && (React.createElement(Box, { borderStyle: "round", borderColor: "gray", paddingX: 1, marginTop: 1 },
1679
1778
  React.createElement(Box, { flexDirection: "column" },
1680
1779
  React.createElement(Text, { color: "magenta", bold: true },
1681
1780
  "Configure ",
@@ -1685,24 +1784,16 @@ OR ` : ''}To provide the final command:
1685
1784
  "Enter command to run ",
1686
1785
  showCommandPrompt === 'test' ? 'tests' : 'dev server',
1687
1786
  " in worktrees"),
1688
- React.createElement(Text, { dimColor: true }, "(Press Enter with empty input to generate with AI, ESC to cancel)"),
1787
+ React.createElement(Text, { dimColor: true }, "(Press Enter with empty input for suggested command, ESC to cancel)"),
1689
1788
  React.createElement(Box, { marginTop: 1 },
1690
1789
  React.createElement(TextInput, { value: commandInput, onChange: setCommandInput, placeholder: showCommandPrompt === 'test' ? 'e.g., npm test, pnpm test' : 'e.g., npm run dev, pnpm dev' }))))),
1691
- showCommandPrompt && showAIPrompt && (React.createElement(Box, { borderStyle: "double", borderColor: "magenta", paddingX: 1, marginTop: 1 },
1790
+ showFileCopyPrompt && (React.createElement(Box, { borderStyle: "double", borderColor: "yellow", paddingX: 1, marginTop: 1 },
1692
1791
  React.createElement(Box, { flexDirection: "column" },
1693
- React.createElement(Text, { color: "magenta", bold: true },
1694
- "AI Generated ",
1695
- showCommandPrompt === 'test' ? 'Test' : 'Dev',
1696
- " Command"),
1697
- React.createElement(Box, { marginTop: 1, borderStyle: "single", borderColor: "gray", paddingX: 1 },
1698
- React.createElement(Text, null, commandInput)),
1792
+ React.createElement(Text, { color: "yellow", bold: true }, "First Run Setup"),
1793
+ React.createElement(Text, null, "Copy non-git files (like .env, configs) from main to worktree?"),
1794
+ React.createElement(Text, { dimColor: true }, "This includes files not tracked by git but excludes node_modules, dist, etc."),
1699
1795
  React.createElement(Box, { marginTop: 1 },
1700
- React.createElement(Text, { dimColor: true }, "Press Enter to accept, or describe changes needed:")),
1701
- React.createElement(Box, { marginTop: 1 },
1702
- React.createElement(TextInput, { value: aiChangeRequest, onChange: setAIChangeRequest, placeholder: "e.g., 'also copy .env file' or press Enter to accept" }))))),
1703
- generatingCommand && (React.createElement(Box, { borderStyle: "single", borderColor: "yellow", paddingX: 1, marginTop: 1 },
1704
- React.createElement(Text, { color: "yellow" },
1705
- React.createElement(Text, { bold: true }, "\u23F3 Generating command with AI...")))),
1796
+ React.createElement(Text, null, "(y/n):"))))),
1706
1797
  runningCommand && (React.createElement(Box, { borderStyle: "single", borderColor: "blue", paddingX: 1, marginTop: 1 },
1707
1798
  React.createElement(Text, { color: "blue" },
1708
1799
  React.createElement(Text, { bold: true }, "\u25B6 Running command...")))),