dmux 4.1.1 → 5.1.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.
Files changed (148) hide show
  1. package/README.md +17 -7
  2. package/dist/DmuxApp.d.ts.map +1 -1
  3. package/dist/DmuxApp.js +119 -183
  4. package/dist/DmuxApp.js.map +1 -1
  5. package/dist/actions/implementations/closeAction.d.ts.map +1 -1
  6. package/dist/actions/implementations/closeAction.js +15 -40
  7. package/dist/actions/implementations/closeAction.js.map +1 -1
  8. package/dist/actions/types.d.ts +0 -1
  9. package/dist/actions/types.d.ts.map +1 -1
  10. package/dist/actions/types.js.map +1 -1
  11. package/dist/components/panes/PanesGrid.d.ts +2 -0
  12. package/dist/components/panes/PanesGrid.d.ts.map +1 -1
  13. package/dist/components/panes/PanesGrid.js +122 -42
  14. package/dist/components/panes/PanesGrid.js.map +1 -1
  15. package/dist/components/popups/agentChoicePopup.d.ts +1 -1
  16. package/dist/components/popups/agentChoicePopup.js +37 -23
  17. package/dist/components/popups/agentChoicePopup.js.map +1 -1
  18. package/dist/components/popups/newPanePopup.js +4 -3
  19. package/dist/components/popups/newPanePopup.js.map +1 -1
  20. package/dist/components/popups/shortcutsPopup.js +14 -13
  21. package/dist/components/popups/shortcutsPopup.js.map +1 -1
  22. package/dist/components/ui/FooterHelp.d.ts +0 -8
  23. package/dist/components/ui/FooterHelp.d.ts.map +1 -1
  24. package/dist/components/ui/FooterHelp.js +5 -24
  25. package/dist/components/ui/FooterHelp.js.map +1 -1
  26. package/dist/hooks/useActionSystem.d.ts +1 -2
  27. package/dist/hooks/useActionSystem.d.ts.map +1 -1
  28. package/dist/hooks/useActionSystem.js +9 -25
  29. package/dist/hooks/useActionSystem.js.map +1 -1
  30. package/dist/hooks/useAgentDetection.d.ts +2 -1
  31. package/dist/hooks/useAgentDetection.d.ts.map +1 -1
  32. package/dist/hooks/useAgentDetection.js.map +1 -1
  33. package/dist/hooks/useAgentStatus.d.ts.map +1 -1
  34. package/dist/hooks/useAgentStatus.js +18 -6
  35. package/dist/hooks/useAgentStatus.js.map +1 -1
  36. package/dist/hooks/useInputHandling.d.ts +6 -9
  37. package/dist/hooks/useInputHandling.d.ts.map +1 -1
  38. package/dist/hooks/useInputHandling.js +115 -73
  39. package/dist/hooks/useInputHandling.js.map +1 -1
  40. package/dist/hooks/useLayoutManagement.d.ts +1 -2
  41. package/dist/hooks/useLayoutManagement.d.ts.map +1 -1
  42. package/dist/hooks/useLayoutManagement.js +2 -9
  43. package/dist/hooks/useLayoutManagement.js.map +1 -1
  44. package/dist/hooks/useNavigation.d.ts +1 -1
  45. package/dist/hooks/useNavigation.d.ts.map +1 -1
  46. package/dist/hooks/useNavigation.js +29 -36
  47. package/dist/hooks/useNavigation.js.map +1 -1
  48. package/dist/hooks/usePaneCreation.d.ts +10 -4
  49. package/dist/hooks/usePaneCreation.d.ts.map +1 -1
  50. package/dist/hooks/usePaneCreation.js +20 -49
  51. package/dist/hooks/usePaneCreation.js.map +1 -1
  52. package/dist/hooks/usePaneLoading.d.ts.map +1 -1
  53. package/dist/hooks/usePaneLoading.js +21 -23
  54. package/dist/hooks/usePaneLoading.js.map +1 -1
  55. package/dist/hooks/usePaneRunner.d.ts +1 -1
  56. package/dist/hooks/usePaneRunner.d.ts.map +1 -1
  57. package/dist/hooks/usePaneRunner.js +5 -2
  58. package/dist/hooks/usePaneRunner.js.map +1 -1
  59. package/dist/hooks/usePaneSync.d.ts.map +1 -1
  60. package/dist/hooks/usePaneSync.js +29 -20
  61. package/dist/hooks/usePaneSync.js.map +1 -1
  62. package/dist/hooks/usePanes.d.ts.map +1 -1
  63. package/dist/hooks/usePanes.js +61 -65
  64. package/dist/hooks/usePanes.js.map +1 -1
  65. package/dist/hooks/useServices.d.ts +4 -7
  66. package/dist/hooks/useServices.d.ts.map +1 -1
  67. package/dist/hooks/useServices.js +0 -4
  68. package/dist/hooks/useServices.js.map +1 -1
  69. package/dist/hooks/useTerminalWidth.d.ts.map +1 -1
  70. package/dist/hooks/useTerminalWidth.js +0 -14
  71. package/dist/hooks/useTerminalWidth.js.map +1 -1
  72. package/dist/hooks/useWorktreeActions.d.ts +1 -2
  73. package/dist/hooks/useWorktreeActions.d.ts.map +1 -1
  74. package/dist/hooks/useWorktreeActions.js +2 -8
  75. package/dist/hooks/useWorktreeActions.js.map +1 -1
  76. package/dist/index.js +260 -54
  77. package/dist/index.js.map +1 -1
  78. package/dist/server/embedded-assets.d.ts.map +1 -1
  79. package/dist/server/embedded-assets.js +381 -239
  80. package/dist/server/embedded-assets.js.map +1 -1
  81. package/dist/server/routes/panesRoutes.d.ts.map +1 -1
  82. package/dist/server/routes/panesRoutes.js +16 -2
  83. package/dist/server/routes/panesRoutes.js.map +1 -1
  84. package/dist/services/PopupManager.d.ts +6 -8
  85. package/dist/services/PopupManager.d.ts.map +1 -1
  86. package/dist/services/PopupManager.js +15 -45
  87. package/dist/services/PopupManager.js.map +1 -1
  88. package/dist/types.d.ts +2 -5
  89. package/dist/types.d.ts.map +1 -1
  90. package/dist/utils/agentLaunch.d.ts +12 -0
  91. package/dist/utils/agentLaunch.d.ts.map +1 -0
  92. package/dist/utils/agentLaunch.js +56 -0
  93. package/dist/utils/agentLaunch.js.map +1 -0
  94. package/dist/utils/generated-agents-doc.d.ts +1 -1
  95. package/dist/utils/generated-agents-doc.js +1 -1
  96. package/dist/utils/hooks.d.ts.map +1 -1
  97. package/dist/utils/hooks.js +50 -18
  98. package/dist/utils/hooks.js.map +1 -1
  99. package/dist/utils/hooksDocs.d.ts +1 -1
  100. package/dist/utils/onboarding.d.ts +9 -0
  101. package/dist/utils/onboarding.d.ts.map +1 -0
  102. package/dist/utils/onboarding.js +110 -0
  103. package/dist/utils/onboarding.js.map +1 -0
  104. package/dist/utils/openRouterApiKeySetup.d.ts +28 -0
  105. package/dist/utils/openRouterApiKeySetup.d.ts.map +1 -0
  106. package/dist/utils/openRouterApiKeySetup.js +143 -0
  107. package/dist/utils/openRouterApiKeySetup.js.map +1 -0
  108. package/dist/utils/paneCreation.d.ts +4 -0
  109. package/dist/utils/paneCreation.d.ts.map +1 -1
  110. package/dist/utils/paneCreation.js +45 -38
  111. package/dist/utils/paneCreation.js.map +1 -1
  112. package/dist/utils/paneGrouping.d.ts +15 -0
  113. package/dist/utils/paneGrouping.d.ts.map +1 -0
  114. package/dist/utils/paneGrouping.js +24 -0
  115. package/dist/utils/paneGrouping.js.map +1 -0
  116. package/dist/utils/paneProject.d.ts +16 -0
  117. package/dist/utils/paneProject.d.ts.map +1 -0
  118. package/dist/utils/paneProject.js +40 -0
  119. package/dist/utils/paneProject.js.map +1 -0
  120. package/dist/utils/paneRebinding.d.ts.map +1 -1
  121. package/dist/utils/paneRebinding.js +13 -7
  122. package/dist/utils/paneRebinding.js.map +1 -1
  123. package/dist/utils/paneTitle.d.ts +13 -0
  124. package/dist/utils/paneTitle.d.ts.map +1 -0
  125. package/dist/utils/paneTitle.js +48 -0
  126. package/dist/utils/paneTitle.js.map +1 -0
  127. package/dist/utils/projectActions.d.ts +32 -0
  128. package/dist/utils/projectActions.d.ts.map +1 -0
  129. package/dist/utils/projectActions.js +108 -0
  130. package/dist/utils/projectActions.js.map +1 -0
  131. package/dist/utils/projectRoot.d.ts +10 -0
  132. package/dist/utils/projectRoot.d.ts.map +1 -0
  133. package/dist/utils/projectRoot.js +66 -0
  134. package/dist/utils/projectRoot.js.map +1 -0
  135. package/dist/utils/reopenWorktree.d.ts +2 -0
  136. package/dist/utils/reopenWorktree.d.ts.map +1 -1
  137. package/dist/utils/reopenWorktree.js +15 -5
  138. package/dist/utils/reopenWorktree.js.map +1 -1
  139. package/dist/utils/shellPaneDetection.d.ts.map +1 -1
  140. package/dist/utils/shellPaneDetection.js +21 -0
  141. package/dist/utils/shellPaneDetection.js.map +1 -1
  142. package/dist/utils/tmux.d.ts +2 -1
  143. package/dist/utils/tmux.d.ts.map +1 -1
  144. package/dist/utils/tmux.js +3 -9
  145. package/dist/utils/tmux.js.map +1 -1
  146. package/dist/workers/panePollingWorker.js +0 -6
  147. package/dist/workers/panePollingWorker.js.map +1 -1
  148. 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 (Claude Code or opencode) in that worktree, with AI powered branch naming and commit messages.
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 opencode with prompts (Claude auto-accepts edits)
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 opencode (`opencode`)
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
@@ -37,7 +38,11 @@ npm install -g dmux
37
38
 
38
39
  ### 2. Enable AI Features
39
40
 
40
- For AI-powered branch naming and commit messages:
41
+ On first run, dmux onboarding can:
42
+ - suggest/install a recommended tmux config preset if you do not have one
43
+ - prompt for your OpenRouter API key and save it to your shell config automatically
44
+
45
+ You can also set it manually for AI-powered branch naming and commit messages:
41
46
 
42
47
  ```bash
43
48
  # Add to your ~/.bashrc or ~/.zshrc
@@ -55,11 +60,13 @@ Get your API key from [OpenRouter](https://openrouter.ai/).
55
60
  cd /path/to/your/project
56
61
  dmux
57
62
  ```
63
+ 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
64
 
59
65
  2. **Create a new development pane**
60
66
  - Press `n` or select "+ New dmux pane"
61
- - Enter an optional prompt like "fix authentication bug"
62
- - Your selected agent launches in a new pane with your prompt
67
+ - Enter an optional prompt like "fix authentication bug"
68
+ - Choose one agent, or an A/B pair (for example Claude Code + Codex)
69
+ - dmux launches one pane per selected agent, each with its own worktree
63
70
 
64
71
  3. **Navigate between panes**
65
72
  - Use `↑/↓` arrows to select panes
@@ -76,7 +83,10 @@ Get your API key from [OpenRouter](https://openrouter.ai/).
76
83
  |-----|--------|
77
84
  | `↑/↓` | Navigate pane list |
78
85
  | `Enter` or `j` | Jump to selected pane |
79
- | `n` | Create new dmux pane |
86
+ | `n` | Create new dmux pane (main project) |
87
+ | `t` | Create terminal pane (main project) |
88
+ | `p` | Create pane in another project |
89
+ | `N` | Create pane in another project (legacy) |
80
90
  | `m` | Merge worktree to main |
81
91
  | `x` | Close selected pane |
82
92
  | `q` | Quit dmux interface |
@@ -1 +1 @@
1
- {"version":3,"file":"DmuxApp.d.ts","sourceRoot":"","sources":["../src/DmuxApp.tsx"],"names":[],"mappings":"AAAA,OAAO,KAA8B,MAAM,OAAO,CAAA;AAmDlD,OAAO,KAAK,EAEV,YAAY,EACb,MAAM,YAAY,CAAA;AAWnB,QAAA,MAAM,OAAO,EAAE,KAAK,CAAC,EAAE,CAAC,YAAY,CAy6BnC,CAAA;AAED,eAAe,OAAO,CAAA"}
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 { REPAINT_SPINNER_DURATION, STATUS_MESSAGE_DURATION_SHORT, } from "./constants/timing.js";
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
- const DmuxApp = ({ panesFile, projectName, sessionName, settingsFile, projectRoot, autoUpdater, serverPort, server, controlPaneId, rerenderRef, }) => {
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, showStatus, clearStatus } = useStatusMessages();
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, eventMode } = usePanes(panesFile, false, sessionName, controlPaneId, useHooks);
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 updatedPanes = prevPanes.map((pane) => {
257
- if (pane.id === event.paneId) {
258
- const updated = {
259
- ...pane,
260
- agentStatus: event.status,
261
- };
262
- // Only update analysis fields if they're present in the event (not undefined)
263
- // This prevents simple status changes from overwriting PaneAnalyzer results
264
- if (event.optionsQuestion !== undefined) {
265
- updated.optionsQuestion = event.optionsQuestion;
266
- }
267
- if (event.options !== undefined) {
268
- updated.options = event.options;
269
- }
270
- if (event.potentialHarm !== undefined) {
271
- updated.potentialHarm = event.potentialHarm;
272
- }
273
- if (event.summary !== undefined) {
274
- updated.agentSummary = event.summary;
275
- }
276
- if (event.analyzerError !== undefined) {
277
- updated.analyzerError = event.analyzerError;
278
- }
279
- // Clear option dialog data when transitioning away from 'waiting' state
280
- if (event.status !== "waiting" && pane.agentStatus === "waiting") {
281
- updated.optionsQuestion = undefined;
282
- updated.options = undefined;
283
- updated.potentialHarm = undefined;
284
- }
285
- // Clear summary when transitioning away from 'idle' state
286
- if (event.status !== "idle" && pane.agentStatus === "idle") {
287
- updated.agentSummary = undefined;
288
- }
289
- // Clear analyzer error when successfully getting a new analysis
290
- // or when transitioning to 'working' status
291
- if (event.status === "working") {
292
- updated.analyzerError = undefined;
293
- }
294
- else if (event.status === "waiting" || event.status === "idle") {
295
- if (event.analyzerError === undefined &&
296
- (event.optionsQuestion || event.summary)) {
297
- updated.analyzerError = undefined;
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
- return pane;
303
- });
304
- // Persist to disk - ConfigWatcher will handle syncing to StateManager
305
- savePanes(updatedPanes).catch((err) => {
306
- console.error("Failed to save panes after status update:", err);
307
- });
308
- return updatedPanes;
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, savePanes]);
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(terminalWidth, panes.length, isLoading);
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 createNewPaneHook(prompt, agents[0]);
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 createNewPaneHook(prompt, settings.defaultAgent);
340
+ await createPanesForAgents([settings.defaultAgent]);
382
341
  }
383
342
  else {
384
343
  // Show agent choice popup
385
- const selectedAgent = await popupManager.launchAgentChoicePopup();
386
- if (selectedAgent) {
387
- await createNewPaneHook(prompt, selectedAgent);
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: projectRoot || process.cwd(),
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 (error) {
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: projectRoot || process.cwd(),
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, showRemoteKey: !!server, quitConfirmMode: quitConfirmMode, hasSidebarLayout: !!controlPaneId, serverPort: serverPort, unreadErrorCount: unreadErrorCount, unreadWarningCount: unreadWarningCount, currentToast: currentToast, toastQueueLength: toastQueueLength, toastQueuePosition: toastQueuePosition, localIp: localIp, tunnelUrl: tunnelUrl, tunnelCreating: tunnelCreating, tunnelCopied: tunnelCopied, tunnelSpinner: tunnelState.getSpinnerChar(), gridInfo: (() => {
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 cols = Math.max(1, Math.floor(terminalWidth / 37));
739
- const rows = Math.ceil((panes.length + 1) / cols);
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
  })() }),