dmux 1.6.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -8
- package/dist/AutoUpdater.d.ts +2 -2
- package/dist/AutoUpdater.d.ts.map +1 -1
- package/dist/AutoUpdater.js +18 -7
- package/dist/AutoUpdater.js.map +1 -1
- package/dist/CleanTextInput.d.ts.map +1 -1
- package/dist/CleanTextInput.js +12 -4
- package/dist/CleanTextInput.js.map +1 -1
- package/dist/DmuxApp.d.ts +1 -10
- package/dist/DmuxApp.d.ts.map +1 -1
- package/dist/DmuxApp.js +146 -1483
- package/dist/DmuxApp.js.map +1 -1
- package/dist/components/AgentChoiceDialog.d.ts +7 -0
- package/dist/components/AgentChoiceDialog.d.ts.map +1 -0
- package/dist/components/AgentChoiceDialog.js +12 -0
- package/dist/components/AgentChoiceDialog.js.map +1 -0
- package/dist/components/CloseOptionsDialog.d.ts +9 -0
- package/dist/components/CloseOptionsDialog.d.ts.map +1 -0
- package/dist/components/CloseOptionsDialog.js +30 -0
- package/dist/components/CloseOptionsDialog.js.map +1 -0
- package/dist/components/CommandPromptDialog.d.ts +9 -0
- package/dist/components/CommandPromptDialog.d.ts.map +1 -0
- package/dist/components/CommandPromptDialog.js +20 -0
- package/dist/components/CommandPromptDialog.js.map +1 -0
- package/dist/components/CreatingIndicator.d.ts +7 -0
- package/dist/components/CreatingIndicator.d.ts.map +1 -0
- package/dist/components/CreatingIndicator.js +10 -0
- package/dist/components/CreatingIndicator.js.map +1 -0
- package/dist/components/FileCopyPrompt.d.ts +4 -0
- package/dist/components/FileCopyPrompt.d.ts.map +1 -0
- package/dist/components/FileCopyPrompt.js +13 -0
- package/dist/components/FileCopyPrompt.js.map +1 -0
- package/dist/components/FooterHelp.d.ts +8 -0
- package/dist/components/FooterHelp.d.ts.map +1 -0
- package/dist/components/FooterHelp.js +12 -0
- package/dist/components/FooterHelp.js.map +1 -0
- package/dist/components/LoadingIndicator.d.ts +4 -0
- package/dist/components/LoadingIndicator.d.ts.map +1 -0
- package/dist/components/LoadingIndicator.js +10 -0
- package/dist/components/LoadingIndicator.js.map +1 -0
- package/dist/components/MergeConfirmationDialog.d.ts +8 -0
- package/dist/components/MergeConfirmationDialog.d.ts.map +1 -0
- package/dist/components/MergeConfirmationDialog.js +13 -0
- package/dist/components/MergeConfirmationDialog.js.map +1 -0
- package/dist/components/NewPaneDialog.d.ts +9 -0
- package/dist/components/NewPaneDialog.d.ts.map +1 -0
- package/dist/components/NewPaneDialog.js +13 -0
- package/dist/components/NewPaneDialog.js.map +1 -0
- package/dist/components/PaneCard.d.ts +9 -0
- package/dist/components/PaneCard.d.ts.map +1 -0
- package/dist/components/PaneCard.js +37 -0
- package/dist/components/PaneCard.js.map +1 -0
- package/dist/components/PanesGrid.d.ts +11 -0
- package/dist/components/PanesGrid.d.ts.map +1 -0
- package/dist/components/PanesGrid.js +11 -0
- package/dist/components/PanesGrid.js.map +1 -0
- package/dist/components/RunningIndicator.d.ts +4 -0
- package/dist/components/RunningIndicator.d.ts.map +1 -0
- package/dist/components/RunningIndicator.js +9 -0
- package/dist/components/RunningIndicator.js.map +1 -0
- package/dist/components/UpdateDialog.d.ts +7 -0
- package/dist/components/UpdateDialog.d.ts.map +1 -0
- package/dist/components/UpdateDialog.js +27 -0
- package/dist/components/UpdateDialog.js.map +1 -0
- package/dist/components/UpdatingIndicator.d.ts +4 -0
- package/dist/components/UpdatingIndicator.d.ts.map +1 -0
- package/dist/components/UpdatingIndicator.js +9 -0
- package/dist/components/UpdatingIndicator.js.map +1 -0
- package/dist/hooks/useAgentDetection.d.ts +4 -0
- package/dist/hooks/useAgentDetection.d.ts.map +1 -0
- package/dist/hooks/useAgentDetection.js +71 -0
- package/dist/hooks/useAgentDetection.js.map +1 -0
- package/dist/hooks/useAgentStatus.d.ts +11 -0
- package/dist/hooks/useAgentStatus.d.ts.map +1 -0
- package/dist/hooks/useAgentStatus.js +172 -0
- package/dist/hooks/useAgentStatus.js.map +1 -0
- package/dist/hooks/useAutoUpdater.d.ts +20 -0
- package/dist/hooks/useAutoUpdater.d.ts.map +1 -0
- package/dist/hooks/useAutoUpdater.js +100 -0
- package/dist/hooks/useAutoUpdater.js.map +1 -0
- package/dist/hooks/useCommandRunner.d.ts +18 -0
- package/dist/hooks/useCommandRunner.d.ts.map +1 -0
- package/dist/hooks/useCommandRunner.js +44 -0
- package/dist/hooks/useCommandRunner.js.map +1 -0
- package/dist/hooks/useNavigation.d.ts +8 -0
- package/dist/hooks/useNavigation.d.ts.map +1 -0
- package/dist/hooks/useNavigation.js +62 -0
- package/dist/hooks/useNavigation.js.map +1 -0
- package/dist/hooks/usePaneCreation.d.ts +16 -0
- package/dist/hooks/usePaneCreation.d.ts.map +1 -0
- package/dist/hooks/usePaneCreation.js +133 -0
- package/dist/hooks/usePaneCreation.js.map +1 -0
- package/dist/hooks/usePaneRunner.d.ts +17 -0
- package/dist/hooks/usePaneRunner.d.ts.map +1 -0
- package/dist/hooks/usePaneRunner.js +149 -0
- package/dist/hooks/usePaneRunner.js.map +1 -0
- package/dist/hooks/usePanes.d.ts +9 -0
- package/dist/hooks/usePanes.d.ts.map +1 -0
- package/dist/hooks/usePanes.js +222 -0
- package/dist/hooks/usePanes.js.map +1 -0
- package/dist/hooks/useProjectSettings.d.ts +6 -0
- package/dist/hooks/useProjectSettings.d.ts.map +1 -0
- package/dist/hooks/useProjectSettings.js +46 -0
- package/dist/hooks/useProjectSettings.js.map +1 -0
- package/dist/hooks/useTerminalWidth.d.ts +2 -0
- package/dist/hooks/useTerminalWidth.d.ts.map +1 -0
- package/dist/hooks/useTerminalWidth.js +13 -0
- package/dist/hooks/useTerminalWidth.js.map +1 -0
- package/dist/hooks/useWorktreeActions.d.ts +17 -0
- package/dist/hooks/useWorktreeActions.d.ts.map +1 -0
- package/dist/hooks/useWorktreeActions.js +190 -0
- package/dist/hooks/useWorktreeActions.js.map +1 -0
- package/dist/index.js +86 -11
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts +38 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/commands.d.ts +6 -0
- package/dist/utils/commands.d.ts.map +1 -0
- package/dist/utils/commands.js +35 -0
- package/dist/utils/commands.js.map +1 -0
- package/dist/utils/input.d.ts +13 -0
- package/dist/utils/input.d.ts.map +1 -0
- package/dist/utils/input.js +100 -0
- package/dist/utils/input.js.map +1 -0
- package/dist/utils/slug.d.ts +3 -0
- package/dist/utils/slug.d.ts.map +1 -0
- package/dist/utils/slug.js +58 -0
- package/dist/utils/slug.js.map +1 -0
- package/dist/utils/tmux.d.ts +4 -0
- package/dist/utils/tmux.d.ts.map +1 -0
- package/dist/utils/tmux.js +62 -0
- package/dist/utils/tmux.js.map +1 -0
- package/dist/workers/updateChecker.d.ts +2 -0
- package/dist/workers/updateChecker.d.ts.map +1 -0
- package/dist/workers/updateChecker.js +33 -0
- package/dist/workers/updateChecker.js.map +1 -0
- package/package.json +6 -2
package/dist/DmuxApp.js
CHANGED
|
@@ -1,15 +1,39 @@
|
|
|
1
1
|
import React, { useState, useEffect } from 'react';
|
|
2
2
|
import { Box, Text, useInput, useApp } from 'ink';
|
|
3
|
-
import CleanTextInput from './CleanTextInput.js';
|
|
4
|
-
import StyledTextInput from './StyledTextInput.js';
|
|
5
3
|
import { execSync } from 'child_process';
|
|
6
|
-
import fs from 'fs/promises';
|
|
7
4
|
import path from 'path';
|
|
8
5
|
import { createRequire } from 'module';
|
|
6
|
+
// Hooks
|
|
7
|
+
import usePanes from './hooks/usePanes.js';
|
|
8
|
+
import useProjectSettings from './hooks/useProjectSettings.js';
|
|
9
|
+
import useTerminalWidth from './hooks/useTerminalWidth.js';
|
|
10
|
+
import useNavigation from './hooks/useNavigation.js';
|
|
11
|
+
import useAutoUpdater from './hooks/useAutoUpdater.js';
|
|
12
|
+
import useAgentDetection from './hooks/useAgentDetection.js';
|
|
13
|
+
import useAgentStatus from './hooks/useAgentStatus.js';
|
|
14
|
+
import useWorktreeActions from './hooks/useWorktreeActions.js';
|
|
15
|
+
import usePaneRunner from './hooks/usePaneRunner.js';
|
|
16
|
+
import usePaneCreation from './hooks/usePaneCreation.js';
|
|
17
|
+
// Utils
|
|
18
|
+
import { applySmartLayout } from './utils/tmux.js';
|
|
19
|
+
import { suggestCommand } from './utils/commands.js';
|
|
20
|
+
import { generateSlug } from './utils/slug.js';
|
|
9
21
|
const require = createRequire(import.meta.url);
|
|
10
22
|
const packageJson = require('../package.json');
|
|
11
|
-
|
|
12
|
-
|
|
23
|
+
import PanesGrid from './components/PanesGrid.js';
|
|
24
|
+
import NewPaneDialog from './components/NewPaneDialog.js';
|
|
25
|
+
import AgentChoiceDialog from './components/AgentChoiceDialog.js';
|
|
26
|
+
import CloseOptionsDialog from './components/CloseOptionsDialog.js';
|
|
27
|
+
import MergeConfirmationDialog from './components/MergeConfirmationDialog.js';
|
|
28
|
+
import CommandPromptDialog from './components/CommandPromptDialog.js';
|
|
29
|
+
import FileCopyPrompt from './components/FileCopyPrompt.js';
|
|
30
|
+
import LoadingIndicator from './components/LoadingIndicator.js';
|
|
31
|
+
import RunningIndicator from './components/RunningIndicator.js';
|
|
32
|
+
import UpdatingIndicator from './components/UpdatingIndicator.js';
|
|
33
|
+
import CreatingIndicator from './components/CreatingIndicator.js';
|
|
34
|
+
import FooterHelp from './components/FooterHelp.js';
|
|
35
|
+
const DmuxApp = ({ panesFile, projectName, sessionName, settingsFile, autoUpdater }) => {
|
|
36
|
+
/* panes state moved to usePanes */
|
|
13
37
|
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
14
38
|
const [showNewPaneDialog, setShowNewPaneDialog] = useState(false);
|
|
15
39
|
const [newPanePrompt, setNewPanePrompt] = useState('');
|
|
@@ -20,43 +44,56 @@ const DmuxApp = ({ dmuxDir, panesFile, projectName, sessionName, settingsFile, a
|
|
|
20
44
|
const [selectedCloseOption, setSelectedCloseOption] = useState(0);
|
|
21
45
|
const [closingPane, setClosingPane] = useState(null);
|
|
22
46
|
const [isCreatingPane, setIsCreatingPane] = useState(false);
|
|
23
|
-
const
|
|
47
|
+
const { projectSettings, saveSettings } = useProjectSettings(settingsFile);
|
|
24
48
|
const [showCommandPrompt, setShowCommandPrompt] = useState(null);
|
|
25
49
|
const [commandInput, setCommandInput] = useState('');
|
|
26
50
|
const [showFileCopyPrompt, setShowFileCopyPrompt] = useState(false);
|
|
27
51
|
const [currentCommandType, setCurrentCommandType] = useState(null);
|
|
28
52
|
const [runningCommand, setRunningCommand] = useState(false);
|
|
29
|
-
|
|
30
|
-
const
|
|
31
|
-
const [isUpdating, setIsUpdating] = useState(false);
|
|
32
|
-
const [isLoading, setIsLoading] = useState(true);
|
|
53
|
+
// Update state handled by hook
|
|
54
|
+
const { updateInfo, showUpdateDialog, isUpdating, performUpdate, skipUpdate, dismissUpdate, updateAvailable } = useAutoUpdater(autoUpdater, setStatusMessage);
|
|
33
55
|
const { exit } = useApp();
|
|
34
56
|
// Agent selection state
|
|
35
|
-
const
|
|
57
|
+
const { availableAgents } = useAgentDetection();
|
|
36
58
|
const [showAgentChoiceDialog, setShowAgentChoiceDialog] = useState(false);
|
|
37
59
|
const [agentChoice, setAgentChoice] = useState(null);
|
|
38
60
|
const [pendingPrompt, setPendingPrompt] = useState('');
|
|
39
61
|
// Track terminal dimensions for responsive layout
|
|
40
|
-
const
|
|
62
|
+
const terminalWidth = useTerminalWidth();
|
|
63
|
+
// Panes state and persistence
|
|
64
|
+
const skipLoading = showNewPaneDialog || showMergeConfirmation || showCloseOptions ||
|
|
65
|
+
!!showCommandPrompt || showFileCopyPrompt;
|
|
66
|
+
const { panes, setPanes, isLoading, loadPanes, savePanes } = usePanes(panesFile, skipLoading);
|
|
67
|
+
// Worktree actions
|
|
68
|
+
const { closePane, mergeWorktree, mergeAndPrune, deleteUnsavedChanges, handleCloseOption } = useWorktreeActions({
|
|
69
|
+
panes,
|
|
70
|
+
savePanes,
|
|
71
|
+
setStatusMessage,
|
|
72
|
+
setShowMergeConfirmation,
|
|
73
|
+
setMergedPane,
|
|
74
|
+
});
|
|
75
|
+
// Pane runner
|
|
76
|
+
const { copyNonGitFiles, runCommandInternal, monitorTestOutput, monitorDevOutput, attachBackgroundWindow } = usePaneRunner({
|
|
77
|
+
panes,
|
|
78
|
+
savePanes,
|
|
79
|
+
projectSettings,
|
|
80
|
+
setStatusMessage,
|
|
81
|
+
setRunningCommand,
|
|
82
|
+
});
|
|
83
|
+
// Pane creation
|
|
84
|
+
const { openInEditor: openEditor2, createNewPane: createNewPaneHook } = usePaneCreation({
|
|
85
|
+
panes,
|
|
86
|
+
savePanes,
|
|
87
|
+
projectName,
|
|
88
|
+
setIsCreatingPane,
|
|
89
|
+
setStatusMessage,
|
|
90
|
+
setNewPanePrompt,
|
|
91
|
+
loadPanes,
|
|
92
|
+
});
|
|
41
93
|
// Load panes and settings on mount and refresh periodically
|
|
42
94
|
useEffect(() => {
|
|
43
|
-
// Load immediately for initial data
|
|
44
|
-
loadPanes();
|
|
45
|
-
loadSettings();
|
|
46
|
-
// Start polling after a short delay to avoid competing with startup
|
|
47
|
-
let pollingInterval;
|
|
48
|
-
const startPolling = setTimeout(() => {
|
|
49
|
-
pollingInterval = setInterval(loadPanes, 3000); // Poll every 3 seconds instead of 2
|
|
50
|
-
}, 1000); // Wait 1 second before starting polling
|
|
51
|
-
// Handle terminal resize
|
|
52
|
-
const handleResize = () => {
|
|
53
|
-
setTerminalWidth(process.stdout.columns || 80);
|
|
54
|
-
};
|
|
55
|
-
// Add resize listener
|
|
56
|
-
process.stdout.on('resize', handleResize);
|
|
57
95
|
// Add cleanup handlers for process termination
|
|
58
96
|
const handleTermination = () => {
|
|
59
|
-
// Clear screen before exit
|
|
60
97
|
process.stdout.write('\x1b[2J\x1b[H');
|
|
61
98
|
process.stdout.write('\x1b[3J');
|
|
62
99
|
try {
|
|
@@ -68,10 +105,6 @@ const DmuxApp = ({ dmuxDir, panesFile, projectName, sessionName, settingsFile, a
|
|
|
68
105
|
process.on('SIGINT', handleTermination);
|
|
69
106
|
process.on('SIGTERM', handleTermination);
|
|
70
107
|
return () => {
|
|
71
|
-
clearTimeout(startPolling);
|
|
72
|
-
if (pollingInterval)
|
|
73
|
-
clearInterval(pollingInterval);
|
|
74
|
-
process.stdout.removeListener('resize', handleResize);
|
|
75
108
|
process.removeListener('SIGINT', handleTermination);
|
|
76
109
|
process.removeListener('SIGTERM', handleTermination);
|
|
77
110
|
};
|
|
@@ -90,703 +123,35 @@ const DmuxApp = ({ dmuxDir, panesFile, projectName, sessionName, settingsFile, a
|
|
|
90
123
|
!showCloseOptions &&
|
|
91
124
|
!showCommandPrompt &&
|
|
92
125
|
!showFileCopyPrompt &&
|
|
93
|
-
!showUpdateDialog &&
|
|
94
126
|
!showAgentChoiceDialog &&
|
|
95
127
|
!isCreatingPane &&
|
|
96
128
|
!runningCommand &&
|
|
97
129
|
!isUpdating) {
|
|
98
130
|
setShowNewPaneDialog(true);
|
|
99
131
|
}
|
|
100
|
-
}, [isLoading, panes.length, showNewPaneDialog, showMergeConfirmation, showCloseOptions, showCommandPrompt, showFileCopyPrompt,
|
|
101
|
-
//
|
|
102
|
-
|
|
103
|
-
if (!autoUpdater)
|
|
104
|
-
return;
|
|
105
|
-
const checkForUpdates = async () => {
|
|
106
|
-
try {
|
|
107
|
-
const info = await autoUpdater.checkForUpdates();
|
|
108
|
-
if (await autoUpdater.shouldShowUpdateNotification(info)) {
|
|
109
|
-
setUpdateInfo(info);
|
|
110
|
-
setShowUpdateDialog(true);
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
catch {
|
|
114
|
-
// Silently ignore update check failures
|
|
115
|
-
}
|
|
116
|
-
};
|
|
117
|
-
// Delay initial update check to avoid slowing startup
|
|
118
|
-
const initialCheckTimer = setTimeout(() => {
|
|
119
|
-
checkForUpdates();
|
|
120
|
-
}, 3000); // Check after 3 seconds
|
|
121
|
-
// Check for updates every 6 hours
|
|
122
|
-
const updateInterval = setInterval(checkForUpdates, 6 * 60 * 60 * 1000);
|
|
123
|
-
return () => {
|
|
124
|
-
clearTimeout(initialCheckTimer);
|
|
125
|
-
clearInterval(updateInterval);
|
|
126
|
-
};
|
|
127
|
-
}, [autoUpdater]);
|
|
128
|
-
// Detect available agents on mount
|
|
132
|
+
}, [isLoading, panes.length, showNewPaneDialog, showMergeConfirmation, showCloseOptions, showCommandPrompt, showFileCopyPrompt, showAgentChoiceDialog, isCreatingPane, runningCommand, isUpdating]);
|
|
133
|
+
// Update checking moved to useAutoUpdater
|
|
134
|
+
// Set default agent choice when detection completes
|
|
129
135
|
useEffect(() => {
|
|
130
|
-
(
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
// Defer Claude monitoring to avoid slowing down startup and dialog interactions
|
|
150
|
-
const startupDelay = setTimeout(() => {
|
|
151
|
-
const monitorAgentStatus = async () => {
|
|
152
|
-
// Skip monitoring if any dialog is open to avoid UI freezing
|
|
153
|
-
if (showNewPaneDialog || showMergeConfirmation || showCloseOptions ||
|
|
154
|
-
showCommandPrompt || showFileCopyPrompt || showUpdateDialog) {
|
|
155
|
-
return;
|
|
156
|
-
}
|
|
157
|
-
// Monitor agent status for all panes
|
|
158
|
-
const updatedPanesWithNulls = await Promise.all(panes.map(async (pane) => {
|
|
159
|
-
try {
|
|
160
|
-
// Skip if recently checked (within 500ms to avoid overlapping checks)
|
|
161
|
-
if (pane.lastAgentCheck && Date.now() - pane.lastAgentCheck < 500) {
|
|
162
|
-
return pane;
|
|
163
|
-
}
|
|
164
|
-
// First check if pane exists before trying to capture
|
|
165
|
-
let paneExists = false;
|
|
166
|
-
try {
|
|
167
|
-
const paneIds = execSync(`tmux list-panes -F '#{pane_id}'`, {
|
|
168
|
-
encoding: 'utf-8',
|
|
169
|
-
stdio: 'pipe',
|
|
170
|
-
timeout: 500 // Quick timeout
|
|
171
|
-
}).trim().split('\n').filter(id => id && id.startsWith('%'));
|
|
172
|
-
paneExists = paneIds.includes(pane.paneId);
|
|
173
|
-
}
|
|
174
|
-
catch {
|
|
175
|
-
// If we can't check, assume pane exists to avoid false removals
|
|
176
|
-
paneExists = true;
|
|
177
|
-
}
|
|
178
|
-
if (!paneExists) {
|
|
179
|
-
// Pane doesn't exist anymore, mark for removal
|
|
180
|
-
return null;
|
|
181
|
-
}
|
|
182
|
-
// Capture the last 30 lines of the pane for better detection
|
|
183
|
-
const captureOutput = execSync(`tmux capture-pane -t '${pane.paneId}' -p -S -30`, { encoding: 'utf-8', stdio: 'pipe' });
|
|
184
|
-
const lines = captureOutput.split('\n');
|
|
185
|
-
const lastLines = lines.slice(-10).join('\n');
|
|
186
|
-
// Determine working patterns based on agent
|
|
187
|
-
let isWorking = false;
|
|
188
|
-
if (pane.agent === 'opencode') {
|
|
189
|
-
const workingPatterns = [
|
|
190
|
-
/esc\s+(to\s+)?interrupt/i,
|
|
191
|
-
/working(\.|\.{2}|\.{3})/i,
|
|
192
|
-
];
|
|
193
|
-
isWorking = workingPatterns.some(pattern => pattern.test(lastLines));
|
|
194
|
-
}
|
|
195
|
-
else {
|
|
196
|
-
// Default to Claude patterns
|
|
197
|
-
const workingPatterns = [
|
|
198
|
-
/esc to interrupt/i,
|
|
199
|
-
];
|
|
200
|
-
isWorking = workingPatterns.some(pattern => pattern.test(captureOutput));
|
|
201
|
-
}
|
|
202
|
-
// Attention patterns - generic prompts needing user input
|
|
203
|
-
const attentionPatterns = [
|
|
204
|
-
/\?\s*$/m,
|
|
205
|
-
/y\/n/i,
|
|
206
|
-
/yes.*no/i,
|
|
207
|
-
/\ballow\b.*\?/i,
|
|
208
|
-
/\bapprove\b.*\?/i,
|
|
209
|
-
/\bgrant\b.*\?/i,
|
|
210
|
-
/\btrust\b.*\?/i,
|
|
211
|
-
/\baccept\b.*\?/i,
|
|
212
|
-
/\bcontinue\b.*\?/i,
|
|
213
|
-
/\bproceed\b.*\?/i,
|
|
214
|
-
/permission/i,
|
|
215
|
-
/confirmation/i,
|
|
216
|
-
/press.*enter/i,
|
|
217
|
-
/waiting for/i,
|
|
218
|
-
/are you sure/i,
|
|
219
|
-
/would you like/i,
|
|
220
|
-
/do you want/i,
|
|
221
|
-
/please confirm/i,
|
|
222
|
-
/requires.*approval/i,
|
|
223
|
-
/needs.*input/i,
|
|
224
|
-
/⏵⏵\s*accept edits/i,
|
|
225
|
-
/shift\+tab to cycle/i,
|
|
226
|
-
];
|
|
227
|
-
// Claude-specific input box detection
|
|
228
|
-
const hasClaudeInputBox = /╭─+╮/.test(lastLines) && /╰─+╯/.test(lastLines) && /│\s+>\s+.*│/.test(lastLines);
|
|
229
|
-
const needsAttention = attentionPatterns.some(pattern => pattern.test(captureOutput)) || (pane.agent !== 'opencode' && hasClaudeInputBox);
|
|
230
|
-
// Determine status - working takes precedence
|
|
231
|
-
let newStatus = 'idle';
|
|
232
|
-
if (isWorking) {
|
|
233
|
-
newStatus = 'working';
|
|
234
|
-
}
|
|
235
|
-
else if (needsAttention && !isWorking) {
|
|
236
|
-
newStatus = 'waiting';
|
|
237
|
-
}
|
|
238
|
-
// Additional Claude-specific checks
|
|
239
|
-
if (pane.agent !== 'opencode') {
|
|
240
|
-
if (/accept edits/i.test(captureOutput) && !/esc to interrupt/i.test(captureOutput)) {
|
|
241
|
-
newStatus = 'waiting';
|
|
242
|
-
}
|
|
243
|
-
if (hasClaudeInputBox && !isWorking) {
|
|
244
|
-
newStatus = 'waiting';
|
|
245
|
-
}
|
|
246
|
-
const claudeQuestionPatterns = [
|
|
247
|
-
/I (can|could|should|would|will|may|might)/i,
|
|
248
|
-
/Let me know/i,
|
|
249
|
-
/Please (tell|let|inform|advise)/i,
|
|
250
|
-
/Would you prefer/i,
|
|
251
|
-
/Should I (proceed|continue|go ahead)/i,
|
|
252
|
-
];
|
|
253
|
-
if (claudeQuestionPatterns.some(pattern => pattern.test(lastLines)) && !isWorking) {
|
|
254
|
-
newStatus = 'waiting';
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
// Return updated pane if status changed
|
|
258
|
-
if (pane.agentStatus !== newStatus) {
|
|
259
|
-
return {
|
|
260
|
-
...pane,
|
|
261
|
-
agentStatus: newStatus,
|
|
262
|
-
lastAgentCheck: Date.now()
|
|
263
|
-
};
|
|
264
|
-
}
|
|
265
|
-
// Just update timestamp
|
|
266
|
-
return {
|
|
267
|
-
...pane,
|
|
268
|
-
lastAgentCheck: Date.now()
|
|
269
|
-
};
|
|
270
|
-
}
|
|
271
|
-
catch (error) {
|
|
272
|
-
// If we can't capture the pane, it might be dead - mark it for removal
|
|
273
|
-
return null; // Will be filtered out below
|
|
274
|
-
}
|
|
275
|
-
}));
|
|
276
|
-
// Filter out null values (dead panes) and keep only valid panes
|
|
277
|
-
const updatedPanes = updatedPanesWithNulls.filter((pane) => pane !== null);
|
|
278
|
-
// Check if panes were removed
|
|
279
|
-
const panesRemoved = updatedPanes.length < panes.length;
|
|
280
|
-
if (panesRemoved) {
|
|
281
|
-
// Save the updated list with removed panes
|
|
282
|
-
await fs.writeFile(panesFile, JSON.stringify(updatedPanes, null, 2));
|
|
283
|
-
// Reload to sync state properly
|
|
284
|
-
await loadPanes();
|
|
285
|
-
}
|
|
286
|
-
else {
|
|
287
|
-
// Only update status if no structural changes
|
|
288
|
-
const hasStatusChanges = updatedPanes.some((pane, index) => pane.agentStatus !== panes[index]?.agentStatus);
|
|
289
|
-
if (hasStatusChanges) {
|
|
290
|
-
// Update state with new status but don't save to file
|
|
291
|
-
// Agent status is ephemeral and shouldn't persist
|
|
292
|
-
setPanes(updatedPanes);
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
};
|
|
296
|
-
// Run monitoring after delay
|
|
297
|
-
monitorAgentStatus();
|
|
298
|
-
// Set up interval for continuous monitoring (less frequent for better performance)
|
|
299
|
-
const agentInterval = setInterval(monitorAgentStatus, 2000); // Check every 2 seconds
|
|
300
|
-
return () => {
|
|
301
|
-
clearInterval(agentInterval);
|
|
302
|
-
};
|
|
303
|
-
}, 500); // Wait 500ms before starting monitoring
|
|
304
|
-
return () => {
|
|
305
|
-
clearTimeout(startupDelay);
|
|
306
|
-
};
|
|
307
|
-
}, [panes, panesFile, showNewPaneDialog, showMergeConfirmation, showCloseOptions,
|
|
308
|
-
showCommandPrompt, showFileCopyPrompt, showUpdateDialog]); // Re-run when panes or dialogs change
|
|
309
|
-
const loadSettings = async () => {
|
|
310
|
-
try {
|
|
311
|
-
const content = await fs.readFile(settingsFile, 'utf-8');
|
|
312
|
-
const settings = JSON.parse(content);
|
|
313
|
-
setProjectSettings(settings);
|
|
314
|
-
}
|
|
315
|
-
catch {
|
|
316
|
-
// Settings file doesn't exist yet, that's ok
|
|
317
|
-
setProjectSettings({});
|
|
318
|
-
}
|
|
319
|
-
};
|
|
320
|
-
const saveSettings = async (settings) => {
|
|
321
|
-
await fs.writeFile(settingsFile, JSON.stringify(settings, null, 2));
|
|
322
|
-
setProjectSettings(settings);
|
|
323
|
-
};
|
|
324
|
-
const loadPanes = async () => {
|
|
325
|
-
// Skip loading if any dialog is open to avoid UI freezing
|
|
326
|
-
if (showNewPaneDialog || showMergeConfirmation || showCloseOptions ||
|
|
327
|
-
showCommandPrompt || showFileCopyPrompt || showUpdateDialog) {
|
|
328
|
-
return;
|
|
329
|
-
}
|
|
330
|
-
if (isLoading) {
|
|
331
|
-
// Don't set loading to false immediately - keep it true for initial load
|
|
332
|
-
}
|
|
333
|
-
try {
|
|
334
|
-
const content = await fs.readFile(panesFile, 'utf-8');
|
|
335
|
-
const loadedPanes = JSON.parse(content);
|
|
336
|
-
// Get all pane IDs with retries for stability
|
|
337
|
-
let allPaneIds = [];
|
|
338
|
-
let retryCount = 0;
|
|
339
|
-
const maxRetries = 2;
|
|
340
|
-
while (retryCount <= maxRetries) {
|
|
341
|
-
try {
|
|
342
|
-
const output = execSync(`tmux list-panes -F '#{pane_id}'`, {
|
|
343
|
-
encoding: 'utf-8',
|
|
344
|
-
stdio: 'pipe',
|
|
345
|
-
timeout: 1000 // 1 second timeout
|
|
346
|
-
});
|
|
347
|
-
allPaneIds = output.trim().split('\n').filter(id => id && id.startsWith('%'));
|
|
348
|
-
// If we got results, break out of retry loop
|
|
349
|
-
if (allPaneIds.length > 0 || retryCount === maxRetries) {
|
|
350
|
-
break;
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
|
-
catch (error) {
|
|
354
|
-
// On error, wait a bit before retry
|
|
355
|
-
if (retryCount < maxRetries) {
|
|
356
|
-
await new Promise(resolve => setTimeout(resolve, 100));
|
|
357
|
-
}
|
|
358
|
-
}
|
|
359
|
-
retryCount++;
|
|
360
|
-
}
|
|
361
|
-
// Filter out dead panes - but keep panes from file as source of truth
|
|
362
|
-
// Only remove panes that definitively don't exist
|
|
363
|
-
const activePanes = loadedPanes.filter(pane => {
|
|
364
|
-
// If we couldn't get pane list, keep all panes (safer)
|
|
365
|
-
if (allPaneIds.length === 0) {
|
|
366
|
-
return true;
|
|
367
|
-
}
|
|
368
|
-
return allPaneIds.includes(pane.paneId);
|
|
369
|
-
});
|
|
370
|
-
// Only update state if there's an actual change to avoid re-renders
|
|
371
|
-
const currentPaneIds = panes.map(p => p.paneId).sort().join(',');
|
|
372
|
-
const newPaneIds = activePanes.map(p => p.paneId).sort().join(',');
|
|
373
|
-
if (currentPaneIds !== newPaneIds || panes.length === 0) {
|
|
374
|
-
// Batch update all pane titles in one go (only if needed)
|
|
375
|
-
if (activePanes.length > 0) {
|
|
376
|
-
// Update titles for all active panes at once
|
|
377
|
-
activePanes.forEach(pane => {
|
|
378
|
-
try {
|
|
379
|
-
execSync(`tmux select-pane -t '${pane.paneId}' -T "${pane.slug}"`, { stdio: 'pipe' });
|
|
380
|
-
}
|
|
381
|
-
catch {
|
|
382
|
-
// Ignore if setting title fails
|
|
383
|
-
}
|
|
384
|
-
});
|
|
385
|
-
}
|
|
386
|
-
setPanes(activePanes);
|
|
387
|
-
// Save cleaned list
|
|
388
|
-
if (activePanes.length !== loadedPanes.length) {
|
|
389
|
-
await fs.writeFile(panesFile, JSON.stringify(activePanes, null, 2));
|
|
390
|
-
}
|
|
391
|
-
}
|
|
392
|
-
// Set loading to false after first load
|
|
393
|
-
if (isLoading) {
|
|
394
|
-
setIsLoading(false);
|
|
395
|
-
}
|
|
396
|
-
}
|
|
397
|
-
catch {
|
|
398
|
-
// Don't clear panes on error - keep current state
|
|
399
|
-
// Set loading to false even on error
|
|
400
|
-
if (isLoading) {
|
|
401
|
-
setIsLoading(false);
|
|
402
|
-
}
|
|
403
|
-
}
|
|
404
|
-
};
|
|
405
|
-
const getPanePositions = () => {
|
|
406
|
-
try {
|
|
407
|
-
const output = execSync(`tmux list-panes -F '#{pane_id} #{pane_left} #{pane_top} #{pane_width} #{pane_height}'`, { encoding: 'utf-8', stdio: 'pipe' }).trim();
|
|
408
|
-
return output.split('\n').map(line => {
|
|
409
|
-
const [paneId, left, top, width, height] = line.split(' ');
|
|
410
|
-
return {
|
|
411
|
-
paneId,
|
|
412
|
-
left: parseInt(left),
|
|
413
|
-
top: parseInt(top),
|
|
414
|
-
width: parseInt(width),
|
|
415
|
-
height: parseInt(height)
|
|
416
|
-
};
|
|
417
|
-
});
|
|
418
|
-
}
|
|
419
|
-
catch {
|
|
420
|
-
return [];
|
|
421
|
-
}
|
|
422
|
-
};
|
|
423
|
-
// Calculate the visual grid position of cards based on their index
|
|
424
|
-
const getCardGridPosition = (index) => {
|
|
425
|
-
// Cards are displayed in a flexbox with wrapping
|
|
426
|
-
// With card width of 35 and typical terminal width of 80-120, we get 2-3 cards per row
|
|
427
|
-
const cardWidth = 35 + 2; // Card width plus gap
|
|
428
|
-
const cardsPerRow = Math.max(1, Math.floor(terminalWidth / cardWidth));
|
|
429
|
-
const row = Math.floor(index / cardsPerRow);
|
|
430
|
-
const col = index % cardsPerRow;
|
|
431
|
-
return { row, col };
|
|
432
|
-
};
|
|
433
|
-
const findCardInDirection = (currentIndex, direction) => {
|
|
434
|
-
const totalItems = panes.length + (isLoading ? 0 : 1); // +1 for "New dmux pane" button when not loading
|
|
435
|
-
const currentPos = getCardGridPosition(currentIndex);
|
|
436
|
-
// Calculate cards per row based on current terminal width
|
|
437
|
-
const cardWidth = 35 + 2; // Card width plus gap
|
|
438
|
-
const cardsPerRow = Math.max(1, Math.floor(terminalWidth / cardWidth));
|
|
439
|
-
let targetIndex = null;
|
|
440
|
-
switch (direction) {
|
|
441
|
-
case 'up':
|
|
442
|
-
// Move up one row, same column
|
|
443
|
-
if (currentPos.row > 0) {
|
|
444
|
-
targetIndex = (currentPos.row - 1) * cardsPerRow + currentPos.col;
|
|
445
|
-
// Make sure target exists
|
|
446
|
-
if (targetIndex >= totalItems) {
|
|
447
|
-
// Try to find the last item in the row above
|
|
448
|
-
targetIndex = Math.min((currentPos.row - 1) * cardsPerRow + cardsPerRow - 1, totalItems - 1);
|
|
449
|
-
}
|
|
450
|
-
}
|
|
451
|
-
break;
|
|
452
|
-
case 'down':
|
|
453
|
-
// Move down one row, same column
|
|
454
|
-
targetIndex = (currentPos.row + 1) * cardsPerRow + currentPos.col;
|
|
455
|
-
if (targetIndex >= totalItems) {
|
|
456
|
-
// If moving down from last row, try to go to "New dmux pane" if not already there
|
|
457
|
-
if (currentIndex < totalItems - 1) {
|
|
458
|
-
targetIndex = totalItems - 1;
|
|
459
|
-
}
|
|
460
|
-
else {
|
|
461
|
-
targetIndex = null;
|
|
462
|
-
}
|
|
463
|
-
}
|
|
464
|
-
break;
|
|
465
|
-
case 'left':
|
|
466
|
-
// Move left one column, same row
|
|
467
|
-
if (currentPos.col > 0) {
|
|
468
|
-
targetIndex = currentIndex - 1;
|
|
469
|
-
}
|
|
470
|
-
else if (currentPos.row > 0) {
|
|
471
|
-
// Wrap to end of previous row
|
|
472
|
-
targetIndex = currentPos.row * cardsPerRow - 1;
|
|
473
|
-
if (targetIndex >= totalItems) {
|
|
474
|
-
targetIndex = totalItems - 1;
|
|
475
|
-
}
|
|
476
|
-
}
|
|
477
|
-
break;
|
|
478
|
-
case 'right':
|
|
479
|
-
// Move right one column, same row
|
|
480
|
-
if (currentPos.col < cardsPerRow - 1 && currentIndex < totalItems - 1) {
|
|
481
|
-
targetIndex = currentIndex + 1;
|
|
482
|
-
}
|
|
483
|
-
else if ((currentPos.row + 1) * cardsPerRow < totalItems) {
|
|
484
|
-
// Wrap to start of next row
|
|
485
|
-
targetIndex = (currentPos.row + 1) * cardsPerRow;
|
|
486
|
-
}
|
|
487
|
-
break;
|
|
488
|
-
}
|
|
489
|
-
// Validate target index
|
|
490
|
-
if (targetIndex !== null && targetIndex >= 0 && targetIndex < totalItems) {
|
|
491
|
-
return targetIndex;
|
|
492
|
-
}
|
|
493
|
-
return null;
|
|
494
|
-
};
|
|
495
|
-
const findPaneInDirection = (currentPane, direction) => {
|
|
496
|
-
const positions = getPanePositions();
|
|
497
|
-
const currentPos = positions.find(p => p.paneId === currentPane.paneId);
|
|
498
|
-
if (!currentPos)
|
|
499
|
-
return null;
|
|
500
|
-
// Calculate center point of current pane
|
|
501
|
-
const currentCenterX = currentPos.left + currentPos.width / 2;
|
|
502
|
-
const currentCenterY = currentPos.top + currentPos.height / 2;
|
|
503
|
-
// Filter panes based on direction
|
|
504
|
-
const candidatePanes = panes.filter(pane => {
|
|
505
|
-
if (pane.paneId === currentPane.paneId)
|
|
506
|
-
return false;
|
|
507
|
-
const pos = positions.find(p => p.paneId === pane.paneId);
|
|
508
|
-
if (!pos)
|
|
509
|
-
return false;
|
|
510
|
-
const centerX = pos.left + pos.width / 2;
|
|
511
|
-
const centerY = pos.top + pos.height / 2;
|
|
512
|
-
switch (direction) {
|
|
513
|
-
case 'up':
|
|
514
|
-
return centerY < currentCenterY;
|
|
515
|
-
case 'down':
|
|
516
|
-
return centerY > currentCenterY;
|
|
517
|
-
case 'left':
|
|
518
|
-
return centerX < currentCenterX;
|
|
519
|
-
case 'right':
|
|
520
|
-
return centerX > currentCenterX;
|
|
521
|
-
default:
|
|
522
|
-
return false;
|
|
523
|
-
}
|
|
524
|
-
});
|
|
525
|
-
if (candidatePanes.length === 0)
|
|
526
|
-
return null;
|
|
527
|
-
// Find the closest pane in the given direction
|
|
528
|
-
let closestPane = candidatePanes[0];
|
|
529
|
-
let minDistance = Infinity;
|
|
530
|
-
for (const pane of candidatePanes) {
|
|
531
|
-
const pos = positions.find(p => p.paneId === pane.paneId);
|
|
532
|
-
if (!pos)
|
|
533
|
-
continue;
|
|
534
|
-
const centerX = pos.left + pos.width / 2;
|
|
535
|
-
const centerY = pos.top + pos.height / 2;
|
|
536
|
-
let distance;
|
|
537
|
-
switch (direction) {
|
|
538
|
-
case 'up':
|
|
539
|
-
case 'down':
|
|
540
|
-
// For vertical movement, prioritize vertical alignment, then distance
|
|
541
|
-
const xDiff = Math.abs(centerX - currentCenterX);
|
|
542
|
-
const yDiff = Math.abs(centerY - currentCenterY);
|
|
543
|
-
// Weight horizontal difference less than vertical
|
|
544
|
-
distance = yDiff + xDiff * 0.3;
|
|
545
|
-
break;
|
|
546
|
-
case 'left':
|
|
547
|
-
case 'right':
|
|
548
|
-
// For horizontal movement, prioritize horizontal alignment, then distance
|
|
549
|
-
const xDiff2 = Math.abs(centerX - currentCenterX);
|
|
550
|
-
const yDiff2 = Math.abs(centerY - currentCenterY);
|
|
551
|
-
// Weight vertical difference less than horizontal
|
|
552
|
-
distance = xDiff2 + yDiff2 * 0.3;
|
|
553
|
-
break;
|
|
554
|
-
}
|
|
555
|
-
if (distance < minDistance) {
|
|
556
|
-
minDistance = distance;
|
|
557
|
-
closestPane = pane;
|
|
558
|
-
}
|
|
559
|
-
}
|
|
560
|
-
return closestPane;
|
|
561
|
-
};
|
|
562
|
-
const savePanes = async (newPanes) => {
|
|
563
|
-
// Filter out any panes that no longer exist in tmux before saving
|
|
564
|
-
let activePanes = newPanes;
|
|
565
|
-
try {
|
|
566
|
-
const paneIds = execSync(`tmux list-panes -F '#{pane_id}'`, {
|
|
567
|
-
encoding: 'utf-8',
|
|
568
|
-
stdio: 'pipe',
|
|
569
|
-
timeout: 1000
|
|
570
|
-
}).trim().split('\n').filter(id => id && id.startsWith('%'));
|
|
571
|
-
// Only keep panes that still exist
|
|
572
|
-
activePanes = newPanes.filter(pane => paneIds.includes(pane.paneId));
|
|
573
|
-
}
|
|
574
|
-
catch {
|
|
575
|
-
// If we can't verify, save all panes
|
|
576
|
-
activePanes = newPanes;
|
|
577
|
-
}
|
|
578
|
-
await fs.writeFile(panesFile, JSON.stringify(activePanes, null, 2));
|
|
579
|
-
setPanes(activePanes);
|
|
580
|
-
};
|
|
581
|
-
const applySmartLayout = (paneCount) => {
|
|
582
|
-
try {
|
|
583
|
-
// Progressive layout strategy based on pane count
|
|
584
|
-
if (paneCount <= 2) {
|
|
585
|
-
// 2 panes: side by side
|
|
586
|
-
execSync('tmux select-layout even-horizontal', { stdio: 'pipe' });
|
|
587
|
-
}
|
|
588
|
-
else if (paneCount === 3) {
|
|
589
|
-
// 3 panes: primary top, two bottom
|
|
590
|
-
execSync('tmux select-layout main-horizontal', { stdio: 'pipe' });
|
|
591
|
-
}
|
|
592
|
-
else if (paneCount === 4) {
|
|
593
|
-
// 4 panes: 2x2 grid
|
|
594
|
-
execSync('tmux select-layout tiled', { stdio: 'pipe' });
|
|
595
|
-
}
|
|
596
|
-
else if (paneCount === 5) {
|
|
597
|
-
// 5 panes: 2 top, 3 bottom
|
|
598
|
-
execSync('tmux select-layout tiled', { stdio: 'pipe' });
|
|
599
|
-
// Custom adjustment for better 2-over-3 layout
|
|
600
|
-
try {
|
|
601
|
-
execSync('tmux resize-pane -t 0 -y 50%', { stdio: 'pipe' });
|
|
602
|
-
}
|
|
603
|
-
catch { }
|
|
604
|
-
}
|
|
605
|
-
else if (paneCount === 6) {
|
|
606
|
-
// 6 panes: 3x2 grid
|
|
607
|
-
execSync('tmux select-layout tiled', { stdio: 'pipe' });
|
|
608
|
-
}
|
|
609
|
-
else if (paneCount <= 9) {
|
|
610
|
-
// 7-9 panes: 3x3 grid
|
|
611
|
-
execSync('tmux select-layout tiled', { stdio: 'pipe' });
|
|
612
|
-
}
|
|
613
|
-
else if (paneCount <= 12) {
|
|
614
|
-
// 10-12 panes: 3x4 grid
|
|
615
|
-
execSync('tmux select-layout tiled', { stdio: 'pipe' });
|
|
616
|
-
}
|
|
617
|
-
else if (paneCount <= 16) {
|
|
618
|
-
// 13-16 panes: 4x4 grid
|
|
619
|
-
execSync('tmux select-layout tiled', { stdio: 'pipe' });
|
|
620
|
-
}
|
|
621
|
-
else {
|
|
622
|
-
// More than 16: use tiled for best automatic arrangement
|
|
623
|
-
execSync('tmux select-layout tiled', { stdio: 'pipe' });
|
|
624
|
-
}
|
|
625
|
-
// Refresh client to ensure layout is applied
|
|
626
|
-
execSync('tmux refresh-client', { stdio: 'pipe' });
|
|
627
|
-
}
|
|
628
|
-
catch (error) {
|
|
629
|
-
// Fallback to even-horizontal if custom layout fails
|
|
630
|
-
try {
|
|
631
|
-
execSync('tmux select-layout even-horizontal', { stdio: 'pipe' });
|
|
632
|
-
}
|
|
633
|
-
catch { }
|
|
634
|
-
}
|
|
635
|
-
};
|
|
636
|
-
const findClaudeCommand = async () => {
|
|
637
|
-
// Prefer passive detection to avoid triggering installers
|
|
638
|
-
try {
|
|
639
|
-
const userShell = process.env.SHELL || '/bin/bash';
|
|
640
|
-
const result = execSync(`${userShell} -i -c "command -v claude 2>/dev/null || which claude 2>/dev/null"`, { encoding: 'utf-8', stdio: 'pipe' }).trim();
|
|
641
|
-
if (result)
|
|
642
|
-
return result.split('\n')[0];
|
|
643
|
-
}
|
|
644
|
-
catch { }
|
|
645
|
-
// Check common installation paths without executing
|
|
646
|
-
const commonPaths = [
|
|
647
|
-
`${process.env.HOME}/.claude/local/claude`,
|
|
648
|
-
`${process.env.HOME}/.local/bin/claude`,
|
|
649
|
-
'/usr/local/bin/claude',
|
|
650
|
-
'/opt/homebrew/bin/claude',
|
|
651
|
-
'/usr/bin/claude',
|
|
652
|
-
`${process.env.HOME}/bin/claude`,
|
|
653
|
-
];
|
|
654
|
-
for (const p of commonPaths) {
|
|
655
|
-
try {
|
|
656
|
-
await fs.access(p);
|
|
657
|
-
return p;
|
|
658
|
-
}
|
|
659
|
-
catch { }
|
|
660
|
-
}
|
|
661
|
-
return null;
|
|
662
|
-
};
|
|
663
|
-
const findopencodeCommand = async () => {
|
|
664
|
-
// Passive detection only (no execution)
|
|
665
|
-
try {
|
|
666
|
-
const userShell = process.env.SHELL || '/bin/bash';
|
|
667
|
-
const result = execSync(`${userShell} -i -c "command -v opencode 2>/dev/null || which opencode 2>/dev/null"`, { encoding: 'utf-8', stdio: 'pipe' }).trim();
|
|
668
|
-
if (result)
|
|
669
|
-
return result.split('\n')[0];
|
|
670
|
-
}
|
|
671
|
-
catch { }
|
|
672
|
-
const commonPaths = [
|
|
673
|
-
'/opt/homebrew/bin/opencode',
|
|
674
|
-
'/usr/local/bin/opencode',
|
|
675
|
-
`${process.env.HOME}/.local/bin/opencode`,
|
|
676
|
-
`${process.env.HOME}/bin/opencode`,
|
|
677
|
-
];
|
|
678
|
-
for (const p of commonPaths) {
|
|
679
|
-
try {
|
|
680
|
-
await fs.access(p);
|
|
681
|
-
return p;
|
|
682
|
-
}
|
|
683
|
-
catch { }
|
|
684
|
-
}
|
|
685
|
-
return null;
|
|
686
|
-
};
|
|
687
|
-
const callClaudeCode = async (prompt) => {
|
|
688
|
-
try {
|
|
689
|
-
// Use a simpler approach: pipe the prompt via stdin and capture output
|
|
690
|
-
const result = execSync(`echo "${prompt.replace(/"/g, '\\"')}" | claude --no-interactive --max-turns 1 2>/dev/null | head -n 5`, {
|
|
691
|
-
encoding: 'utf-8',
|
|
692
|
-
stdio: 'pipe',
|
|
693
|
-
timeout: 5000 // 5 second timeout
|
|
694
|
-
});
|
|
695
|
-
// Extract just the content (first few lines should have the response)
|
|
696
|
-
const lines = result.trim().split('\n');
|
|
697
|
-
const response = lines.join(' ').trim();
|
|
698
|
-
return response || null;
|
|
699
|
-
}
|
|
700
|
-
catch (error) {
|
|
701
|
-
// Claude not available or error occurred
|
|
702
|
-
return null;
|
|
703
|
-
}
|
|
704
|
-
};
|
|
705
|
-
const generateSlug = async (prompt) => {
|
|
706
|
-
if (!prompt) {
|
|
707
|
-
return `dmux-${Date.now()}`;
|
|
708
|
-
}
|
|
709
|
-
// Try OpenRouter first if API key is available
|
|
710
|
-
const apiKey = process.env.OPENROUTER_API_KEY;
|
|
711
|
-
if (apiKey) {
|
|
712
|
-
try {
|
|
713
|
-
const response = await fetch('https://openrouter.ai/api/v1/chat/completions', {
|
|
714
|
-
method: 'POST',
|
|
715
|
-
headers: {
|
|
716
|
-
'Content-Type': 'application/json',
|
|
717
|
-
'Authorization': `Bearer ${apiKey}`,
|
|
718
|
-
},
|
|
719
|
-
body: JSON.stringify({
|
|
720
|
-
model: 'openai/gpt-4o-mini',
|
|
721
|
-
messages: [
|
|
722
|
-
{
|
|
723
|
-
role: 'user',
|
|
724
|
-
content: `Generate a 1-2 word kebab-case slug for this prompt. Only respond with the slug, nothing else: "${prompt}"`
|
|
725
|
-
}
|
|
726
|
-
],
|
|
727
|
-
max_tokens: 10,
|
|
728
|
-
temperature: 0.3
|
|
729
|
-
})
|
|
730
|
-
});
|
|
731
|
-
if (response.ok) {
|
|
732
|
-
const data = await response.json();
|
|
733
|
-
const slug = data.choices[0].message.content.trim().toLowerCase().replace(/[^a-z0-9-]/g, '');
|
|
734
|
-
if (slug)
|
|
735
|
-
return slug;
|
|
736
|
-
}
|
|
737
|
-
}
|
|
738
|
-
catch {
|
|
739
|
-
// Fall through to Claude Code
|
|
740
|
-
}
|
|
741
|
-
}
|
|
742
|
-
// Try Claude Code as fallback
|
|
743
|
-
const claudeResponse = await callClaudeCode(`Generate a 1-2 word kebab-case slug for this prompt. Only respond with the slug, nothing else: "${prompt}"`);
|
|
744
|
-
if (claudeResponse) {
|
|
745
|
-
const slug = claudeResponse.trim().toLowerCase().replace(/[^a-z0-9-]/g, '');
|
|
746
|
-
if (slug)
|
|
747
|
-
return slug;
|
|
748
|
-
}
|
|
749
|
-
// Final fallback
|
|
750
|
-
return `dmux-${Date.now()}`;
|
|
751
|
-
};
|
|
752
|
-
const openInEditor = async () => {
|
|
753
|
-
try {
|
|
754
|
-
const os = require('os');
|
|
755
|
-
const fs = require('fs');
|
|
756
|
-
const tmpFile = path.join(os.tmpdir(), `dmux-prompt-${Date.now()}.md`);
|
|
757
|
-
// Write current prompt to temp file
|
|
758
|
-
fs.writeFileSync(tmpFile, newPanePrompt || '# Enter your Claude prompt here\n\n');
|
|
759
|
-
// Get editor from environment or use default
|
|
760
|
-
const editor = process.env.EDITOR || process.env.VISUAL || 'nano';
|
|
761
|
-
// Clear screen and open editor
|
|
762
|
-
process.stdout.write('\x1b[2J\x1b[H');
|
|
763
|
-
// Use spawn to open editor in foreground
|
|
764
|
-
const { spawn } = require('child_process');
|
|
765
|
-
const editorProcess = spawn(editor, [tmpFile], {
|
|
766
|
-
stdio: 'inherit',
|
|
767
|
-
shell: true
|
|
768
|
-
});
|
|
769
|
-
editorProcess.on('close', (code) => {
|
|
770
|
-
// Read the file back
|
|
771
|
-
try {
|
|
772
|
-
const content = fs.readFileSync(tmpFile, 'utf8')
|
|
773
|
-
.replace(/^# Enter your Claude prompt here\s*\n*/m, '')
|
|
774
|
-
.trim();
|
|
775
|
-
setNewPanePrompt(content);
|
|
776
|
-
// Clean up temp file
|
|
777
|
-
fs.unlinkSync(tmpFile);
|
|
778
|
-
// Clear screen and return to dmux
|
|
779
|
-
process.stdout.write('\x1b[2J\x1b[H');
|
|
780
|
-
}
|
|
781
|
-
catch (error) {
|
|
782
|
-
// If file read fails, just continue
|
|
783
|
-
}
|
|
784
|
-
});
|
|
785
|
-
}
|
|
786
|
-
catch (error) {
|
|
787
|
-
// If editor fails, just continue with inline input
|
|
788
|
-
}
|
|
789
|
-
};
|
|
136
|
+
if (agentChoice == null && availableAgents.length > 0) {
|
|
137
|
+
setAgentChoice(availableAgents[0] || 'claude');
|
|
138
|
+
}
|
|
139
|
+
}, [availableAgents]);
|
|
140
|
+
// Monitor agent status across panes
|
|
141
|
+
useAgentStatus({
|
|
142
|
+
panes,
|
|
143
|
+
setPanes,
|
|
144
|
+
panesFile,
|
|
145
|
+
suspend: showNewPaneDialog || showMergeConfirmation || showCloseOptions || !!showCommandPrompt || showFileCopyPrompt,
|
|
146
|
+
loadPanes,
|
|
147
|
+
});
|
|
148
|
+
// loadPanes moved to usePanes
|
|
149
|
+
// getPanePositions moved to utils/tmux
|
|
150
|
+
// Navigation logic moved to hook
|
|
151
|
+
const { getCardGridPosition, findCardInDirection } = useNavigation(terminalWidth, panes.length, isLoading);
|
|
152
|
+
// findCardInDirection provided by useNavigation
|
|
153
|
+
// savePanes moved to usePanes
|
|
154
|
+
// applySmartLayout moved to utils/tmux
|
|
790
155
|
const createNewPane = async (prompt, agent) => {
|
|
791
156
|
setIsCreatingPane(true);
|
|
792
157
|
setStatusMessage('Generating slug...');
|
|
@@ -804,8 +169,8 @@ const DmuxApp = ({ dmuxDir, panesFile, projectName, sessionName, settingsFile, a
|
|
|
804
169
|
// Fallback to current directory if not in a git repo
|
|
805
170
|
projectRoot = process.cwd();
|
|
806
171
|
}
|
|
807
|
-
// Create worktree path relative to project root
|
|
808
|
-
const worktreePath = path.join(path.dirname(projectRoot),
|
|
172
|
+
// Create worktree path relative to project root with dmux- prefix
|
|
173
|
+
const worktreePath = path.join(path.dirname(projectRoot), `dmux-${slug}`);
|
|
809
174
|
// Get the original pane ID (where dmux is running) before clearing
|
|
810
175
|
const originalPaneId = execSync('tmux display-message -p "#{pane_id}"', { encoding: 'utf-8' }).trim();
|
|
811
176
|
// Multiple clearing strategies to prevent artifacts
|
|
@@ -899,11 +264,12 @@ const DmuxApp = ({ dmuxDir, panesFile, projectName, sessionName, settingsFile, a
|
|
|
899
264
|
execSync(`tmux send-keys -t '${paneInfo}' '${escapedOpenCmd}'`, { stdio: 'pipe' });
|
|
900
265
|
execSync(`tmux send-keys -t '${paneInfo}' Enter`, { stdio: 'pipe' });
|
|
901
266
|
if (prompt && prompt.trim()) {
|
|
902
|
-
await new Promise(resolve => setTimeout(resolve,
|
|
267
|
+
await new Promise(resolve => setTimeout(resolve, 1500));
|
|
903
268
|
const bufName = `dmux_prompt_${Date.now()}`;
|
|
904
269
|
const promptEsc = prompt.replace(/\\/g, '\\\\').replace(/'/g, "'\\''");
|
|
905
270
|
execSync(`tmux set-buffer -b '${bufName}' -- '${promptEsc}'`, { stdio: 'pipe' });
|
|
906
|
-
execSync(`tmux paste-buffer -b '${bufName}' -t '${paneInfo}'
|
|
271
|
+
execSync(`tmux paste-buffer -b '${bufName}' -t '${paneInfo}'`, { stdio: 'pipe' });
|
|
272
|
+
await new Promise(resolve => setTimeout(resolve, 200));
|
|
907
273
|
execSync(`tmux delete-buffer -b '${bufName}'`, { stdio: 'pipe' });
|
|
908
274
|
execSync(`tmux send-keys -t '${paneInfo}' Enter`, { stdio: 'pipe' });
|
|
909
275
|
}
|
|
@@ -1072,354 +438,6 @@ const DmuxApp = ({ dmuxDir, panesFile, projectName, sessionName, settingsFile, a
|
|
|
1072
438
|
setTimeout(() => setStatusMessage(''), 2000);
|
|
1073
439
|
}
|
|
1074
440
|
};
|
|
1075
|
-
const closePane = async (pane) => {
|
|
1076
|
-
try {
|
|
1077
|
-
// Kill associated test/dev windows if they exist
|
|
1078
|
-
if (pane.testWindowId) {
|
|
1079
|
-
try {
|
|
1080
|
-
execSync(`tmux kill-window -t '${pane.testWindowId}'`, { stdio: 'pipe' });
|
|
1081
|
-
}
|
|
1082
|
-
catch { }
|
|
1083
|
-
}
|
|
1084
|
-
if (pane.devWindowId) {
|
|
1085
|
-
try {
|
|
1086
|
-
execSync(`tmux kill-window -t '${pane.devWindowId}'`, { stdio: 'pipe' });
|
|
1087
|
-
}
|
|
1088
|
-
catch { }
|
|
1089
|
-
}
|
|
1090
|
-
// Multiple clearing strategies to prevent artifacts
|
|
1091
|
-
// 1. Clear screen with ANSI codes
|
|
1092
|
-
process.stdout.write('\x1b[2J\x1b[H');
|
|
1093
|
-
// 2. Fill with blank lines to push content off screen
|
|
1094
|
-
process.stdout.write('\n'.repeat(100));
|
|
1095
|
-
// 3. Clear tmux history and send clear command
|
|
1096
|
-
try {
|
|
1097
|
-
execSync('tmux clear-history', { stdio: 'pipe' });
|
|
1098
|
-
execSync('tmux send-keys C-l', { stdio: 'pipe' });
|
|
1099
|
-
}
|
|
1100
|
-
catch { }
|
|
1101
|
-
// 4. Force tmux to refresh the display
|
|
1102
|
-
try {
|
|
1103
|
-
execSync('tmux refresh-client', { stdio: 'pipe' });
|
|
1104
|
-
}
|
|
1105
|
-
catch { }
|
|
1106
|
-
// Small delay to let clearing complete
|
|
1107
|
-
await new Promise(resolve => setTimeout(resolve, 100));
|
|
1108
|
-
// Kill the tmux pane
|
|
1109
|
-
execSync(`tmux kill-pane -t '${pane.paneId}'`, { stdio: 'pipe' });
|
|
1110
|
-
// Get current pane count to determine layout
|
|
1111
|
-
const paneCount = parseInt(execSync('tmux list-panes | wc -l', { encoding: 'utf-8' }).trim());
|
|
1112
|
-
// Apply smart layout after pane removal
|
|
1113
|
-
if (paneCount > 1) {
|
|
1114
|
-
applySmartLayout(paneCount);
|
|
1115
|
-
}
|
|
1116
|
-
// Remove from list
|
|
1117
|
-
const updatedPanes = panes.filter(p => p.id !== pane.id);
|
|
1118
|
-
await savePanes(updatedPanes);
|
|
1119
|
-
setStatusMessage(`Closed pane: ${pane.slug}`);
|
|
1120
|
-
setTimeout(() => setStatusMessage(''), 3000);
|
|
1121
|
-
}
|
|
1122
|
-
catch {
|
|
1123
|
-
setStatusMessage('Failed to close pane');
|
|
1124
|
-
setTimeout(() => setStatusMessage(''), 2000);
|
|
1125
|
-
}
|
|
1126
|
-
};
|
|
1127
|
-
const mergeAndPrune = async (pane) => {
|
|
1128
|
-
if (!pane.worktreePath) {
|
|
1129
|
-
setStatusMessage('No worktree to merge');
|
|
1130
|
-
setTimeout(() => setStatusMessage(''), 2000);
|
|
1131
|
-
return;
|
|
1132
|
-
}
|
|
1133
|
-
try {
|
|
1134
|
-
setStatusMessage('Checking worktree status...');
|
|
1135
|
-
// Get current branch
|
|
1136
|
-
const mainBranch = execSync('git branch --show-current', { encoding: 'utf-8' }).trim();
|
|
1137
|
-
// Check for uncommitted changes in the worktree
|
|
1138
|
-
const statusOutput = execSync(`git -C "${pane.worktreePath}" status --porcelain`, { encoding: 'utf-8' });
|
|
1139
|
-
if (statusOutput.trim()) {
|
|
1140
|
-
setStatusMessage('Generating commit message...');
|
|
1141
|
-
// Get the diff for uncommitted changes
|
|
1142
|
-
const diffOutput = execSync(`git -C "${pane.worktreePath}" diff HEAD`, { encoding: 'utf-8' });
|
|
1143
|
-
const statusDetails = execSync(`git -C "${pane.worktreePath}" status`, { encoding: 'utf-8' });
|
|
1144
|
-
// Generate commit message using LLM
|
|
1145
|
-
const commitMessage = await generateCommitMessage(`${statusDetails}\n\n${diffOutput}`);
|
|
1146
|
-
setStatusMessage('Committing changes...');
|
|
1147
|
-
// Stage all changes and commit with generated message
|
|
1148
|
-
execSync(`git -C "${pane.worktreePath}" add -A`, { stdio: 'pipe' });
|
|
1149
|
-
// Escape the commit message for shell
|
|
1150
|
-
const escapedMessage = commitMessage.replace(/'/g, "'\\''");
|
|
1151
|
-
execSync(`git -C "${pane.worktreePath}" commit -m '${escapedMessage}'`, { stdio: 'pipe' });
|
|
1152
|
-
}
|
|
1153
|
-
setStatusMessage('Merging into main...');
|
|
1154
|
-
// Try to merge the worktree branch
|
|
1155
|
-
try {
|
|
1156
|
-
execSync(`git merge ${pane.slug}`, { stdio: 'pipe' });
|
|
1157
|
-
}
|
|
1158
|
-
catch (mergeError) {
|
|
1159
|
-
// Check if this is a merge conflict
|
|
1160
|
-
const errorMessage = mergeError.message || mergeError.toString();
|
|
1161
|
-
if (errorMessage.includes('CONFLICT') || errorMessage.includes('conflict')) {
|
|
1162
|
-
// Merge conflict detected - exit dmux and inform user
|
|
1163
|
-
// Merge conflict detected - exit dmux and inform user
|
|
1164
|
-
process.stderr.write('\n\x1b[31m✗ Merge conflict detected!\x1b[0m\n');
|
|
1165
|
-
process.stderr.write(`\nThere are merge conflicts when merging branch '${pane.slug}' into '${mainBranch}'.\n`);
|
|
1166
|
-
process.stderr.write('\nTo resolve:\n');
|
|
1167
|
-
process.stderr.write('1. Manually resolve the merge conflicts in your editor\n');
|
|
1168
|
-
process.stderr.write('2. Stage the resolved files: git add <resolved-files>\n');
|
|
1169
|
-
process.stderr.write('3. Complete the merge: git commit\n');
|
|
1170
|
-
process.stderr.write('4. Run dmux again to continue managing your panes\n');
|
|
1171
|
-
process.stderr.write('\nExiting dmux now...\n\n');
|
|
1172
|
-
// Clean exit
|
|
1173
|
-
process.stdout.write('\x1b[2J\x1b[H');
|
|
1174
|
-
process.stdout.write('\x1b[3J');
|
|
1175
|
-
try {
|
|
1176
|
-
execSync('tmux clear-history', { stdio: 'pipe' });
|
|
1177
|
-
}
|
|
1178
|
-
catch { }
|
|
1179
|
-
process.exit(1);
|
|
1180
|
-
}
|
|
1181
|
-
else {
|
|
1182
|
-
// Some other merge error
|
|
1183
|
-
throw mergeError;
|
|
1184
|
-
}
|
|
1185
|
-
}
|
|
1186
|
-
// Remove worktree
|
|
1187
|
-
execSync(`git worktree remove "${pane.worktreePath}"`, { stdio: 'pipe' });
|
|
1188
|
-
// Delete branch
|
|
1189
|
-
execSync(`git branch -d ${pane.slug}`, { stdio: 'pipe' });
|
|
1190
|
-
// Close the pane (includes clearing)
|
|
1191
|
-
await closePane(pane);
|
|
1192
|
-
setStatusMessage(`Merged ${pane.slug} into ${mainBranch} and closed pane`);
|
|
1193
|
-
setTimeout(() => setStatusMessage(''), 3000);
|
|
1194
|
-
}
|
|
1195
|
-
catch (error) {
|
|
1196
|
-
setStatusMessage('Failed to merge - check git status');
|
|
1197
|
-
setTimeout(() => setStatusMessage(''), 3000);
|
|
1198
|
-
}
|
|
1199
|
-
};
|
|
1200
|
-
const deleteUnsavedChanges = async (pane) => {
|
|
1201
|
-
if (!pane.worktreePath) {
|
|
1202
|
-
// No worktree, just close the pane (includes clearing)
|
|
1203
|
-
await closePane(pane);
|
|
1204
|
-
return;
|
|
1205
|
-
}
|
|
1206
|
-
try {
|
|
1207
|
-
setStatusMessage('Removing worktree with unsaved changes...');
|
|
1208
|
-
// Force remove worktree (discards uncommitted changes)
|
|
1209
|
-
execSync(`git worktree remove --force "${pane.worktreePath}"`, { stdio: 'pipe' });
|
|
1210
|
-
// Delete branch
|
|
1211
|
-
try {
|
|
1212
|
-
execSync(`git branch -D ${pane.slug}`, { stdio: 'pipe' });
|
|
1213
|
-
}
|
|
1214
|
-
catch {
|
|
1215
|
-
// Branch might not exist or have commits, that's ok
|
|
1216
|
-
}
|
|
1217
|
-
// Close the pane (includes clearing)
|
|
1218
|
-
await closePane(pane);
|
|
1219
|
-
setStatusMessage(`Deleted worktree ${pane.slug} and closed pane`);
|
|
1220
|
-
setTimeout(() => setStatusMessage(''), 3000);
|
|
1221
|
-
}
|
|
1222
|
-
catch (error) {
|
|
1223
|
-
setStatusMessage('Failed to delete worktree');
|
|
1224
|
-
setTimeout(() => setStatusMessage(''), 3000);
|
|
1225
|
-
}
|
|
1226
|
-
};
|
|
1227
|
-
const handleCloseOption = async (option, pane) => {
|
|
1228
|
-
setShowCloseOptions(false);
|
|
1229
|
-
setClosingPane(null);
|
|
1230
|
-
setSelectedCloseOption(0);
|
|
1231
|
-
switch (option) {
|
|
1232
|
-
case 0: // Merge & Prune
|
|
1233
|
-
await mergeAndPrune(pane);
|
|
1234
|
-
break;
|
|
1235
|
-
case 1: // Merge Only
|
|
1236
|
-
await mergeWorktree(pane);
|
|
1237
|
-
break;
|
|
1238
|
-
case 2: // Delete Unsaved Changes
|
|
1239
|
-
await deleteUnsavedChanges(pane);
|
|
1240
|
-
break;
|
|
1241
|
-
case 3: // Just Close
|
|
1242
|
-
await closePane(pane);
|
|
1243
|
-
break;
|
|
1244
|
-
}
|
|
1245
|
-
};
|
|
1246
|
-
const generateCommitMessage = async (changes) => {
|
|
1247
|
-
// Try OpenRouter first if API key is available
|
|
1248
|
-
const apiKey = process.env.OPENROUTER_API_KEY;
|
|
1249
|
-
if (apiKey) {
|
|
1250
|
-
try {
|
|
1251
|
-
const response = await fetch('https://openrouter.ai/api/v1/chat/completions', {
|
|
1252
|
-
method: 'POST',
|
|
1253
|
-
headers: {
|
|
1254
|
-
'Content-Type': 'application/json',
|
|
1255
|
-
'Authorization': `Bearer ${apiKey}`,
|
|
1256
|
-
},
|
|
1257
|
-
body: JSON.stringify({
|
|
1258
|
-
model: 'openai/gpt-4o-mini',
|
|
1259
|
-
messages: [
|
|
1260
|
-
{
|
|
1261
|
-
role: 'system',
|
|
1262
|
-
content: 'You are a git commit message generator. Generate semantic commit messages following conventional commits format (feat:, fix:, docs:, style:, refactor:, test:, chore:). Be concise and specific.'
|
|
1263
|
-
},
|
|
1264
|
-
{
|
|
1265
|
-
role: 'user',
|
|
1266
|
-
content: `Generate a semantic commit message for these changes:\n\n${changes.substring(0, 3000)}`
|
|
1267
|
-
}
|
|
1268
|
-
],
|
|
1269
|
-
max_tokens: 100,
|
|
1270
|
-
temperature: 0.3
|
|
1271
|
-
})
|
|
1272
|
-
});
|
|
1273
|
-
if (response.ok) {
|
|
1274
|
-
const data = await response.json();
|
|
1275
|
-
const message = data.choices[0].message.content.trim();
|
|
1276
|
-
if (message)
|
|
1277
|
-
return message;
|
|
1278
|
-
}
|
|
1279
|
-
}
|
|
1280
|
-
catch {
|
|
1281
|
-
// Fall through to Claude Code
|
|
1282
|
-
}
|
|
1283
|
-
}
|
|
1284
|
-
// Try Claude Code as fallback
|
|
1285
|
-
const systemPrompt = 'You are a git commit message generator. Generate semantic commit messages following conventional commits format (feat:, fix:, docs:, style:, refactor:, test:, chore:). Be concise and specific.';
|
|
1286
|
-
const userPrompt = `${systemPrompt}\n\nGenerate a semantic commit message for these changes:\n\n${changes.substring(0, 3000)}`;
|
|
1287
|
-
const claudeResponse = await callClaudeCode(userPrompt);
|
|
1288
|
-
if (claudeResponse) {
|
|
1289
|
-
const message = claudeResponse.trim();
|
|
1290
|
-
if (message)
|
|
1291
|
-
return message;
|
|
1292
|
-
}
|
|
1293
|
-
// Final fallback
|
|
1294
|
-
return 'chore: merge worktree changes';
|
|
1295
|
-
};
|
|
1296
|
-
const detectPackageManager = async () => {
|
|
1297
|
-
try {
|
|
1298
|
-
// Get project root
|
|
1299
|
-
const projectRoot = execSync('git rev-parse --show-toplevel', {
|
|
1300
|
-
encoding: 'utf-8',
|
|
1301
|
-
stdio: 'pipe'
|
|
1302
|
-
}).trim();
|
|
1303
|
-
// Check if package.json exists
|
|
1304
|
-
try {
|
|
1305
|
-
await fs.access(path.join(projectRoot, 'package.json'));
|
|
1306
|
-
// Check for lock files to determine package manager
|
|
1307
|
-
const files = await fs.readdir(projectRoot);
|
|
1308
|
-
if (files.includes('pnpm-lock.yaml')) {
|
|
1309
|
-
return { manager: 'pnpm', hasPackageJson: true };
|
|
1310
|
-
}
|
|
1311
|
-
else if (files.includes('yarn.lock')) {
|
|
1312
|
-
return { manager: 'yarn', hasPackageJson: true };
|
|
1313
|
-
}
|
|
1314
|
-
else if (files.includes('package-lock.json')) {
|
|
1315
|
-
return { manager: 'npm', hasPackageJson: true };
|
|
1316
|
-
}
|
|
1317
|
-
else {
|
|
1318
|
-
// Default to npm if no lock file found
|
|
1319
|
-
return { manager: 'npm', hasPackageJson: true };
|
|
1320
|
-
}
|
|
1321
|
-
}
|
|
1322
|
-
catch {
|
|
1323
|
-
// No package.json found
|
|
1324
|
-
return { manager: null, hasPackageJson: false };
|
|
1325
|
-
}
|
|
1326
|
-
}
|
|
1327
|
-
catch {
|
|
1328
|
-
return { manager: null, hasPackageJson: false };
|
|
1329
|
-
}
|
|
1330
|
-
};
|
|
1331
|
-
const suggestCommand = async (type) => {
|
|
1332
|
-
const { manager, hasPackageJson } = await detectPackageManager();
|
|
1333
|
-
if (!hasPackageJson) {
|
|
1334
|
-
return null;
|
|
1335
|
-
}
|
|
1336
|
-
// Suggest standard commands based on package manager
|
|
1337
|
-
if (type === 'test') {
|
|
1338
|
-
return `${manager} run test`;
|
|
1339
|
-
}
|
|
1340
|
-
else {
|
|
1341
|
-
return `${manager} run dev`;
|
|
1342
|
-
}
|
|
1343
|
-
};
|
|
1344
|
-
const copyNonGitFiles = async (worktreePath) => {
|
|
1345
|
-
try {
|
|
1346
|
-
setStatusMessage('Copying non-git files from main...');
|
|
1347
|
-
// Get project root
|
|
1348
|
-
const projectRoot = execSync('git rev-parse --show-toplevel', {
|
|
1349
|
-
encoding: 'utf-8',
|
|
1350
|
-
stdio: 'pipe'
|
|
1351
|
-
}).trim();
|
|
1352
|
-
// Use rsync to copy non-tracked files
|
|
1353
|
-
// This copies everything except git-tracked files, .git, and common build directories
|
|
1354
|
-
const rsyncCmd = `rsync -avz --exclude='.git' --exclude='node_modules' --exclude='dist' --exclude='build' --exclude='.next' --exclude='.turbo' "${projectRoot}/" "${worktreePath}/"`;
|
|
1355
|
-
execSync(rsyncCmd, { stdio: 'pipe' });
|
|
1356
|
-
setStatusMessage('Non-git files copied successfully');
|
|
1357
|
-
setTimeout(() => setStatusMessage(''), 2000);
|
|
1358
|
-
}
|
|
1359
|
-
catch (error) {
|
|
1360
|
-
setStatusMessage('Failed to copy non-git files');
|
|
1361
|
-
setTimeout(() => setStatusMessage(''), 2000);
|
|
1362
|
-
}
|
|
1363
|
-
};
|
|
1364
|
-
const runCommandInternal = async (type, pane) => {
|
|
1365
|
-
if (!pane.worktreePath) {
|
|
1366
|
-
setStatusMessage('No worktree path for this pane');
|
|
1367
|
-
setTimeout(() => setStatusMessage(''), 2000);
|
|
1368
|
-
return;
|
|
1369
|
-
}
|
|
1370
|
-
const command = type === 'test' ? projectSettings.testCommand : projectSettings.devCommand;
|
|
1371
|
-
if (!command) {
|
|
1372
|
-
setStatusMessage('No command configured');
|
|
1373
|
-
setTimeout(() => setStatusMessage(''), 2000);
|
|
1374
|
-
return;
|
|
1375
|
-
}
|
|
1376
|
-
try {
|
|
1377
|
-
setRunningCommand(true);
|
|
1378
|
-
setStatusMessage(`Starting ${type} in background window...`);
|
|
1379
|
-
// Kill existing window if present
|
|
1380
|
-
const existingWindowId = type === 'test' ? pane.testWindowId : pane.devWindowId;
|
|
1381
|
-
if (existingWindowId) {
|
|
1382
|
-
try {
|
|
1383
|
-
execSync(`tmux kill-window -t '${existingWindowId}'`, { stdio: 'pipe' });
|
|
1384
|
-
}
|
|
1385
|
-
catch { }
|
|
1386
|
-
}
|
|
1387
|
-
// Create a new background window for the command
|
|
1388
|
-
const windowName = `${pane.slug}-${type}`;
|
|
1389
|
-
const windowId = execSync(`tmux new-window -d -n '${windowName}' -P -F '#{window_id}'`, { encoding: 'utf-8', stdio: 'pipe' }).trim();
|
|
1390
|
-
// Create a log file to capture output
|
|
1391
|
-
const logFile = `/tmp/dmux-${pane.id}-${type}.log`;
|
|
1392
|
-
// Build the command with output capture
|
|
1393
|
-
const fullCommand = `cd "${pane.worktreePath}" && ${command} 2>&1 | tee ${logFile}`;
|
|
1394
|
-
// Send the command to the new window
|
|
1395
|
-
execSync(`tmux send-keys -t '${windowId}' '${fullCommand.replace(/'/g, "'\\''")}' Enter`, { stdio: 'pipe' });
|
|
1396
|
-
// Update pane with window info
|
|
1397
|
-
const updatedPane = {
|
|
1398
|
-
...pane,
|
|
1399
|
-
[type === 'test' ? 'testWindowId' : 'devWindowId']: windowId,
|
|
1400
|
-
[type === 'test' ? 'testStatus' : 'devStatus']: 'running'
|
|
1401
|
-
};
|
|
1402
|
-
const updatedPanes = panes.map(p => p.id === pane.id ? updatedPane : p);
|
|
1403
|
-
await savePanes(updatedPanes);
|
|
1404
|
-
// Start monitoring the output
|
|
1405
|
-
if (type === 'test') {
|
|
1406
|
-
// For tests, monitor for completion
|
|
1407
|
-
setTimeout(() => monitorTestOutput(pane.id, logFile), 2000);
|
|
1408
|
-
}
|
|
1409
|
-
else {
|
|
1410
|
-
// For dev, monitor for server URL
|
|
1411
|
-
setTimeout(() => monitorDevOutput(pane.id, logFile), 2000);
|
|
1412
|
-
}
|
|
1413
|
-
setRunningCommand(false);
|
|
1414
|
-
setStatusMessage(`${type === 'test' ? 'Test' : 'Dev server'} started in background`);
|
|
1415
|
-
setTimeout(() => setStatusMessage(''), 3000);
|
|
1416
|
-
}
|
|
1417
|
-
catch (error) {
|
|
1418
|
-
setRunningCommand(false);
|
|
1419
|
-
setStatusMessage(`Failed to run ${type} command`);
|
|
1420
|
-
setTimeout(() => setStatusMessage(''), 3000);
|
|
1421
|
-
}
|
|
1422
|
-
};
|
|
1423
441
|
const runCommand = async (type, pane) => {
|
|
1424
442
|
if (!pane.worktreePath) {
|
|
1425
443
|
setStatusMessage('No worktree path for this pane');
|
|
@@ -1492,229 +510,7 @@ const DmuxApp = ({ dmuxDir, panesFile, projectName, sessionName, settingsFile, a
|
|
|
1492
510
|
setTimeout(() => setStatusMessage(''), 3000);
|
|
1493
511
|
}
|
|
1494
512
|
};
|
|
1495
|
-
|
|
1496
|
-
try {
|
|
1497
|
-
const content = await fs.readFile(logFile, 'utf-8');
|
|
1498
|
-
// Look for common test result patterns
|
|
1499
|
-
let status = 'running';
|
|
1500
|
-
if (content.match(/(?:tests?|specs?) (?:passed|✓|succeeded)/i) ||
|
|
1501
|
-
content.match(/\b0 fail(?:ing|ed|ures?)\b/i)) {
|
|
1502
|
-
status = 'passed';
|
|
1503
|
-
}
|
|
1504
|
-
else if (content.match(/(?:tests?|specs?) (?:failed|✗|✖)/i) ||
|
|
1505
|
-
content.match(/\d+ fail(?:ing|ed|ures?)/i) ||
|
|
1506
|
-
content.match(/error:/i)) {
|
|
1507
|
-
status = 'failed';
|
|
1508
|
-
}
|
|
1509
|
-
// Check if process is still running
|
|
1510
|
-
const pane = panes.find(p => p.id === paneId);
|
|
1511
|
-
if (pane?.testWindowId) {
|
|
1512
|
-
try {
|
|
1513
|
-
execSync(`tmux list-windows -F '#{window_id}' | grep -q '${pane.testWindowId}'`, { stdio: 'pipe' });
|
|
1514
|
-
// Window still exists, check if command is done
|
|
1515
|
-
const paneOutput = execSync(`tmux capture-pane -t '${pane.testWindowId}' -p | tail -5`, { encoding: 'utf-8' });
|
|
1516
|
-
if (paneOutput.includes('$') || paneOutput.includes('#')) {
|
|
1517
|
-
// Command prompt returned, test is done
|
|
1518
|
-
if (status === 'running')
|
|
1519
|
-
status = 'passed'; // Assume success if no errors found
|
|
1520
|
-
}
|
|
1521
|
-
}
|
|
1522
|
-
catch {
|
|
1523
|
-
// Window doesn't exist or command failed
|
|
1524
|
-
if (status === 'running')
|
|
1525
|
-
status = 'failed';
|
|
1526
|
-
}
|
|
1527
|
-
}
|
|
1528
|
-
// Update pane status
|
|
1529
|
-
const updatedPanes = panes.map(p => p.id === paneId
|
|
1530
|
-
? { ...p, testStatus: status, testOutput: content.slice(-5000) } // Keep last 5000 chars
|
|
1531
|
-
: p);
|
|
1532
|
-
await savePanes(updatedPanes);
|
|
1533
|
-
// Continue monitoring if still running
|
|
1534
|
-
if (status === 'running') {
|
|
1535
|
-
setTimeout(() => monitorTestOutput(paneId, logFile), 2000);
|
|
1536
|
-
}
|
|
1537
|
-
}
|
|
1538
|
-
catch { }
|
|
1539
|
-
};
|
|
1540
|
-
const monitorDevOutput = async (paneId, logFile) => {
|
|
1541
|
-
try {
|
|
1542
|
-
const content = await fs.readFile(logFile, 'utf-8');
|
|
1543
|
-
// Look for dev server URLs
|
|
1544
|
-
const urlMatch = content.match(/https?:\/\/(?:localhost|127\.0\.0\.1|0\.0\.0\.0):\d+/i) ||
|
|
1545
|
-
content.match(/Local:\s+(https?:\/\/[^\s]+)/i) ||
|
|
1546
|
-
content.match(/listening on port (\d+)/i);
|
|
1547
|
-
let devUrl = '';
|
|
1548
|
-
if (urlMatch) {
|
|
1549
|
-
if (urlMatch[0].startsWith('http')) {
|
|
1550
|
-
devUrl = urlMatch[0];
|
|
1551
|
-
}
|
|
1552
|
-
else if (urlMatch[1]) {
|
|
1553
|
-
// If we just found a port number
|
|
1554
|
-
devUrl = `http://localhost:${urlMatch[1]}`;
|
|
1555
|
-
}
|
|
1556
|
-
}
|
|
1557
|
-
// Check if process is still running
|
|
1558
|
-
const pane = panes.find(p => p.id === paneId);
|
|
1559
|
-
let status = 'running';
|
|
1560
|
-
if (pane?.devWindowId) {
|
|
1561
|
-
try {
|
|
1562
|
-
execSync(`tmux list-windows -F '#{window_id}' | grep -q '${pane.devWindowId}'`, { stdio: 'pipe' });
|
|
1563
|
-
}
|
|
1564
|
-
catch {
|
|
1565
|
-
// Window doesn't exist
|
|
1566
|
-
status = 'stopped';
|
|
1567
|
-
}
|
|
1568
|
-
}
|
|
1569
|
-
// Update pane status
|
|
1570
|
-
const updatedPanes = panes.map(p => p.id === paneId
|
|
1571
|
-
? { ...p, devStatus: status, devUrl: devUrl || p.devUrl }
|
|
1572
|
-
: p);
|
|
1573
|
-
await savePanes(updatedPanes);
|
|
1574
|
-
// Continue monitoring if still running
|
|
1575
|
-
if (status === 'running') {
|
|
1576
|
-
setTimeout(() => monitorDevOutput(paneId, logFile), 2000);
|
|
1577
|
-
}
|
|
1578
|
-
}
|
|
1579
|
-
catch { }
|
|
1580
|
-
};
|
|
1581
|
-
const attachBackgroundWindow = async (pane, type) => {
|
|
1582
|
-
const windowId = type === 'test' ? pane.testWindowId : pane.devWindowId;
|
|
1583
|
-
if (!windowId) {
|
|
1584
|
-
setStatusMessage(`No ${type} window to attach`);
|
|
1585
|
-
setTimeout(() => setStatusMessage(''), 2000);
|
|
1586
|
-
return;
|
|
1587
|
-
}
|
|
1588
|
-
try {
|
|
1589
|
-
// Join the window to current window as a pane
|
|
1590
|
-
execSync(`tmux join-pane -h -s '${windowId}'`, { stdio: 'pipe' });
|
|
1591
|
-
// Apply smart layout
|
|
1592
|
-
const paneCount = parseInt(execSync('tmux list-panes | wc -l', { encoding: 'utf-8' }).trim());
|
|
1593
|
-
applySmartLayout(paneCount);
|
|
1594
|
-
// Focus on the newly attached pane
|
|
1595
|
-
execSync(`tmux select-pane -t '{last}'`, { stdio: 'pipe' });
|
|
1596
|
-
setStatusMessage(`Attached ${type} window`);
|
|
1597
|
-
setTimeout(() => setStatusMessage(''), 2000);
|
|
1598
|
-
}
|
|
1599
|
-
catch (error) {
|
|
1600
|
-
setStatusMessage(`Failed to attach ${type} window`);
|
|
1601
|
-
setTimeout(() => setStatusMessage(''), 2000);
|
|
1602
|
-
}
|
|
1603
|
-
};
|
|
1604
|
-
const mergeWorktree = async (pane) => {
|
|
1605
|
-
if (!pane.worktreePath) {
|
|
1606
|
-
setStatusMessage('No worktree to merge');
|
|
1607
|
-
setTimeout(() => setStatusMessage(''), 2000);
|
|
1608
|
-
return;
|
|
1609
|
-
}
|
|
1610
|
-
try {
|
|
1611
|
-
setStatusMessage('Checking worktree status...');
|
|
1612
|
-
// Get current branch
|
|
1613
|
-
const mainBranch = execSync('git branch --show-current', { encoding: 'utf-8' }).trim();
|
|
1614
|
-
// Check for uncommitted changes in the worktree
|
|
1615
|
-
const statusOutput = execSync(`git -C "${pane.worktreePath}" status --porcelain`, { encoding: 'utf-8' });
|
|
1616
|
-
if (statusOutput.trim()) {
|
|
1617
|
-
setStatusMessage('Staging changes...');
|
|
1618
|
-
// Stage all changes first (including untracked files)
|
|
1619
|
-
execSync(`git -C "${pane.worktreePath}" add -A`, { stdio: 'pipe' });
|
|
1620
|
-
setStatusMessage('Generating commit message...');
|
|
1621
|
-
// Get the diff of staged changes (after adding files)
|
|
1622
|
-
const diffOutput = execSync(`git -C "${pane.worktreePath}" diff --cached`, { encoding: 'utf-8' });
|
|
1623
|
-
const statusDetails = execSync(`git -C "${pane.worktreePath}" status`, { encoding: 'utf-8' });
|
|
1624
|
-
// Generate commit message using LLM
|
|
1625
|
-
const commitMessage = await generateCommitMessage(`${statusDetails}\n\n${diffOutput}`);
|
|
1626
|
-
setStatusMessage('Committing changes...');
|
|
1627
|
-
// Escape the commit message for shell
|
|
1628
|
-
const escapedMessage = commitMessage.replace(/'/g, "'\\''");
|
|
1629
|
-
execSync(`git -C "${pane.worktreePath}" commit -m '${escapedMessage}'`, { stdio: 'pipe' });
|
|
1630
|
-
}
|
|
1631
|
-
setStatusMessage('Merging into main...');
|
|
1632
|
-
// Try to merge the worktree branch
|
|
1633
|
-
try {
|
|
1634
|
-
execSync(`git merge ${pane.slug}`, { stdio: 'pipe' });
|
|
1635
|
-
}
|
|
1636
|
-
catch (mergeError) {
|
|
1637
|
-
// Check if this is a merge conflict
|
|
1638
|
-
const errorMessage = mergeError.message || mergeError.toString();
|
|
1639
|
-
if (errorMessage.includes('CONFLICT') || errorMessage.includes('conflict')) {
|
|
1640
|
-
// Merge conflict detected - exit dmux and inform user
|
|
1641
|
-
// Merge conflict detected - exit dmux and inform user
|
|
1642
|
-
process.stderr.write('\n\x1b[31m✗ Merge conflict detected!\x1b[0m\n');
|
|
1643
|
-
process.stderr.write(`\nThere are merge conflicts when merging branch '${pane.slug}' into '${mainBranch}'.\n`);
|
|
1644
|
-
process.stderr.write('\nTo resolve:\n');
|
|
1645
|
-
process.stderr.write('1. Manually resolve the merge conflicts in your editor\n');
|
|
1646
|
-
process.stderr.write('2. Stage the resolved files: git add <resolved-files>\n');
|
|
1647
|
-
process.stderr.write('3. Complete the merge: git commit\n');
|
|
1648
|
-
process.stderr.write('4. Run dmux again to continue managing your panes\n');
|
|
1649
|
-
process.stderr.write('\nExiting dmux now...\n\n');
|
|
1650
|
-
// Clean exit
|
|
1651
|
-
process.stdout.write('\x1b[2J\x1b[H');
|
|
1652
|
-
process.stdout.write('\x1b[3J');
|
|
1653
|
-
try {
|
|
1654
|
-
execSync('tmux clear-history', { stdio: 'pipe' });
|
|
1655
|
-
}
|
|
1656
|
-
catch { }
|
|
1657
|
-
process.exit(1);
|
|
1658
|
-
}
|
|
1659
|
-
else {
|
|
1660
|
-
// Some other merge error
|
|
1661
|
-
throw mergeError;
|
|
1662
|
-
}
|
|
1663
|
-
}
|
|
1664
|
-
// Remove worktree
|
|
1665
|
-
execSync(`git worktree remove "${pane.worktreePath}"`, { stdio: 'pipe' });
|
|
1666
|
-
// Delete branch
|
|
1667
|
-
execSync(`git branch -d ${pane.slug}`, { stdio: 'pipe' });
|
|
1668
|
-
setStatusMessage(`Merged ${pane.slug} into ${mainBranch}`);
|
|
1669
|
-
setTimeout(() => setStatusMessage(''), 3000);
|
|
1670
|
-
// Show confirmation dialog to close the pane
|
|
1671
|
-
setMergedPane(pane);
|
|
1672
|
-
setShowMergeConfirmation(true);
|
|
1673
|
-
}
|
|
1674
|
-
catch (error) {
|
|
1675
|
-
setStatusMessage('Failed to merge - check git status');
|
|
1676
|
-
setTimeout(() => setStatusMessage(''), 3000);
|
|
1677
|
-
}
|
|
1678
|
-
};
|
|
1679
|
-
// Update handling functions
|
|
1680
|
-
const performUpdate = async () => {
|
|
1681
|
-
if (!autoUpdater || !updateInfo)
|
|
1682
|
-
return;
|
|
1683
|
-
try {
|
|
1684
|
-
setIsUpdating(true);
|
|
1685
|
-
setStatusMessage('Updating dmux...');
|
|
1686
|
-
const success = await autoUpdater.performUpdate(updateInfo);
|
|
1687
|
-
if (success) {
|
|
1688
|
-
setStatusMessage('Update completed successfully! Please restart dmux.');
|
|
1689
|
-
setTimeout(() => {
|
|
1690
|
-
process.exit(0);
|
|
1691
|
-
}, 3000);
|
|
1692
|
-
}
|
|
1693
|
-
else {
|
|
1694
|
-
setStatusMessage('Update failed. Please update manually.');
|
|
1695
|
-
setTimeout(() => setStatusMessage(''), 3000);
|
|
1696
|
-
}
|
|
1697
|
-
}
|
|
1698
|
-
catch (error) {
|
|
1699
|
-
setStatusMessage('Update failed. Please update manually.');
|
|
1700
|
-
setTimeout(() => setStatusMessage(''), 3000);
|
|
1701
|
-
}
|
|
1702
|
-
finally {
|
|
1703
|
-
setIsUpdating(false);
|
|
1704
|
-
setShowUpdateDialog(false);
|
|
1705
|
-
}
|
|
1706
|
-
};
|
|
1707
|
-
const skipUpdate = async () => {
|
|
1708
|
-
if (!autoUpdater || !updateInfo)
|
|
1709
|
-
return;
|
|
1710
|
-
await autoUpdater.skipVersion(updateInfo.latestVersion);
|
|
1711
|
-
setShowUpdateDialog(false);
|
|
1712
|
-
setUpdateInfo(null);
|
|
1713
|
-
};
|
|
1714
|
-
const dismissUpdate = () => {
|
|
1715
|
-
setShowUpdateDialog(false);
|
|
1716
|
-
setUpdateInfo(null);
|
|
1717
|
-
};
|
|
513
|
+
// Update handling moved to useAutoUpdater
|
|
1718
514
|
// Cleanup function for exit
|
|
1719
515
|
const cleanExit = () => {
|
|
1720
516
|
// Clear screen multiple times to ensure no artifacts
|
|
@@ -1735,18 +531,6 @@ const DmuxApp = ({ dmuxDir, panesFile, projectName, sessionName, settingsFile, a
|
|
|
1735
531
|
// Disable input while performing operations or loading
|
|
1736
532
|
return;
|
|
1737
533
|
}
|
|
1738
|
-
if (showUpdateDialog && updateInfo) {
|
|
1739
|
-
if (input === 'u' || input === 'U') {
|
|
1740
|
-
await performUpdate();
|
|
1741
|
-
}
|
|
1742
|
-
else if (input === 's' || input === 'S') {
|
|
1743
|
-
await skipUpdate();
|
|
1744
|
-
}
|
|
1745
|
-
else if (input === 'l' || input === 'L' || key.escape) {
|
|
1746
|
-
dismissUpdate();
|
|
1747
|
-
}
|
|
1748
|
-
return;
|
|
1749
|
-
}
|
|
1750
534
|
if (showFileCopyPrompt) {
|
|
1751
535
|
if (input === 'y' || input === 'Y') {
|
|
1752
536
|
setShowFileCopyPrompt(false);
|
|
@@ -1854,7 +638,7 @@ const DmuxApp = ({ dmuxDir, panesFile, projectName, sessionName, settingsFile, a
|
|
|
1854
638
|
}
|
|
1855
639
|
else if (key.ctrl && input === 'o') {
|
|
1856
640
|
// Open in external editor
|
|
1857
|
-
|
|
641
|
+
openEditor2(newPanePrompt, setNewPanePrompt);
|
|
1858
642
|
}
|
|
1859
643
|
// TextInput handles other input events
|
|
1860
644
|
return;
|
|
@@ -1886,9 +670,18 @@ const DmuxApp = ({ dmuxDir, panesFile, projectName, sessionName, settingsFile, a
|
|
|
1886
670
|
setSelectedCloseOption(Math.min(3, selectedCloseOption + 1));
|
|
1887
671
|
}
|
|
1888
672
|
else if (key.return && closingPane) {
|
|
1889
|
-
handleCloseOption(selectedCloseOption, closingPane).
|
|
673
|
+
handleCloseOption(selectedCloseOption, closingPane).then(() => {
|
|
674
|
+
// Close the dialog after the action is performed
|
|
675
|
+
setShowCloseOptions(false);
|
|
676
|
+
setClosingPane(null);
|
|
677
|
+
setSelectedCloseOption(0);
|
|
678
|
+
}).catch(error => {
|
|
1890
679
|
setStatusMessage('Failed to close pane');
|
|
1891
680
|
setTimeout(() => setStatusMessage(''), 2000);
|
|
681
|
+
// Also close the dialog on error
|
|
682
|
+
setShowCloseOptions(false);
|
|
683
|
+
setClosingPane(null);
|
|
684
|
+
setSelectedCloseOption(0);
|
|
1892
685
|
});
|
|
1893
686
|
}
|
|
1894
687
|
return;
|
|
@@ -1964,185 +757,55 @@ const DmuxApp = ({ dmuxDir, panesFile, projectName, sessionName, settingsFile, a
|
|
|
1964
757
|
React.createElement(Text, { bold: true, color: "cyan" },
|
|
1965
758
|
"dmux - ",
|
|
1966
759
|
projectName)),
|
|
1967
|
-
React.createElement(
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
}
|
|
1977
|
-
else if (
|
|
1978
|
-
|
|
760
|
+
React.createElement(PanesGrid, { panes: panes, selectedIndex: selectedIndex, isLoading: isLoading, showNewPaneDialog: showNewPaneDialog }),
|
|
761
|
+
isLoading && (React.createElement(LoadingIndicator, null)),
|
|
762
|
+
showNewPaneDialog && !showAgentChoiceDialog && (React.createElement(NewPaneDialog, { value: newPanePrompt, onChange: setNewPanePrompt, onSubmit: (value) => {
|
|
763
|
+
const promptValue = value;
|
|
764
|
+
const agents = availableAgents;
|
|
765
|
+
if (agents.length === 0) {
|
|
766
|
+
setShowNewPaneDialog(false);
|
|
767
|
+
setNewPanePrompt('');
|
|
768
|
+
createNewPaneHook(promptValue);
|
|
769
|
+
}
|
|
770
|
+
else if (agents.length === 1) {
|
|
771
|
+
setShowNewPaneDialog(false);
|
|
772
|
+
setNewPanePrompt('');
|
|
773
|
+
createNewPaneHook(promptValue, agents[0]);
|
|
1979
774
|
}
|
|
1980
|
-
else
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
(pane.testStatus || pane.devStatus) && (React.createElement(Box, null,
|
|
1997
|
-
pane.testStatus === 'running' && (React.createElement(Text, { color: "yellow" }, "\u23F3 Test")),
|
|
1998
|
-
pane.testStatus === 'passed' && (React.createElement(Text, { color: "green" }, "\u2713 Test")),
|
|
1999
|
-
pane.testStatus === 'failed' && (React.createElement(Text, { color: "red" }, "\u2717 Test")),
|
|
2000
|
-
pane.devStatus === 'running' && (React.createElement(Text, { color: "green" },
|
|
2001
|
-
"\u25B6 Dev",
|
|
2002
|
-
pane.devUrl && (React.createElement(Text, { color: "cyan", wrap: "truncate" },
|
|
2003
|
-
" ",
|
|
2004
|
-
pane.devUrl.replace(/https?:\/\//, '').substring(0, 15))))))))));
|
|
2005
|
-
}),
|
|
2006
|
-
!isLoading && !showNewPaneDialog && (React.createElement(Box, { paddingX: 1, borderStyle: "single", borderColor: selectedIndex === panes.length ? 'green' : 'gray', width: 35, flexShrink: 0 },
|
|
2007
|
-
React.createElement(Text, { color: selectedIndex === panes.length ? 'green' : 'white' }, "+ New dmux pane")))),
|
|
2008
|
-
isLoading && (React.createElement(Box, { borderStyle: "round", borderColor: "cyan", paddingX: 2, marginTop: 1 },
|
|
2009
|
-
React.createElement(Box, { flexDirection: "row", gap: 1 },
|
|
2010
|
-
React.createElement(Text, { color: "cyan" }, "\u23F3"),
|
|
2011
|
-
React.createElement(Text, null, "Loading dmux sessions...")))),
|
|
2012
|
-
showNewPaneDialog && !showAgentChoiceDialog && (React.createElement(Box, { flexDirection: "column", marginTop: 1 },
|
|
2013
|
-
React.createElement(Text, null, "Enter initial prompt (ESC to cancel):"),
|
|
2014
|
-
React.createElement(Box, { borderStyle: "round", borderColor: "#E67E22", paddingX: 1, marginTop: 1 },
|
|
2015
|
-
React.createElement(CleanTextInput, { value: newPanePrompt, onChange: setNewPanePrompt, onSubmit: (expandedValue) => {
|
|
2016
|
-
const promptValue = expandedValue || newPanePrompt;
|
|
2017
|
-
const agents = availableAgents;
|
|
2018
|
-
if (agents.length === 0) {
|
|
2019
|
-
setShowNewPaneDialog(false);
|
|
2020
|
-
setNewPanePrompt('');
|
|
2021
|
-
createNewPane(promptValue);
|
|
2022
|
-
}
|
|
2023
|
-
else if (agents.length === 1) {
|
|
2024
|
-
setShowNewPaneDialog(false);
|
|
2025
|
-
setNewPanePrompt('');
|
|
2026
|
-
createNewPane(promptValue, agents[0]);
|
|
2027
|
-
}
|
|
2028
|
-
else {
|
|
2029
|
-
setPendingPrompt(promptValue);
|
|
2030
|
-
setShowNewPaneDialog(false);
|
|
2031
|
-
setNewPanePrompt('');
|
|
2032
|
-
setShowAgentChoiceDialog(true);
|
|
2033
|
-
setAgentChoice(agentChoice || 'claude');
|
|
2034
|
-
}
|
|
2035
|
-
} })),
|
|
2036
|
-
React.createElement(Box, { marginTop: 1 },
|
|
2037
|
-
React.createElement(Text, { dimColor: true, italic: true }, "Press Ctrl+O to open in $EDITOR for complex multi-line input")))),
|
|
2038
|
-
showAgentChoiceDialog && (React.createElement(Box, { borderStyle: "round", borderColor: "cyan", paddingX: 1, marginTop: 1 },
|
|
2039
|
-
React.createElement(Box, { flexDirection: "column" },
|
|
2040
|
-
React.createElement(Text, null, "Select agent (\u2190/\u2192, 1/2, C/O, Enter, ESC):"),
|
|
2041
|
-
React.createElement(Box, { marginTop: 1, gap: 3 },
|
|
2042
|
-
React.createElement(Text, { color: agentChoice === 'claude' ? 'cyan' : 'white' }, agentChoice === 'claude' ? '▶ Claude Code' : ' Claude Code'),
|
|
2043
|
-
React.createElement(Text, { color: agentChoice === 'opencode' ? 'cyan' : 'white' }, agentChoice === 'opencode' ? '▶ opencode' : ' opencode'))))),
|
|
2044
|
-
isCreatingPane && (React.createElement(Box, { borderStyle: "single", borderColor: "yellow", paddingX: 1, marginTop: 1 },
|
|
2045
|
-
React.createElement(Text, { color: "yellow" },
|
|
2046
|
-
React.createElement(Text, { bold: true }, "\u23F3 Creating new pane... "),
|
|
2047
|
-
statusMessage))),
|
|
2048
|
-
showMergeConfirmation && mergedPane && (React.createElement(Box, { borderStyle: "double", borderColor: "yellow", paddingX: 1, marginTop: 1 },
|
|
2049
|
-
React.createElement(Box, { flexDirection: "column" },
|
|
2050
|
-
React.createElement(Text, { color: "yellow", bold: true }, "Worktree merged successfully!"),
|
|
2051
|
-
React.createElement(Text, null,
|
|
2052
|
-
"Close the pane \"",
|
|
2053
|
-
mergedPane.slug,
|
|
2054
|
-
"\"? (y/n)")))),
|
|
2055
|
-
showCloseOptions && closingPane && (React.createElement(Box, { borderStyle: "double", borderColor: "red", paddingX: 1, marginTop: 1 },
|
|
2056
|
-
React.createElement(Box, { flexDirection: "column" },
|
|
2057
|
-
React.createElement(Text, { color: "red", bold: true },
|
|
2058
|
-
"Close pane \"",
|
|
2059
|
-
closingPane.slug,
|
|
2060
|
-
"\"?"),
|
|
2061
|
-
React.createElement(Text, { dimColor: true }, "Select an option (ESC to cancel):"),
|
|
2062
|
-
React.createElement(Box, { flexDirection: "column", marginTop: 1 },
|
|
2063
|
-
React.createElement(Box, null,
|
|
2064
|
-
React.createElement(Text, { color: selectedCloseOption === 0 ? 'cyan' : 'white' },
|
|
2065
|
-
selectedCloseOption === 0 ? '▶ ' : ' ',
|
|
2066
|
-
"Merge & Prune - Merge worktree to main and close")),
|
|
2067
|
-
React.createElement(Box, null,
|
|
2068
|
-
React.createElement(Text, { color: selectedCloseOption === 1 ? 'cyan' : 'white' },
|
|
2069
|
-
selectedCloseOption === 1 ? '▶ ' : ' ',
|
|
2070
|
-
"Merge Only - Merge worktree but keep pane open")),
|
|
2071
|
-
React.createElement(Box, null,
|
|
2072
|
-
React.createElement(Text, { color: selectedCloseOption === 2 ? 'cyan' : 'white' },
|
|
2073
|
-
selectedCloseOption === 2 ? '▶ ' : ' ',
|
|
2074
|
-
"Delete Unsaved - Remove worktree (discard changes)")),
|
|
2075
|
-
React.createElement(Box, null,
|
|
2076
|
-
React.createElement(Text, { color: selectedCloseOption === 3 ? 'cyan' : 'white' },
|
|
2077
|
-
selectedCloseOption === 3 ? '▶ ' : ' ',
|
|
2078
|
-
"Just Close - Close pane only")))))),
|
|
2079
|
-
showCommandPrompt && (React.createElement(Box, { borderStyle: "round", borderColor: "gray", paddingX: 1, marginTop: 1 },
|
|
2080
|
-
React.createElement(Box, { flexDirection: "column" },
|
|
2081
|
-
React.createElement(Text, { color: "magenta", bold: true },
|
|
2082
|
-
"Configure ",
|
|
2083
|
-
showCommandPrompt === 'test' ? 'Test' : 'Dev',
|
|
2084
|
-
" Command"),
|
|
2085
|
-
React.createElement(Text, { dimColor: true },
|
|
2086
|
-
"Enter command to run ",
|
|
2087
|
-
showCommandPrompt === 'test' ? 'tests' : 'dev server',
|
|
2088
|
-
" in worktrees"),
|
|
2089
|
-
React.createElement(Text, { dimColor: true }, "(Press Enter with empty input for suggested command, ESC to cancel)"),
|
|
2090
|
-
React.createElement(Box, { marginTop: 1 },
|
|
2091
|
-
React.createElement(StyledTextInput, { value: commandInput, onChange: setCommandInput, placeholder: showCommandPrompt === 'test' ? 'e.g., npm test, pnpm test' : 'e.g., npm run dev, pnpm dev' }))))),
|
|
2092
|
-
showFileCopyPrompt && (React.createElement(Box, { borderStyle: "double", borderColor: "yellow", paddingX: 1, marginTop: 1 },
|
|
2093
|
-
React.createElement(Box, { flexDirection: "column" },
|
|
2094
|
-
React.createElement(Text, { color: "yellow", bold: true }, "First Run Setup"),
|
|
2095
|
-
React.createElement(Text, null, "Copy non-git files (like .env, configs) from main to worktree?"),
|
|
2096
|
-
React.createElement(Text, { dimColor: true }, "This includes files not tracked by git but excludes node_modules, dist, etc."),
|
|
2097
|
-
React.createElement(Box, { marginTop: 1 },
|
|
2098
|
-
React.createElement(Text, null, "(y/n):"))))),
|
|
2099
|
-
runningCommand && (React.createElement(Box, { borderStyle: "single", borderColor: "blue", paddingX: 1, marginTop: 1 },
|
|
2100
|
-
React.createElement(Text, { color: "blue" },
|
|
2101
|
-
React.createElement(Text, { bold: true }, "\u25B6 Running command...")))),
|
|
2102
|
-
isUpdating && (React.createElement(Box, { borderStyle: "single", borderColor: "yellow", paddingX: 1, marginTop: 1 },
|
|
2103
|
-
React.createElement(Text, { color: "yellow" },
|
|
2104
|
-
React.createElement(Text, { bold: true }, "\u2B07 Updating dmux...")))),
|
|
2105
|
-
showUpdateDialog && updateInfo && (React.createElement(Box, { borderStyle: "double", borderColor: "green", paddingX: 1, marginTop: 1 },
|
|
2106
|
-
React.createElement(Box, { flexDirection: "column" },
|
|
2107
|
-
React.createElement(Text, { color: "green", bold: true }, "\uD83C\uDF89 dmux Update Available!"),
|
|
2108
|
-
React.createElement(Text, null,
|
|
2109
|
-
"Current version: ",
|
|
2110
|
-
React.createElement(Text, { color: "cyan" }, updateInfo.currentVersion)),
|
|
2111
|
-
React.createElement(Text, null,
|
|
2112
|
-
"Latest version: ",
|
|
2113
|
-
React.createElement(Text, { color: "green" }, updateInfo.latestVersion)),
|
|
2114
|
-
updateInfo.installMethod === 'global' && updateInfo.packageManager && (React.createElement(Text, null,
|
|
2115
|
-
"Detected global install via: ",
|
|
2116
|
-
React.createElement(Text, { color: "yellow" }, updateInfo.packageManager))),
|
|
2117
|
-
React.createElement(Box, { marginTop: 1 }, updateInfo.installMethod === 'global' && updateInfo.packageManager ? (React.createElement(Text, null, "[U]pdate now \u2022 [S]kip this version \u2022 [L]ater")) : (React.createElement(Text, null,
|
|
2118
|
-
"Manual update required: ",
|
|
2119
|
-
React.createElement(Text, { color: "cyan" },
|
|
2120
|
-
updateInfo.packageManager || 'npm',
|
|
2121
|
-
" update -g dmux"),
|
|
2122
|
-
'\n',
|
|
2123
|
-
"[S]kip this version \u2022 [L]ater")))))),
|
|
775
|
+
else {
|
|
776
|
+
setPendingPrompt(promptValue);
|
|
777
|
+
setShowNewPaneDialog(false);
|
|
778
|
+
setNewPanePrompt('');
|
|
779
|
+
setShowAgentChoiceDialog(true);
|
|
780
|
+
setAgentChoice(agentChoice || 'claude');
|
|
781
|
+
}
|
|
782
|
+
} })),
|
|
783
|
+
showAgentChoiceDialog && (React.createElement(AgentChoiceDialog, { agentChoice: agentChoice })),
|
|
784
|
+
isCreatingPane && (React.createElement(CreatingIndicator, { message: statusMessage })),
|
|
785
|
+
showMergeConfirmation && mergedPane && (React.createElement(MergeConfirmationDialog, { pane: mergedPane })),
|
|
786
|
+
showCloseOptions && closingPane && (React.createElement(CloseOptionsDialog, { pane: closingPane, selectedIndex: selectedCloseOption })),
|
|
787
|
+
showCommandPrompt && (React.createElement(CommandPromptDialog, { type: showCommandPrompt, value: commandInput, onChange: setCommandInput })),
|
|
788
|
+
showFileCopyPrompt && (React.createElement(FileCopyPrompt, null)),
|
|
789
|
+
runningCommand && (React.createElement(RunningIndicator, null)),
|
|
790
|
+
isUpdating && (React.createElement(UpdatingIndicator, null)),
|
|
2124
791
|
statusMessage && (React.createElement(Box, { marginTop: 1 },
|
|
2125
792
|
React.createElement(Text, { color: "green" }, statusMessage))),
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
" rows | Selected: ",
|
|
2135
|
-
(() => {
|
|
2136
|
-
const pos = getCardGridPosition(selectedIndex);
|
|
2137
|
-
return ` row ${pos.row}, col ${pos.col}`;
|
|
2138
|
-
})(),
|
|
2139
|
-
" | Terminal: ",
|
|
2140
|
-
terminalWidth,
|
|
2141
|
-
"w")))),
|
|
793
|
+
React.createElement(FooterHelp, { show: !showNewPaneDialog && !showCommandPrompt, gridInfo: (() => {
|
|
794
|
+
if (!process.env.DEBUG_DMUX)
|
|
795
|
+
return undefined;
|
|
796
|
+
const cols = Math.max(1, Math.floor(terminalWidth / 37));
|
|
797
|
+
const rows = Math.ceil((panes.length + 1) / cols);
|
|
798
|
+
const pos = getCardGridPosition(selectedIndex);
|
|
799
|
+
return `Grid: ${cols} cols × ${rows} rows | Selected: row ${pos.row}, col ${pos.col} | Terminal: ${terminalWidth}w`;
|
|
800
|
+
})() }),
|
|
2142
801
|
React.createElement(Box, { marginTop: 1 },
|
|
2143
802
|
React.createElement(Text, { dimColor: true },
|
|
2144
803
|
"dmux v",
|
|
2145
|
-
packageJson.version
|
|
804
|
+
packageJson.version,
|
|
805
|
+
updateAvailable && updateInfo && (React.createElement(Text, { color: "yellow" },
|
|
806
|
+
" \u2022 New version ",
|
|
807
|
+
updateInfo.latestVersion,
|
|
808
|
+
" available! Run: npm i -g dmux@latest"))))));
|
|
2146
809
|
};
|
|
2147
810
|
export default DmuxApp;
|
|
2148
811
|
//# sourceMappingURL=DmuxApp.js.map
|