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.d.ts.map +1 -1
- package/dist/DmuxApp.js +385 -294
- package/dist/DmuxApp.js.map +1 -1
- package/dist/SimpleEnhancedInput.d.ts.map +1 -1
- package/dist/SimpleEnhancedInput.js +176 -164
- package/dist/SimpleEnhancedInput.js.map +1 -1
- package/package.json +3 -2
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 [
|
|
27
|
-
const [
|
|
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
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
/
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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
|
-
//
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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(
|
|
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
|
-
//
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
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:
|
|
604
|
-
|
|
605
|
-
|
|
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
|
|
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
|
-
//
|
|
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
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
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
|
-
|
|
1096
|
-
|
|
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
|
-
|
|
1123
|
-
|
|
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
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
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
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1157
|
-
setStatusMessage(`Failed to
|
|
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 ||
|
|
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
|
|
1541
|
+
else if (key.return) {
|
|
1436
1542
|
if (commandInput.trim() === '') {
|
|
1437
|
-
//
|
|
1438
|
-
const
|
|
1439
|
-
if (
|
|
1440
|
-
setCommandInput(
|
|
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
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
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
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
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
|
-
|
|
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: "
|
|
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(
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
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 &&
|
|
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
|
|
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
|
-
|
|
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: "
|
|
1694
|
-
|
|
1695
|
-
|
|
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,
|
|
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...")))),
|