dmux 4.1.1 → 5.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 -6
- package/dist/DmuxApp.d.ts.map +1 -1
- package/dist/DmuxApp.js +119 -183
- package/dist/DmuxApp.js.map +1 -1
- package/dist/actions/implementations/closeAction.d.ts.map +1 -1
- package/dist/actions/implementations/closeAction.js +15 -40
- package/dist/actions/implementations/closeAction.js.map +1 -1
- package/dist/actions/types.d.ts +0 -1
- package/dist/actions/types.d.ts.map +1 -1
- package/dist/actions/types.js.map +1 -1
- package/dist/components/panes/PanesGrid.d.ts +2 -0
- package/dist/components/panes/PanesGrid.d.ts.map +1 -1
- package/dist/components/panes/PanesGrid.js +122 -42
- package/dist/components/panes/PanesGrid.js.map +1 -1
- package/dist/components/popups/agentChoicePopup.d.ts +1 -1
- package/dist/components/popups/agentChoicePopup.js +37 -23
- package/dist/components/popups/agentChoicePopup.js.map +1 -1
- package/dist/components/popups/newPanePopup.js +4 -3
- package/dist/components/popups/newPanePopup.js.map +1 -1
- package/dist/components/popups/shortcutsPopup.js +6 -4
- package/dist/components/popups/shortcutsPopup.js.map +1 -1
- package/dist/components/ui/FooterHelp.d.ts +0 -8
- package/dist/components/ui/FooterHelp.d.ts.map +1 -1
- package/dist/components/ui/FooterHelp.js +5 -24
- package/dist/components/ui/FooterHelp.js.map +1 -1
- package/dist/hooks/useActionSystem.d.ts +1 -2
- package/dist/hooks/useActionSystem.d.ts.map +1 -1
- package/dist/hooks/useActionSystem.js +9 -25
- package/dist/hooks/useActionSystem.js.map +1 -1
- package/dist/hooks/useAgentDetection.d.ts +2 -1
- package/dist/hooks/useAgentDetection.d.ts.map +1 -1
- package/dist/hooks/useAgentDetection.js.map +1 -1
- package/dist/hooks/useAgentStatus.d.ts.map +1 -1
- package/dist/hooks/useAgentStatus.js +18 -6
- package/dist/hooks/useAgentStatus.js.map +1 -1
- package/dist/hooks/useInputHandling.d.ts +6 -9
- package/dist/hooks/useInputHandling.d.ts.map +1 -1
- package/dist/hooks/useInputHandling.js +91 -69
- package/dist/hooks/useInputHandling.js.map +1 -1
- package/dist/hooks/useLayoutManagement.d.ts +1 -2
- package/dist/hooks/useLayoutManagement.d.ts.map +1 -1
- package/dist/hooks/useLayoutManagement.js +2 -9
- package/dist/hooks/useLayoutManagement.js.map +1 -1
- package/dist/hooks/useNavigation.d.ts +1 -1
- package/dist/hooks/useNavigation.d.ts.map +1 -1
- package/dist/hooks/useNavigation.js +29 -36
- package/dist/hooks/useNavigation.js.map +1 -1
- package/dist/hooks/usePaneCreation.d.ts +10 -4
- package/dist/hooks/usePaneCreation.d.ts.map +1 -1
- package/dist/hooks/usePaneCreation.js +20 -49
- package/dist/hooks/usePaneCreation.js.map +1 -1
- package/dist/hooks/usePaneLoading.d.ts.map +1 -1
- package/dist/hooks/usePaneLoading.js +21 -23
- package/dist/hooks/usePaneLoading.js.map +1 -1
- package/dist/hooks/usePaneRunner.d.ts +1 -1
- package/dist/hooks/usePaneRunner.d.ts.map +1 -1
- package/dist/hooks/usePaneRunner.js +5 -2
- package/dist/hooks/usePaneRunner.js.map +1 -1
- package/dist/hooks/usePaneSync.d.ts.map +1 -1
- package/dist/hooks/usePaneSync.js +29 -20
- package/dist/hooks/usePaneSync.js.map +1 -1
- package/dist/hooks/usePanes.d.ts.map +1 -1
- package/dist/hooks/usePanes.js +61 -65
- package/dist/hooks/usePanes.js.map +1 -1
- package/dist/hooks/useServices.d.ts +4 -7
- package/dist/hooks/useServices.d.ts.map +1 -1
- package/dist/hooks/useServices.js +0 -4
- package/dist/hooks/useServices.js.map +1 -1
- package/dist/hooks/useTerminalWidth.d.ts.map +1 -1
- package/dist/hooks/useTerminalWidth.js +0 -14
- package/dist/hooks/useTerminalWidth.js.map +1 -1
- package/dist/hooks/useWorktreeActions.d.ts +1 -2
- package/dist/hooks/useWorktreeActions.d.ts.map +1 -1
- package/dist/hooks/useWorktreeActions.js +2 -8
- package/dist/hooks/useWorktreeActions.js.map +1 -1
- package/dist/index.js +257 -51
- package/dist/index.js.map +1 -1
- package/dist/server/embedded-assets.d.ts.map +1 -1
- package/dist/server/embedded-assets.js +378 -236
- package/dist/server/embedded-assets.js.map +1 -1
- package/dist/server/routes/panesRoutes.d.ts.map +1 -1
- package/dist/server/routes/panesRoutes.js +16 -2
- package/dist/server/routes/panesRoutes.js.map +1 -1
- package/dist/services/PopupManager.d.ts +5 -7
- package/dist/services/PopupManager.d.ts.map +1 -1
- package/dist/services/PopupManager.js +8 -42
- package/dist/services/PopupManager.js.map +1 -1
- package/dist/types.d.ts +2 -5
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/agentLaunch.d.ts +12 -0
- package/dist/utils/agentLaunch.d.ts.map +1 -0
- package/dist/utils/agentLaunch.js +56 -0
- package/dist/utils/agentLaunch.js.map +1 -0
- package/dist/utils/paneCreation.d.ts +4 -0
- package/dist/utils/paneCreation.d.ts.map +1 -1
- package/dist/utils/paneCreation.js +33 -17
- package/dist/utils/paneCreation.js.map +1 -1
- package/dist/utils/paneGrouping.d.ts +15 -0
- package/dist/utils/paneGrouping.d.ts.map +1 -0
- package/dist/utils/paneGrouping.js +24 -0
- package/dist/utils/paneGrouping.js.map +1 -0
- package/dist/utils/paneProject.d.ts +16 -0
- package/dist/utils/paneProject.d.ts.map +1 -0
- package/dist/utils/paneProject.js +40 -0
- package/dist/utils/paneProject.js.map +1 -0
- package/dist/utils/paneRebinding.d.ts.map +1 -1
- package/dist/utils/paneRebinding.js +13 -7
- package/dist/utils/paneRebinding.js.map +1 -1
- package/dist/utils/paneTitle.d.ts +13 -0
- package/dist/utils/paneTitle.d.ts.map +1 -0
- package/dist/utils/paneTitle.js +48 -0
- package/dist/utils/paneTitle.js.map +1 -0
- package/dist/utils/projectActions.d.ts +32 -0
- package/dist/utils/projectActions.d.ts.map +1 -0
- package/dist/utils/projectActions.js +108 -0
- package/dist/utils/projectActions.js.map +1 -0
- package/dist/utils/projectRoot.d.ts +10 -0
- package/dist/utils/projectRoot.d.ts.map +1 -0
- package/dist/utils/projectRoot.js +66 -0
- package/dist/utils/projectRoot.js.map +1 -0
- package/dist/utils/reopenWorktree.d.ts +2 -0
- package/dist/utils/reopenWorktree.d.ts.map +1 -1
- package/dist/utils/reopenWorktree.js +14 -4
- package/dist/utils/reopenWorktree.js.map +1 -1
- package/dist/utils/shellPaneDetection.d.ts.map +1 -1
- package/dist/utils/shellPaneDetection.js +21 -0
- package/dist/utils/shellPaneDetection.js.map +1 -1
- package/dist/utils/tmux.d.ts.map +1 -1
- package/dist/utils/tmux.js +0 -8
- package/dist/utils/tmux.js.map +1 -1
- package/dist/workers/panePollingWorker.js +0 -6
- package/dist/workers/panePollingWorker.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
# dmux - AI-Powered tmux Development Sessions
|
|
6
6
|
|
|
7
|
-
Tools for running agents in parallel are too complex. `dmux` makes running parallel development agents like Claude Code or opencode very simple. It's a simple tool that creates a new tmux pane, a new git worktree, and launches your chosen agent
|
|
7
|
+
Tools for running agents in parallel are too complex. `dmux` makes running parallel development agents like Claude Code, Codex, or opencode very simple. It's a simple tool that creates a new tmux pane, a new git worktree, and launches your chosen agent in that worktree, with AI powered branch naming and commit messages.
|
|
8
8
|
|
|
9
9
|
`dmux` lets you merge the open panes back into your main branch easily, close failed experiments, and spin up more agents quickly.
|
|
10
10
|
|
|
@@ -15,7 +15,8 @@ Tools for running agents in parallel are too complex. `dmux` makes running paral
|
|
|
15
15
|
- **🚀 Parallel Development**: Work on multiple features simultaneously in separate panes
|
|
16
16
|
- **🌳 Git Worktree Integration**: Each pane operates in its own isolated git worktree
|
|
17
17
|
- **🤖 AI-Powered**: Automatic branch naming and commit message generation
|
|
18
|
-
- **🎯 Agent Integration**: Launch Claude Code or
|
|
18
|
+
- **🎯 Agent Integration**: Launch Claude Code, Codex, or OpenCode with prompts
|
|
19
|
+
- **🧪 A/B Agent Launches**: Start two agents at once from the same prompt for side-by-side experimentation
|
|
19
20
|
- **📦 Project Isolation**: Each project gets its own tmux session
|
|
20
21
|
- **🔄 Smart Merging**: One-command merge workflow with automatic cleanup
|
|
21
22
|
|
|
@@ -24,7 +25,7 @@ Tools for running agents in parallel are too complex. `dmux` makes running paral
|
|
|
24
25
|
- **tmux** 3.0 or higher
|
|
25
26
|
- **Node.js** 18 or higher
|
|
26
27
|
- **Git** 2.20 or higher (with worktree support)
|
|
27
|
-
- **Agent CLI**: Claude Code (`claude`) or
|
|
28
|
+
- **Agent CLI**: Claude Code (`claude`), Codex (`codex`), or OpenCode (`opencode`)
|
|
28
29
|
- **OpenRouter API Key** (optional but recommended for AI features)
|
|
29
30
|
|
|
30
31
|
## Installation
|
|
@@ -55,11 +56,13 @@ Get your API key from [OpenRouter](https://openrouter.ai/).
|
|
|
55
56
|
cd /path/to/your/project
|
|
56
57
|
dmux
|
|
57
58
|
```
|
|
59
|
+
If you run `dmux` from a different repo while already inside a `dmux-*` tmux session, dmux will prompt to add that repo into the current session.
|
|
58
60
|
|
|
59
61
|
2. **Create a new development pane**
|
|
60
62
|
- Press `n` or select "+ New dmux pane"
|
|
61
|
-
- Enter an optional prompt like "fix authentication bug"
|
|
62
|
-
-
|
|
63
|
+
- Enter an optional prompt like "fix authentication bug"
|
|
64
|
+
- Choose one agent, or an A/B pair (for example Claude Code + Codex)
|
|
65
|
+
- dmux launches one pane per selected agent, each with its own worktree
|
|
63
66
|
|
|
64
67
|
3. **Navigate between panes**
|
|
65
68
|
- Use `↑/↓` arrows to select panes
|
|
@@ -76,7 +79,10 @@ Get your API key from [OpenRouter](https://openrouter.ai/).
|
|
|
76
79
|
|-----|--------|
|
|
77
80
|
| `↑/↓` | Navigate pane list |
|
|
78
81
|
| `Enter` or `j` | Jump to selected pane |
|
|
79
|
-
| `n` | Create new dmux pane |
|
|
82
|
+
| `n` | Create new dmux pane (main project) |
|
|
83
|
+
| `t` | Create terminal pane (main project) |
|
|
84
|
+
| `p` | Create pane in another project |
|
|
85
|
+
| `N` | Create pane in another project (legacy) |
|
|
80
86
|
| `m` | Merge worktree to main |
|
|
81
87
|
| `x` | Close selected pane |
|
|
82
88
|
| `q` | Quit dmux interface |
|
package/dist/DmuxApp.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"DmuxApp.d.ts","sourceRoot":"","sources":["../src/DmuxApp.tsx"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"DmuxApp.d.ts","sourceRoot":"","sources":["../src/DmuxApp.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAuC,MAAM,OAAO,CAAA;AAiD3D,OAAO,KAAK,EAEV,YAAY,EACb,MAAM,YAAY,CAAA;AAenB,QAAA,MAAM,OAAO,EAAE,KAAK,CAAC,EAAE,CAAC,YAAY,CA41BnC,CAAA;AAED,eAAe,OAAO,CAAA"}
|
package/dist/DmuxApp.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import React, { useState, useEffect } from "react";
|
|
1
|
+
import React, { useState, useEffect, useMemo } from "react";
|
|
2
2
|
import { Box, Text, useApp, useStdout, useInput } from "ink";
|
|
3
|
-
import { createRequire } from "module";
|
|
4
3
|
import { TmuxService } from "./services/TmuxService.js";
|
|
5
4
|
// Hooks
|
|
6
5
|
import usePanes from "./hooks/usePanes.js";
|
|
@@ -17,13 +16,12 @@ import { useStatusMessages } from "./hooks/useStatusMessages.js";
|
|
|
17
16
|
import { useLayoutManagement } from "./hooks/useLayoutManagement.js";
|
|
18
17
|
import { useInputHandling } from "./hooks/useInputHandling.js";
|
|
19
18
|
import { useDialogState } from "./hooks/useDialogState.js";
|
|
20
|
-
import { useTunnelManagement } from "./hooks/useTunnelManagement.js";
|
|
21
19
|
import { useDebugInfo } from "./hooks/useDebugInfo.js";
|
|
22
20
|
// Utils
|
|
23
21
|
import { SIDEBAR_WIDTH } from "./utils/layoutManager.js";
|
|
24
22
|
import { supportsPopups } from "./utils/popup.js";
|
|
25
23
|
import { StateManager } from "./shared/StateManager.js";
|
|
26
|
-
import {
|
|
24
|
+
import { STATUS_MESSAGE_DURATION_SHORT, } from "./constants/timing.js";
|
|
27
25
|
import { getStatusDetector, } from "./services/StatusDetector.js";
|
|
28
26
|
import { SettingsManager } from "./utils/settingsManager.js";
|
|
29
27
|
import { useServices } from "./hooks/useServices.js";
|
|
@@ -31,10 +29,10 @@ import { PaneLifecycleManager } from "./services/PaneLifecycleManager.js";
|
|
|
31
29
|
import { reopenWorktree } from "./utils/reopenWorktree.js";
|
|
32
30
|
import { fileURLToPath } from "url";
|
|
33
31
|
import { dirname } from "path";
|
|
32
|
+
import { getAgentSlugSuffix, } from "./utils/agentLaunch.js";
|
|
33
|
+
import { generateSlug } from "./utils/slug.js";
|
|
34
34
|
const __filename = fileURLToPath(import.meta.url);
|
|
35
35
|
const __dirname = dirname(__filename);
|
|
36
|
-
const require = createRequire(import.meta.url);
|
|
37
|
-
const packageJson = require("../package.json");
|
|
38
36
|
import PanesGrid from "./components/panes/PanesGrid.js";
|
|
39
37
|
import CommandPromptDialog from "./components/dialogs/CommandPromptDialog.js";
|
|
40
38
|
import FileCopyPrompt from "./components/ui/FileCopyPrompt.js";
|
|
@@ -44,26 +42,20 @@ import UpdatingIndicator from "./components/indicators/UpdatingIndicator.js";
|
|
|
44
42
|
import FooterHelp from "./components/ui/FooterHelp.js";
|
|
45
43
|
import TmuxHooksPromptDialog from "./components/dialogs/TmuxHooksPromptDialog.js";
|
|
46
44
|
import { PaneEventService } from "./services/PaneEventService.js";
|
|
47
|
-
|
|
45
|
+
import { buildProjectActionLayout, buildVisualNavigationRows, } from "./utils/projectActions.js";
|
|
46
|
+
const DmuxApp = ({ panesFile, projectName, sessionName, settingsFile, projectRoot, autoUpdater, controlPaneId, }) => {
|
|
48
47
|
const { stdout } = useStdout();
|
|
49
48
|
const terminalHeight = stdout?.rows || 40;
|
|
50
49
|
/* panes state moved to usePanes */
|
|
51
50
|
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
52
|
-
const { statusMessage, setStatusMessage
|
|
51
|
+
const { statusMessage, setStatusMessage } = useStatusMessages();
|
|
53
52
|
const [isCreatingPane, setIsCreatingPane] = useState(false);
|
|
54
53
|
// Settings state
|
|
55
54
|
const [settingsManager] = useState(() => new SettingsManager(projectRoot));
|
|
56
|
-
// Force repaint trigger - incrementing this causes Ink to re-render
|
|
57
|
-
const [forceRepaintTrigger, setForceRepaintTrigger] = useState(0);
|
|
58
|
-
// Spinner state - shows for a few frames to force render
|
|
59
|
-
const [showRepaintSpinner, setShowRepaintSpinner] = useState(false);
|
|
60
55
|
const { projectSettings, saveSettings } = useProjectSettings(settingsFile);
|
|
61
56
|
// Dialog state management
|
|
62
57
|
const dialogState = useDialogState();
|
|
63
58
|
const { showCommandPrompt, setShowCommandPrompt, commandInput, setCommandInput, showFileCopyPrompt, setShowFileCopyPrompt, currentCommandType, setCurrentCommandType, runningCommand, setRunningCommand, quitConfirmMode, setQuitConfirmMode, } = dialogState;
|
|
64
|
-
// Tunnel/network state management
|
|
65
|
-
const tunnelState = useTunnelManagement();
|
|
66
|
-
const { tunnelUrl, setTunnelUrl, tunnelCreating, setTunnelCreating, tunnelCopied, setTunnelCopied, localIp, setLocalIp, } = tunnelState;
|
|
67
59
|
// Debug/development info
|
|
68
60
|
const { debugMessage, setDebugMessage, currentBranch } = useDebugInfo(__dirname);
|
|
69
61
|
// Update state handled by hook
|
|
@@ -95,11 +87,11 @@ const DmuxApp = ({ panesFile, projectName, sessionName, settingsFile, projectRoo
|
|
|
95
87
|
const stateManager = StateManager.getInstance();
|
|
96
88
|
const updateState = () => {
|
|
97
89
|
const state = stateManager.getState();
|
|
98
|
-
setUnreadErrorCount(state.unreadErrorCount);
|
|
99
|
-
setUnreadWarningCount(state.unreadWarningCount);
|
|
100
|
-
setCurrentToast(state.currentToast);
|
|
101
|
-
setToastQueueLength(state.toastQueueLength);
|
|
102
|
-
setToastQueuePosition(state.toastQueuePosition);
|
|
90
|
+
setUnreadErrorCount((prev) => prev === state.unreadErrorCount ? prev : state.unreadErrorCount);
|
|
91
|
+
setUnreadWarningCount((prev) => prev === state.unreadWarningCount ? prev : state.unreadWarningCount);
|
|
92
|
+
setCurrentToast((prev) => prev === state.currentToast ? prev : state.currentToast);
|
|
93
|
+
setToastQueueLength((prev) => prev === state.toastQueueLength ? prev : state.toastQueueLength);
|
|
94
|
+
setToastQueuePosition((prev) => prev === state.toastQueuePosition ? prev : state.toastQueuePosition);
|
|
103
95
|
};
|
|
104
96
|
// Initial state
|
|
105
97
|
updateState();
|
|
@@ -110,7 +102,7 @@ const DmuxApp = ({ panesFile, projectName, sessionName, settingsFile, projectRoo
|
|
|
110
102
|
};
|
|
111
103
|
}, []);
|
|
112
104
|
// Panes state and persistence (skipLoading will be updated after actionSystem is initialized)
|
|
113
|
-
const { panes, setPanes, isLoading, loadPanes, savePanes
|
|
105
|
+
const { panes, setPanes, isLoading, loadPanes, savePanes } = usePanes(panesFile, false, sessionName, controlPaneId, useHooks);
|
|
114
106
|
// Check for tmux hooks preference on startup
|
|
115
107
|
useEffect(() => {
|
|
116
108
|
const checkHooksPreference = async () => {
|
|
@@ -156,77 +148,18 @@ const DmuxApp = ({ panesFile, projectName, sessionName, settingsFile, projectRoo
|
|
|
156
148
|
setStatusMessage,
|
|
157
149
|
setRunningCommand,
|
|
158
150
|
});
|
|
159
|
-
// Force repaint helper - shows spinner for a few frames to force full re-render
|
|
160
|
-
const forceRepaint = () => {
|
|
161
|
-
setForceRepaintTrigger((prev) => prev + 1);
|
|
162
|
-
setShowRepaintSpinner(true);
|
|
163
|
-
// CRITICAL: Use Ink's official rerender method to force complete redraw
|
|
164
|
-
// When tmux clears the pane via selectLayout, Ink's output is lost
|
|
165
|
-
// Calling rerender forces Ink to redraw the entire component tree
|
|
166
|
-
if (rerenderRef?.current) {
|
|
167
|
-
rerenderRef.current(React.createElement(DmuxApp, {
|
|
168
|
-
panesFile,
|
|
169
|
-
projectName,
|
|
170
|
-
sessionName,
|
|
171
|
-
settingsFile,
|
|
172
|
-
projectRoot,
|
|
173
|
-
autoUpdater,
|
|
174
|
-
serverPort,
|
|
175
|
-
server,
|
|
176
|
-
controlPaneId,
|
|
177
|
-
rerenderRef,
|
|
178
|
-
}));
|
|
179
|
-
}
|
|
180
|
-
// Hide spinner after a few frames (enough to trigger multiple renders)
|
|
181
|
-
setTimeout(() => setShowRepaintSpinner(false), REPAINT_SPINNER_DURATION);
|
|
182
|
-
};
|
|
183
|
-
// Force repaint effect - ensures Ink re-renders when trigger changes
|
|
184
|
-
useEffect(() => {
|
|
185
|
-
if (forceRepaintTrigger > 0) {
|
|
186
|
-
// Small delay to ensure terminal is ready
|
|
187
|
-
const timer = setTimeout(async () => {
|
|
188
|
-
try {
|
|
189
|
-
const tmuxService = TmuxService.getInstance();
|
|
190
|
-
await tmuxService.refreshClient();
|
|
191
|
-
}
|
|
192
|
-
catch { }
|
|
193
|
-
}, 50);
|
|
194
|
-
return () => clearTimeout(timer);
|
|
195
|
-
}
|
|
196
|
-
}, [forceRepaintTrigger]);
|
|
197
|
-
// Get local network IP on mount
|
|
198
|
-
useEffect(() => {
|
|
199
|
-
const getLocalIp = async () => {
|
|
200
|
-
try {
|
|
201
|
-
// Get local IP address (not 127.0.0.1)
|
|
202
|
-
const { execSync } = await import("child_process");
|
|
203
|
-
const result = execSync(`hostname -I 2>/dev/null || ifconfig | grep "inet " | grep -v 127.0.0.1 | awk '{print $2}' | head -1`, {
|
|
204
|
-
encoding: "utf-8",
|
|
205
|
-
stdio: "pipe",
|
|
206
|
-
}).trim();
|
|
207
|
-
if (result) {
|
|
208
|
-
setLocalIp(result.split(" ")[0]); // Take first IP if multiple
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
catch {
|
|
212
|
-
// Fallback to 127.0.0.1
|
|
213
|
-
setLocalIp("127.0.0.1");
|
|
214
|
-
}
|
|
215
|
-
};
|
|
216
|
-
getLocalIp();
|
|
217
|
-
}, []);
|
|
218
151
|
// Spinner animation and branch detection now handled in hooks
|
|
219
152
|
// Pane creation
|
|
220
153
|
const { createNewPane: createNewPaneHook } = usePaneCreation({
|
|
221
154
|
panes,
|
|
222
155
|
savePanes,
|
|
223
156
|
projectName,
|
|
157
|
+
sessionProjectRoot: projectRoot || process.cwd(),
|
|
158
|
+
panesFile,
|
|
224
159
|
setIsCreatingPane,
|
|
225
160
|
setStatusMessage,
|
|
226
161
|
loadPanes,
|
|
227
|
-
panesFile,
|
|
228
162
|
availableAgents,
|
|
229
|
-
forceRepaint,
|
|
230
163
|
});
|
|
231
164
|
// Initialize services
|
|
232
165
|
const { popupManager } = useServices({
|
|
@@ -238,81 +171,82 @@ const DmuxApp = ({ panesFile, projectName, sessionName, settingsFile, projectRoo
|
|
|
238
171
|
terminalHeight,
|
|
239
172
|
availableAgents,
|
|
240
173
|
agentChoice,
|
|
241
|
-
serverPort,
|
|
242
|
-
server,
|
|
243
174
|
settingsManager,
|
|
244
175
|
projectSettings,
|
|
245
176
|
// Callbacks
|
|
246
177
|
setStatusMessage,
|
|
247
178
|
setIgnoreInput,
|
|
248
|
-
savePanes,
|
|
249
|
-
loadPanes,
|
|
250
179
|
});
|
|
251
180
|
// Listen for status updates with analysis data and merge into panes
|
|
252
181
|
useEffect(() => {
|
|
253
182
|
const statusDetector = getStatusDetector();
|
|
254
183
|
const handleStatusUpdate = (event) => {
|
|
255
184
|
setPanes((prevPanes) => {
|
|
256
|
-
const
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
return updated;
|
|
185
|
+
const paneIndex = prevPanes.findIndex((pane) => pane.id === event.paneId);
|
|
186
|
+
if (paneIndex === -1)
|
|
187
|
+
return prevPanes;
|
|
188
|
+
const pane = prevPanes[paneIndex];
|
|
189
|
+
const updated = {
|
|
190
|
+
...pane,
|
|
191
|
+
agentStatus: event.status,
|
|
192
|
+
};
|
|
193
|
+
// Only update analysis fields if they're present in the event (not undefined)
|
|
194
|
+
// This prevents simple status changes from overwriting PaneAnalyzer results
|
|
195
|
+
if (event.optionsQuestion !== undefined) {
|
|
196
|
+
updated.optionsQuestion = event.optionsQuestion;
|
|
197
|
+
}
|
|
198
|
+
if (event.options !== undefined) {
|
|
199
|
+
updated.options = event.options;
|
|
200
|
+
}
|
|
201
|
+
if (event.potentialHarm !== undefined) {
|
|
202
|
+
updated.potentialHarm = event.potentialHarm;
|
|
203
|
+
}
|
|
204
|
+
if (event.summary !== undefined) {
|
|
205
|
+
updated.agentSummary = event.summary;
|
|
206
|
+
}
|
|
207
|
+
if (event.analyzerError !== undefined) {
|
|
208
|
+
updated.analyzerError = event.analyzerError;
|
|
209
|
+
}
|
|
210
|
+
// Clear option dialog data when transitioning away from 'waiting' state
|
|
211
|
+
if (event.status !== "waiting" && pane.agentStatus === "waiting") {
|
|
212
|
+
updated.optionsQuestion = undefined;
|
|
213
|
+
updated.options = undefined;
|
|
214
|
+
updated.potentialHarm = undefined;
|
|
215
|
+
}
|
|
216
|
+
// Clear summary when transitioning away from 'idle' state
|
|
217
|
+
if (event.status !== "idle" && pane.agentStatus === "idle") {
|
|
218
|
+
updated.agentSummary = undefined;
|
|
219
|
+
}
|
|
220
|
+
// Clear analyzer error when successfully getting a new analysis
|
|
221
|
+
// or when transitioning to 'working' status
|
|
222
|
+
if (event.status === "working") {
|
|
223
|
+
updated.analyzerError = undefined;
|
|
224
|
+
}
|
|
225
|
+
else if (event.status === "waiting" || event.status === "idle") {
|
|
226
|
+
if (event.analyzerError === undefined &&
|
|
227
|
+
(event.optionsQuestion || event.summary)) {
|
|
228
|
+
updated.analyzerError = undefined;
|
|
301
229
|
}
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
230
|
+
}
|
|
231
|
+
const unchanged = pane.agentStatus === updated.agentStatus &&
|
|
232
|
+
pane.optionsQuestion === updated.optionsQuestion &&
|
|
233
|
+
pane.options === updated.options &&
|
|
234
|
+
pane.potentialHarm === updated.potentialHarm &&
|
|
235
|
+
pane.agentSummary === updated.agentSummary &&
|
|
236
|
+
pane.analyzerError === updated.analyzerError;
|
|
237
|
+
if (unchanged) {
|
|
238
|
+
return prevPanes;
|
|
239
|
+
}
|
|
240
|
+
const next = prevPanes.slice();
|
|
241
|
+
next[paneIndex] = updated;
|
|
242
|
+
return next;
|
|
309
243
|
});
|
|
310
244
|
};
|
|
311
245
|
statusDetector.on("status-updated", handleStatusUpdate);
|
|
312
246
|
return () => {
|
|
313
247
|
statusDetector.off("status-updated", handleStatusUpdate);
|
|
314
248
|
};
|
|
315
|
-
}, [setPanes
|
|
249
|
+
}, [setPanes]);
|
|
316
250
|
// Note: No need to sync panes with StateManager here.
|
|
317
251
|
// The ConfigWatcher automatically updates StateManager when the config file changes.
|
|
318
252
|
// This prevents unnecessary SSE broadcasts on every local state update.
|
|
@@ -360,63 +294,80 @@ const DmuxApp = ({ panesFile, projectName, sessionName, settingsFile, projectRoo
|
|
|
360
294
|
// No polling needed!
|
|
361
295
|
// loadPanes moved to usePanes
|
|
362
296
|
// getPanePositions moved to utils/tmux
|
|
297
|
+
const sessionProjectRoot = projectRoot || process.cwd();
|
|
298
|
+
const projectActionLayout = useMemo(() => buildProjectActionLayout(panes, sessionProjectRoot, projectName), [panes, sessionProjectRoot, projectName]);
|
|
299
|
+
const navigationRows = useMemo(() => isLoading
|
|
300
|
+
? projectActionLayout.groups.flatMap((group) => group.panes.map((entry) => [entry.index]))
|
|
301
|
+
: buildVisualNavigationRows(projectActionLayout), [isLoading, projectActionLayout]);
|
|
363
302
|
// Navigation logic moved to hook
|
|
364
|
-
const { getCardGridPosition, findCardInDirection } = useNavigation(
|
|
303
|
+
const { getCardGridPosition, findCardInDirection } = useNavigation(navigationRows);
|
|
365
304
|
// findCardInDirection provided by useNavigation
|
|
366
305
|
// savePanes moved to usePanes
|
|
367
306
|
// applySmartLayout moved to utils/tmux
|
|
368
307
|
// Helper function to handle agent choice and pane creation
|
|
369
|
-
const handlePaneCreationWithAgent = async (prompt) => {
|
|
308
|
+
const handlePaneCreationWithAgent = async (prompt, targetProjectRoot) => {
|
|
370
309
|
const agents = availableAgents;
|
|
310
|
+
const createPanesForAgents = async (selectedAgents) => {
|
|
311
|
+
const dedupedAgents = selectedAgents.filter((agent, index) => selectedAgents.indexOf(agent) === index);
|
|
312
|
+
let panesForCreation = panes;
|
|
313
|
+
const isMultiLaunch = dedupedAgents.length > 1;
|
|
314
|
+
const slugBase = isMultiLaunch ? await generateSlug(prompt) : undefined;
|
|
315
|
+
for (const selectedAgent of dedupedAgents) {
|
|
316
|
+
const pane = await createNewPaneHook(prompt, selectedAgent, {
|
|
317
|
+
existingPanes: panesForCreation,
|
|
318
|
+
slugSuffix: isMultiLaunch
|
|
319
|
+
? getAgentSlugSuffix(selectedAgent)
|
|
320
|
+
: undefined,
|
|
321
|
+
slugBase,
|
|
322
|
+
targetProjectRoot,
|
|
323
|
+
});
|
|
324
|
+
if (!pane) {
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
panesForCreation = [...panesForCreation, pane];
|
|
328
|
+
}
|
|
329
|
+
};
|
|
371
330
|
if (agents.length === 0) {
|
|
372
|
-
await createNewPaneHook(prompt);
|
|
331
|
+
await createNewPaneHook(prompt, undefined, { targetProjectRoot });
|
|
373
332
|
}
|
|
374
333
|
else if (agents.length === 1) {
|
|
375
|
-
await
|
|
334
|
+
await createPanesForAgents([agents[0]]);
|
|
376
335
|
}
|
|
377
336
|
else {
|
|
378
337
|
// Multiple agents available - check for default agent setting first
|
|
379
338
|
const settings = settingsManager.getSettings();
|
|
380
339
|
if (settings.defaultAgent && agents.includes(settings.defaultAgent)) {
|
|
381
|
-
await
|
|
340
|
+
await createPanesForAgents([settings.defaultAgent]);
|
|
382
341
|
}
|
|
383
342
|
else {
|
|
384
343
|
// Show agent choice popup
|
|
385
|
-
const
|
|
386
|
-
if (
|
|
387
|
-
await
|
|
344
|
+
const selectedAgents = await popupManager.launchAgentChoicePopup();
|
|
345
|
+
if (selectedAgents && selectedAgents.length > 0) {
|
|
346
|
+
await createPanesForAgents(selectedAgents);
|
|
388
347
|
}
|
|
389
348
|
}
|
|
390
349
|
}
|
|
391
350
|
};
|
|
392
351
|
// Helper function to reopen a closed worktree
|
|
393
|
-
const handleReopenWorktree = async (slug, worktreePath) => {
|
|
394
|
-
// Force repaint first
|
|
395
|
-
forceRepaint();
|
|
396
|
-
// Minimal clearing
|
|
397
|
-
process.stdout.write('\x1b[2J\x1b[H');
|
|
352
|
+
const handleReopenWorktree = async (slug, worktreePath, targetProjectRoot) => {
|
|
398
353
|
try {
|
|
399
354
|
setIsCreatingPane(true);
|
|
400
355
|
setStatusMessage(`Reopening ${slug}...`);
|
|
356
|
+
const reopenProjectRoot = targetProjectRoot || projectRoot || process.cwd();
|
|
401
357
|
const result = await reopenWorktree({
|
|
402
358
|
slug,
|
|
403
359
|
worktreePath,
|
|
404
|
-
projectRoot:
|
|
360
|
+
projectRoot: reopenProjectRoot,
|
|
361
|
+
sessionProjectRoot: projectRoot || process.cwd(),
|
|
362
|
+
sessionConfigPath: panesFile,
|
|
405
363
|
existingPanes: panes,
|
|
406
364
|
});
|
|
407
365
|
// Save the pane
|
|
408
366
|
const updatedPanes = [...panes, result.pane];
|
|
409
367
|
await savePanes(updatedPanes);
|
|
410
|
-
// Force repaint and refresh
|
|
411
|
-
forceRepaint();
|
|
412
|
-
process.stdout.write('\x1b[2J\x1b[3J\x1b[H');
|
|
413
|
-
const tmuxService = TmuxService.getInstance();
|
|
414
|
-
tmuxService.clearHistorySync();
|
|
415
|
-
tmuxService.refreshClientSync();
|
|
416
368
|
await loadPanes();
|
|
417
369
|
setStatusMessage(`Reopened ${slug}`);
|
|
418
370
|
setTimeout(() => setStatusMessage(""), STATUS_MESSAGE_DURATION_SHORT);
|
|
419
|
-
forceRepaint();
|
|
420
371
|
}
|
|
421
372
|
catch (error) {
|
|
422
373
|
setStatusMessage(`Failed to reopen: ${error.message}`);
|
|
@@ -481,9 +432,7 @@ const DmuxApp = ({ panesFile, projectName, sessionName, settingsFile, projectRoo
|
|
|
481
432
|
try {
|
|
482
433
|
TmuxService.getInstance().selectPane(targetPane.paneId);
|
|
483
434
|
}
|
|
484
|
-
catch
|
|
485
|
-
console.error('[onActionResult] Failed to navigate to pane:', error);
|
|
486
|
-
}
|
|
435
|
+
catch { }
|
|
487
436
|
}
|
|
488
437
|
}
|
|
489
438
|
// Show message if dismissable
|
|
@@ -515,7 +464,6 @@ const DmuxApp = ({ panesFile, projectName, sessionName, settingsFile, projectRoo
|
|
|
515
464
|
await lifecycleManager.completeClose(paneId);
|
|
516
465
|
},
|
|
517
466
|
onActionResult: handleActionResult,
|
|
518
|
-
forceRepaint,
|
|
519
467
|
popupLaunchers: popupsSupported
|
|
520
468
|
? {
|
|
521
469
|
launchConfirmPopup: popupManager.launchConfirmPopup.bind(popupManager),
|
|
@@ -538,7 +486,6 @@ const DmuxApp = ({ panesFile, projectName, sessionName, settingsFile, projectRoo
|
|
|
538
486
|
isCreatingPane ||
|
|
539
487
|
runningCommand ||
|
|
540
488
|
isUpdating,
|
|
541
|
-
onForceRepaint: forceRepaint,
|
|
542
489
|
});
|
|
543
490
|
// Monitor agent status across panes (returns a map of pane ID to status)
|
|
544
491
|
const agentStatuses = useAgentStatus({
|
|
@@ -645,23 +592,19 @@ const DmuxApp = ({ panesFile, projectName, sessionName, settingsFile, projectRoo
|
|
|
645
592
|
projectSettings,
|
|
646
593
|
saveSettings,
|
|
647
594
|
settingsManager,
|
|
648
|
-
tunnelUrl,
|
|
649
|
-
setTunnelUrl,
|
|
650
|
-
tunnelCreating,
|
|
651
|
-
setTunnelCreating,
|
|
652
|
-
setTunnelCopied,
|
|
653
595
|
popupManager,
|
|
654
596
|
actionSystem,
|
|
655
|
-
server,
|
|
656
597
|
controlPaneId,
|
|
657
598
|
setStatusMessage,
|
|
658
599
|
copyNonGitFiles,
|
|
659
600
|
runCommandInternal,
|
|
660
601
|
handlePaneCreationWithAgent,
|
|
661
602
|
handleReopenWorktree,
|
|
603
|
+
savePanes,
|
|
662
604
|
loadPanes,
|
|
663
605
|
cleanExit,
|
|
664
|
-
projectRoot:
|
|
606
|
+
projectRoot: sessionProjectRoot,
|
|
607
|
+
projectActionItems: projectActionLayout.actionItems,
|
|
665
608
|
findCardInDirection,
|
|
666
609
|
});
|
|
667
610
|
// Calculate available height for content (terminal height - footer lines - active status messages)
|
|
@@ -670,7 +613,6 @@ const DmuxApp = ({ panesFile, projectName, sessionName, settingsFile, projectRoo
|
|
|
670
613
|
// - Normal mode calculation:
|
|
671
614
|
// - Base: 4 lines (marginTop + logs divider + logs line + keyboard shortcuts)
|
|
672
615
|
// - Toast: +2 lines (toast message + marginBottom) if currentToast exists
|
|
673
|
-
// - Network section: +4 lines (divider, local IP, remote tunnel, divider) if serverPort exists
|
|
674
616
|
// - Debug info: +1 line if DEBUG_DMUX
|
|
675
617
|
// - Status line: +1 line if updateAvailable/currentBranch/debugMessage
|
|
676
618
|
// - Status messages: +1 line per active message
|
|
@@ -692,10 +634,6 @@ const DmuxApp = ({ panesFile, projectName, sessionName, settingsFile, projectRoo
|
|
|
692
634
|
const wrappedLines = Math.ceil(toastTextLength / availableWidth);
|
|
693
635
|
footerLines += wrappedLines + 1 + 1; // wrapped lines + header line + marginBottom
|
|
694
636
|
}
|
|
695
|
-
// Add network section (now 2 lines for local IP + remote tunnel, plus 2 dividers)
|
|
696
|
-
if (serverPort && serverPort > 0) {
|
|
697
|
-
footerLines += 4;
|
|
698
|
-
}
|
|
699
637
|
// Add debug info
|
|
700
638
|
if (process.env.DEBUG_DMUX) {
|
|
701
639
|
footerLines += 1;
|
|
@@ -714,10 +652,8 @@ const DmuxApp = ({ panesFile, projectName, sessionName, settingsFile, projectRoo
|
|
|
714
652
|
}
|
|
715
653
|
const contentHeight = Math.max(terminalHeight - footerLines, 10);
|
|
716
654
|
return (React.createElement(Box, { flexDirection: "column", height: terminalHeight },
|
|
717
|
-
showRepaintSpinner && (React.createElement(Box, { marginTop: -10, marginLeft: -100 },
|
|
718
|
-
React.createElement(Text, null, "\u27F3"))),
|
|
719
655
|
React.createElement(Box, { flexDirection: "column", height: contentHeight, overflow: "hidden" },
|
|
720
|
-
React.createElement(PanesGrid, { panes: panes, selectedIndex: selectedIndex, isLoading: isLoading, agentStatuses: agentStatuses }),
|
|
656
|
+
React.createElement(PanesGrid, { panes: panes, selectedIndex: selectedIndex, isLoading: isLoading, agentStatuses: agentStatuses, fallbackProjectRoot: projectRoot || process.cwd(), fallbackProjectName: projectName }),
|
|
721
657
|
isLoading && React.createElement(LoadingIndicator, null),
|
|
722
658
|
showCommandPrompt && (React.createElement(CommandPromptDialog, { type: showCommandPrompt, value: commandInput, onChange: setCommandInput })),
|
|
723
659
|
showFileCopyPrompt && React.createElement(FileCopyPrompt, null),
|
|
@@ -732,11 +668,11 @@ const DmuxApp = ({ panesFile, projectName, sessionName, settingsFile, projectRoo
|
|
|
732
668
|
: actionSystem.actionState.statusType === "success"
|
|
733
669
|
? "green"
|
|
734
670
|
: "cyan" }, actionSystem.actionState.statusMessage))),
|
|
735
|
-
React.createElement(FooterHelp, { show: !showCommandPrompt,
|
|
671
|
+
React.createElement(FooterHelp, { show: !showCommandPrompt, quitConfirmMode: quitConfirmMode, unreadErrorCount: unreadErrorCount, unreadWarningCount: unreadWarningCount, currentToast: currentToast, toastQueueLength: toastQueueLength, toastQueuePosition: toastQueuePosition, gridInfo: (() => {
|
|
736
672
|
if (!process.env.DEBUG_DMUX)
|
|
737
673
|
return undefined;
|
|
738
|
-
const
|
|
739
|
-
const
|
|
674
|
+
const rows = navigationRows.length;
|
|
675
|
+
const cols = Math.max(1, ...navigationRows.map((row) => row.length));
|
|
740
676
|
const pos = getCardGridPosition(selectedIndex);
|
|
741
677
|
return `Grid: ${cols} cols × ${rows} rows | Selected: row ${pos.row}, col ${pos.col} | Terminal: ${terminalWidth}w`;
|
|
742
678
|
})() }),
|