dmux 3.1.1 → 3.2.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 (232) hide show
  1. package/dist/CleanTextInput.d.ts +9 -0
  2. package/dist/CleanTextInput.d.ts.map +1 -1
  3. package/dist/CleanTextInput.js +127 -47
  4. package/dist/CleanTextInput.js.map +1 -1
  5. package/dist/DmuxApp.d.ts +2 -2
  6. package/dist/DmuxApp.d.ts.map +1 -1
  7. package/dist/DmuxApp.js +1396 -750
  8. package/dist/DmuxApp.js.map +1 -1
  9. package/dist/actions/paneActions.d.ts.map +1 -1
  10. package/dist/actions/paneActions.js +68 -11
  11. package/dist/actions/paneActions.js.map +1 -1
  12. package/dist/actions/types.d.ts +1 -0
  13. package/dist/actions/types.d.ts.map +1 -1
  14. package/dist/actions/types.js +1 -0
  15. package/dist/actions/types.js.map +1 -1
  16. package/dist/components/ActionInputDialog.d.ts.map +1 -1
  17. package/dist/components/ActionInputDialog.js +3 -2
  18. package/dist/components/ActionInputDialog.js.map +1 -1
  19. package/dist/components/DialogBox.d.ts +16 -0
  20. package/dist/components/DialogBox.d.ts.map +1 -0
  21. package/dist/components/DialogBox.js +12 -0
  22. package/dist/components/DialogBox.js.map +1 -0
  23. package/dist/components/FooterHelp.d.ts +9 -0
  24. package/dist/components/FooterHelp.d.ts.map +1 -1
  25. package/dist/components/FooterHelp.js +41 -5
  26. package/dist/components/FooterHelp.js.map +1 -1
  27. package/dist/components/PaneCard.d.ts +3 -1
  28. package/dist/components/PaneCard.d.ts.map +1 -1
  29. package/dist/components/PaneCard.js +50 -33
  30. package/dist/components/PaneCard.js.map +1 -1
  31. package/dist/components/PanesGrid.d.ts +3 -5
  32. package/dist/components/PanesGrid.d.ts.map +1 -1
  33. package/dist/components/PanesGrid.js +40 -10
  34. package/dist/components/PanesGrid.js.map +1 -1
  35. package/dist/dashboard.js +1 -1
  36. package/dist/decorative-pane.d.ts +3 -0
  37. package/dist/decorative-pane.d.ts.map +1 -0
  38. package/dist/decorative-pane.js +136 -0
  39. package/dist/decorative-pane.js.map +1 -0
  40. package/dist/hooks/useActionSystem.d.ts +14 -1
  41. package/dist/hooks/useActionSystem.d.ts.map +1 -1
  42. package/dist/hooks/useActionSystem.js +93 -4
  43. package/dist/hooks/useActionSystem.js.map +1 -1
  44. package/dist/hooks/useNavigation.js +1 -1
  45. package/dist/hooks/useNavigation.js.map +1 -1
  46. package/dist/hooks/usePaneCreation.d.ts +1 -2
  47. package/dist/hooks/usePaneCreation.d.ts.map +1 -1
  48. package/dist/hooks/usePaneCreation.js +13 -27
  49. package/dist/hooks/usePaneCreation.js.map +1 -1
  50. package/dist/hooks/usePaneRunner.d.ts.map +1 -1
  51. package/dist/hooks/usePaneRunner.js +8 -3
  52. package/dist/hooks/usePaneRunner.js.map +1 -1
  53. package/dist/hooks/usePanes.d.ts.map +1 -1
  54. package/dist/hooks/usePanes.js +210 -37
  55. package/dist/hooks/usePanes.js.map +1 -1
  56. package/dist/hooks/useWorktreeActions.d.ts.map +1 -1
  57. package/dist/hooks/useWorktreeActions.js +7 -13
  58. package/dist/hooks/useWorktreeActions.js.map +1 -1
  59. package/dist/index.js +217 -29
  60. package/dist/index.js.map +1 -1
  61. package/dist/popups/agentChoicePopup.d.ts +7 -0
  62. package/dist/popups/agentChoicePopup.d.ts.map +1 -0
  63. package/dist/popups/agentChoicePopup.js +74 -0
  64. package/dist/popups/agentChoicePopup.js.map +1 -0
  65. package/dist/popups/choicePopup.d.ts +7 -0
  66. package/dist/popups/choicePopup.d.ts.map +1 -0
  67. package/dist/popups/choicePopup.js +64 -0
  68. package/dist/popups/choicePopup.js.map +1 -0
  69. package/dist/popups/components/FileList.d.ts +13 -0
  70. package/dist/popups/components/FileList.d.ts.map +1 -0
  71. package/dist/popups/components/FileList.js +61 -0
  72. package/dist/popups/components/FileList.js.map +1 -0
  73. package/dist/popups/components/PopupContainer.d.ts +14 -0
  74. package/dist/popups/components/PopupContainer.d.ts.map +1 -0
  75. package/dist/popups/components/PopupContainer.js +15 -0
  76. package/dist/popups/components/PopupContainer.js.map +1 -0
  77. package/dist/popups/components/PopupInputBox.d.ts +11 -0
  78. package/dist/popups/components/PopupInputBox.d.ts.map +1 -0
  79. package/dist/popups/components/PopupInputBox.js +10 -0
  80. package/dist/popups/components/PopupInputBox.js.map +1 -0
  81. package/dist/popups/components/PopupWrapper.d.ts +37 -0
  82. package/dist/popups/components/PopupWrapper.d.ts.map +1 -0
  83. package/dist/popups/components/PopupWrapper.js +88 -0
  84. package/dist/popups/components/PopupWrapper.js.map +1 -0
  85. package/dist/popups/components/index.d.ts +8 -0
  86. package/dist/popups/components/index.d.ts.map +1 -0
  87. package/dist/popups/components/index.js +8 -0
  88. package/dist/popups/components/index.js.map +1 -0
  89. package/dist/popups/config.d.ts +40 -0
  90. package/dist/popups/config.d.ts.map +1 -0
  91. package/dist/popups/config.js +40 -0
  92. package/dist/popups/config.js.map +1 -0
  93. package/dist/popups/confirmPopup.d.ts +7 -0
  94. package/dist/popups/confirmPopup.d.ts.map +1 -0
  95. package/dist/popups/confirmPopup.js +72 -0
  96. package/dist/popups/confirmPopup.js.map +1 -0
  97. package/dist/popups/hooksPopup.d.ts +7 -0
  98. package/dist/popups/hooksPopup.d.ts.map +1 -0
  99. package/dist/popups/hooksPopup.js +71 -0
  100. package/dist/popups/hooksPopup.js.map +1 -0
  101. package/dist/popups/inputPopup.d.ts +7 -0
  102. package/dist/popups/inputPopup.d.ts.map +1 -0
  103. package/dist/popups/inputPopup.js +48 -0
  104. package/dist/popups/inputPopup.js.map +1 -0
  105. package/dist/popups/kebabMenuPopup.d.ts +7 -0
  106. package/dist/popups/kebabMenuPopup.d.ts.map +1 -0
  107. package/dist/popups/kebabMenuPopup.js +52 -0
  108. package/dist/popups/kebabMenuPopup.js.map +1 -0
  109. package/dist/popups/logsPopup.d.ts +12 -0
  110. package/dist/popups/logsPopup.d.ts.map +1 -0
  111. package/dist/popups/logsPopup.js +364 -0
  112. package/dist/popups/logsPopup.js.map +1 -0
  113. package/dist/popups/mergePopup.d.ts +7 -0
  114. package/dist/popups/mergePopup.d.ts.map +1 -0
  115. package/dist/popups/mergePopup.js +310 -0
  116. package/dist/popups/mergePopup.js.map +1 -0
  117. package/dist/popups/newPanePopup.d.ts +7 -0
  118. package/dist/popups/newPanePopup.d.ts.map +1 -0
  119. package/dist/popups/newPanePopup.js +234 -0
  120. package/dist/popups/newPanePopup.js.map +1 -0
  121. package/dist/popups/progressPopup.d.ts +8 -0
  122. package/dist/popups/progressPopup.d.ts.map +1 -0
  123. package/dist/popups/progressPopup.js +54 -0
  124. package/dist/popups/progressPopup.js.map +1 -0
  125. package/dist/popups/remotePopup.d.ts +6 -0
  126. package/dist/popups/remotePopup.d.ts.map +1 -0
  127. package/dist/popups/remotePopup.js +166 -0
  128. package/dist/popups/remotePopup.js.map +1 -0
  129. package/dist/popups/settingsPopup.d.ts +7 -0
  130. package/dist/popups/settingsPopup.d.ts.map +1 -0
  131. package/dist/popups/settingsPopup.js +212 -0
  132. package/dist/popups/settingsPopup.js.map +1 -0
  133. package/dist/popups/shortcutsPopup.d.ts +6 -0
  134. package/dist/popups/shortcutsPopup.d.ts.map +1 -0
  135. package/dist/popups/shortcutsPopup.js +74 -0
  136. package/dist/popups/shortcutsPopup.js.map +1 -0
  137. package/dist/popups/templates/SimpleInputPopup.d.ts +15 -0
  138. package/dist/popups/templates/SimpleInputPopup.d.ts.map +1 -0
  139. package/dist/popups/templates/SimpleInputPopup.js +28 -0
  140. package/dist/popups/templates/SimpleInputPopup.js.map +1 -0
  141. package/dist/server/embedded-assets.d.ts.map +1 -1
  142. package/dist/server/embedded-assets.js +2066 -968
  143. package/dist/server/embedded-assets.js.map +1 -1
  144. package/dist/server/index.js +1 -1
  145. package/dist/server/index.js.map +1 -1
  146. package/dist/server/routes.d.ts +1 -1
  147. package/dist/server/routes.d.ts.map +1 -1
  148. package/dist/server/routes.js +73 -6
  149. package/dist/server/routes.js.map +1 -1
  150. package/dist/services/ConfigWatcher.d.ts.map +1 -1
  151. package/dist/services/ConfigWatcher.js +10 -3
  152. package/dist/services/ConfigWatcher.js.map +1 -1
  153. package/dist/services/LogService.d.ts +112 -0
  154. package/dist/services/LogService.d.ts.map +1 -0
  155. package/dist/services/LogService.js +252 -0
  156. package/dist/services/LogService.js.map +1 -0
  157. package/dist/services/PaneWorkerManager.d.ts.map +1 -1
  158. package/dist/services/PaneWorkerManager.js +35 -9
  159. package/dist/services/PaneWorkerManager.js.map +1 -1
  160. package/dist/services/TunnelService.d.ts +1 -0
  161. package/dist/services/TunnelService.d.ts.map +1 -1
  162. package/dist/services/TunnelService.js +56 -15
  163. package/dist/services/TunnelService.js.map +1 -1
  164. package/dist/shared/StateManager.d.ts +49 -1
  165. package/dist/shared/StateManager.d.ts.map +1 -1
  166. package/dist/shared/StateManager.js +97 -2
  167. package/dist/shared/StateManager.js.map +1 -1
  168. package/dist/spacer-pane.d.ts +8 -0
  169. package/dist/spacer-pane.d.ts.map +1 -0
  170. package/dist/spacer-pane.js +40 -0
  171. package/dist/spacer-pane.js.map +1 -0
  172. package/dist/theme/colors.d.ts +25 -0
  173. package/dist/theme/colors.d.ts.map +1 -0
  174. package/dist/theme/colors.js +33 -0
  175. package/dist/theme/colors.js.map +1 -0
  176. package/dist/types.d.ts +14 -0
  177. package/dist/types.d.ts.map +1 -1
  178. package/dist/utils/asciiArt.d.ts +26 -0
  179. package/dist/utils/asciiArt.d.ts.map +1 -0
  180. package/dist/utils/asciiArt.js +88 -0
  181. package/dist/utils/asciiArt.js.map +1 -0
  182. package/dist/utils/conflictResolutionPane.d.ts.map +1 -1
  183. package/dist/utils/conflictResolutionPane.js +8 -4
  184. package/dist/utils/conflictResolutionPane.js.map +1 -1
  185. package/dist/utils/fileScanner.d.ts +23 -0
  186. package/dist/utils/fileScanner.d.ts.map +1 -0
  187. package/dist/utils/fileScanner.js +123 -0
  188. package/dist/utils/fileScanner.js.map +1 -0
  189. package/dist/utils/generated-agents-doc.d.ts +1 -1
  190. package/dist/utils/generated-agents-doc.js +1 -1
  191. package/dist/utils/hooks.d.ts.map +1 -1
  192. package/dist/utils/hooks.js +65 -36
  193. package/dist/utils/hooks.js.map +1 -1
  194. package/dist/utils/hooksDocs.d.ts +1 -1
  195. package/dist/utils/layoutManager.d.ts +45 -0
  196. package/dist/utils/layoutManager.d.ts.map +1 -0
  197. package/dist/utils/layoutManager.js +500 -0
  198. package/dist/utils/layoutManager.js.map +1 -0
  199. package/dist/utils/paneCreation.d.ts.map +1 -1
  200. package/dist/utils/paneCreation.js +125 -11
  201. package/dist/utils/paneCreation.js.map +1 -1
  202. package/dist/utils/popup.d.ts +97 -0
  203. package/dist/utils/popup.d.ts.map +1 -0
  204. package/dist/utils/popup.js +509 -0
  205. package/dist/utils/popup.js.map +1 -0
  206. package/dist/utils/postPaneCleanup.d.ts +12 -0
  207. package/dist/utils/postPaneCleanup.d.ts.map +1 -0
  208. package/dist/utils/postPaneCleanup.js +53 -0
  209. package/dist/utils/postPaneCleanup.js.map +1 -0
  210. package/dist/utils/shellPaneDetection.d.ts +44 -0
  211. package/dist/utils/shellPaneDetection.d.ts.map +1 -0
  212. package/dist/utils/shellPaneDetection.js +175 -0
  213. package/dist/utils/shellPaneDetection.js.map +1 -0
  214. package/dist/utils/tmux.d.ts +53 -1
  215. package/dist/utils/tmux.d.ts.map +1 -1
  216. package/dist/utils/tmux.js +352 -84
  217. package/dist/utils/tmux.js.map +1 -1
  218. package/dist/utils/welcomePane.d.ts +22 -0
  219. package/dist/utils/welcomePane.d.ts.map +1 -0
  220. package/dist/utils/welcomePane.js +119 -0
  221. package/dist/utils/welcomePane.js.map +1 -0
  222. package/dist/utils/welcomePaneManager.d.ts +36 -0
  223. package/dist/utils/welcomePaneManager.d.ts.map +1 -0
  224. package/dist/utils/welcomePaneManager.js +160 -0
  225. package/dist/utils/welcomePaneManager.js.map +1 -0
  226. package/dist/workers/PaneWorker.js +2 -0
  227. package/dist/workers/PaneWorker.js.map +1 -1
  228. package/package.json +5 -2
  229. package/dist/components/NewPaneDialog.d.ts +0 -9
  230. package/dist/components/NewPaneDialog.d.ts.map +0 -1
  231. package/dist/components/NewPaneDialog.js +0 -11
  232. package/dist/components/NewPaneDialog.js.map +0 -1
package/dist/DmuxApp.js CHANGED
@@ -1,123 +1,114 @@
1
- import React, { useState, useEffect } from 'react';
2
- import { Box, Text, useInput, useApp } from 'ink';
3
- import { execSync } from 'child_process';
4
- import path from 'path';
5
- import { createRequire } from 'module';
1
+ import React, { useState, useEffect } from "react";
2
+ import { Box, Text, useInput, useApp, useStdout } from "ink";
3
+ import { execSync } from "child_process";
4
+ import fs from "fs/promises";
5
+ import path from "path";
6
+ import { createRequire } from "module";
6
7
  // 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 usePaneRunner from './hooks/usePaneRunner.js';
15
- import usePaneCreation from './hooks/usePaneCreation.js';
16
- import useActionSystem from './hooks/useActionSystem.js';
8
+ import usePanes from "./hooks/usePanes.js";
9
+ import useProjectSettings from "./hooks/useProjectSettings.js";
10
+ import useTerminalWidth from "./hooks/useTerminalWidth.js";
11
+ import useNavigation from "./hooks/useNavigation.js";
12
+ import useAutoUpdater from "./hooks/useAutoUpdater.js";
13
+ import useAgentDetection from "./hooks/useAgentDetection.js";
14
+ import useAgentStatus from "./hooks/useAgentStatus.js";
15
+ import usePaneRunner from "./hooks/usePaneRunner.js";
16
+ import usePaneCreation from "./hooks/usePaneCreation.js";
17
+ import useActionSystem from "./hooks/useActionSystem.js";
17
18
  // Utils
18
- import { applySmartLayout } from './utils/tmux.js';
19
- import { suggestCommand } from './utils/commands.js';
20
- import { generateSlug } from './utils/slug.js';
21
- import { getMainBranch } from './utils/git.js';
22
- import { capturePaneContent } from './utils/paneCapture.js';
23
- import { StateManager } from './shared/StateManager.js';
24
- import { getStatusDetector } from './services/StatusDetector.js';
25
- import { PaneAction, getAvailableActions } from './actions/index.js';
26
- import { SettingsManager, SETTING_DEFINITIONS } from './utils/settingsManager.js';
19
+ import { enforceControlPaneSize } from "./utils/tmux.js";
20
+ import { SIDEBAR_WIDTH } from "./utils/layoutManager.js";
21
+ import { suggestCommand } from "./utils/commands.js";
22
+ import { generateSlug } from "./utils/slug.js";
23
+ import { getMainBranch } from "./utils/git.js";
24
+ import { capturePaneContent } from "./utils/paneCapture.js";
25
+ import { supportsPopups, launchNodePopupNonBlocking, POPUP_POSITIONING, } from "./utils/popup.js";
26
+ import { StateManager } from "./shared/StateManager.js";
27
+ import { LogService } from "./services/LogService.js";
28
+ import { getStatusDetector, } from "./services/StatusDetector.js";
29
+ import { PaneAction, getAvailableActions, } from "./actions/index.js";
30
+ import { SettingsManager, SETTING_DEFINITIONS, } from "./utils/settingsManager.js";
31
+ import { fileURLToPath } from "url";
32
+ import { dirname } from "path";
33
+ const __filename = fileURLToPath(import.meta.url);
34
+ const __dirname = dirname(__filename);
27
35
  const require = createRequire(import.meta.url);
28
- const packageJson = require('../package.json');
29
- import PanesGrid from './components/PanesGrid.js';
30
- import NewPaneDialog from './components/NewPaneDialog.js';
31
- import AgentChoiceDialog from './components/AgentChoiceDialog.js';
32
- import CommandPromptDialog from './components/CommandPromptDialog.js';
33
- import FileCopyPrompt from './components/FileCopyPrompt.js';
34
- import LoadingIndicator from './components/LoadingIndicator.js';
35
- import RunningIndicator from './components/RunningIndicator.js';
36
- import UpdatingIndicator from './components/UpdatingIndicator.js';
37
- import CreatingIndicator from './components/CreatingIndicator.js';
38
- import FooterHelp from './components/FooterHelp.js';
39
- import QRCode from './components/QRCode.js';
40
- import KebabMenu from './components/KebabMenu.js';
41
- import ActionChoiceDialog from './components/ActionChoiceDialog.js';
42
- import ActionConfirmDialog from './components/ActionConfirmDialog.js';
43
- import ActionInputDialog from './components/ActionInputDialog.js';
44
- import ActionProgressDialog from './components/ActionProgressDialog.js';
45
- import SettingsDialog from './components/SettingsDialog.js';
46
- import HooksDialog from './components/HooksDialog.js';
47
- const DmuxApp = ({ panesFile, projectName, sessionName, settingsFile, projectRoot, autoUpdater, serverPort, server }) => {
36
+ const packageJson = require("../package.json");
37
+ import PanesGrid from "./components/PanesGrid.js";
38
+ import CommandPromptDialog from "./components/CommandPromptDialog.js";
39
+ import FileCopyPrompt from "./components/FileCopyPrompt.js";
40
+ import LoadingIndicator from "./components/LoadingIndicator.js";
41
+ import RunningIndicator from "./components/RunningIndicator.js";
42
+ import UpdatingIndicator from "./components/UpdatingIndicator.js";
43
+ import FooterHelp from "./components/FooterHelp.js";
44
+ const DmuxApp = ({ panesFile, projectName, sessionName, settingsFile, projectRoot, autoUpdater, serverPort, server, controlPaneId, }) => {
45
+ const { stdout } = useStdout();
46
+ const terminalHeight = stdout?.rows || 40;
48
47
  /* panes state moved to usePanes */
49
48
  const [selectedIndex, setSelectedIndex] = useState(0);
50
- const [showNewPaneDialog, setShowNewPaneDialog] = useState(false);
51
- const [newPanePrompt, setNewPanePrompt] = useState('');
52
- const [statusMessage, setStatusMessage] = useState('');
49
+ const [statusMessage, setStatusMessage] = useState("");
53
50
  const [isCreatingPane, setIsCreatingPane] = useState(false);
54
- const [showQRCode, setShowQRCode] = useState(false);
55
- const [tunnelUrl, setTunnelUrl] = useState(null);
56
- const [isCreatingTunnel, setIsCreatingTunnel] = useState(false);
57
51
  // Settings state
58
52
  const [settingsManager] = useState(() => new SettingsManager(projectRoot));
59
- const [showSettingsDialog, setShowSettingsDialog] = useState(false);
60
- const [settingsMode, setSettingsMode] = useState('list');
61
- const [settingsSelectedIndex, setSettingsSelectedIndex] = useState(0);
62
- const [settingsEditingKey, setSettingsEditingKey] = useState();
63
- const [settingsEditingValueIndex, setSettingsEditingValueIndex] = useState(0);
64
- const [settingsScopeIndex, setSettingsScopeIndex] = useState(0);
65
53
  // Force repaint trigger - incrementing this causes Ink to re-render
66
54
  const [forceRepaintTrigger, setForceRepaintTrigger] = useState(0);
67
55
  // Spinner state - shows for a few frames to force render
68
56
  const [showRepaintSpinner, setShowRepaintSpinner] = useState(false);
69
57
  const { projectSettings, saveSettings } = useProjectSettings(settingsFile);
70
- // Hooks management state
71
- const [showHooksDialog, setShowHooksDialog] = useState(false);
72
- const [hooksSelectedIndex, setHooksSelectedIndex] = useState(0);
73
- const [hooksData, setHooksData] = useState([]);
74
58
  const [showCommandPrompt, setShowCommandPrompt] = useState(null);
75
- const [commandInput, setCommandInput] = useState('');
59
+ const [commandInput, setCommandInput] = useState("");
76
60
  const [showFileCopyPrompt, setShowFileCopyPrompt] = useState(false);
77
61
  const [currentCommandType, setCurrentCommandType] = useState(null);
78
62
  const [runningCommand, setRunningCommand] = useState(false);
79
63
  const [quitConfirmMode, setQuitConfirmMode] = useState(false);
80
- const [showKebabMenu, setShowKebabMenu] = useState(false);
81
- const [kebabMenuPaneIndex, setKebabMenuPaneIndex] = useState(null);
82
- const [kebabMenuOption, setKebabMenuOption] = useState(0);
83
- const [kebabMenuActions, setKebabMenuActions] = useState([]);
84
64
  // Debug message state - for temporary logging messages
85
- const [debugMessage, setDebugMessage] = useState('');
65
+ const [debugMessage, setDebugMessage] = useState("");
66
+ // Current git branch state (for dev builds)
67
+ const [currentBranch, setCurrentBranch] = useState(null);
86
68
  // Update state handled by hook
87
- const { updateInfo, showUpdateDialog, isUpdating, performUpdate, skipUpdate, dismissUpdate, updateAvailable } = useAutoUpdater(autoUpdater, setStatusMessage);
69
+ const { updateInfo, showUpdateDialog, isUpdating, performUpdate, skipUpdate, dismissUpdate, updateAvailable, } = useAutoUpdater(autoUpdater, setStatusMessage);
88
70
  const { exit } = useApp();
71
+ // Flag to ignore input temporarily after popup closes (prevents buffered keys)
72
+ const [ignoreInput, setIgnoreInput] = useState(false);
89
73
  // Agent selection state
90
74
  const { availableAgents } = useAgentDetection();
91
- const [showAgentChoiceDialog, setShowAgentChoiceDialog] = useState(false);
92
75
  const [agentChoice, setAgentChoice] = useState(null);
93
- const [pendingPrompt, setPendingPrompt] = useState('');
76
+ // Popup support detection
77
+ const [popupsSupported, setPopupsSupported] = useState(false);
94
78
  // Track terminal dimensions for responsive layout
95
79
  const terminalWidth = useTerminalWidth();
80
+ // Track unread error and warning counts for logs badge
81
+ const [unreadErrorCount, setUnreadErrorCount] = useState(0);
82
+ const [unreadWarningCount, setUnreadWarningCount] = useState(0);
83
+ // Tunnel state
84
+ const [tunnelUrl, setTunnelUrl] = useState(null);
85
+ const [tunnelCreating, setTunnelCreating] = useState(false);
86
+ const [tunnelSpinnerFrame, setTunnelSpinnerFrame] = useState(0);
87
+ const [localIp, setLocalIp] = useState("127.0.0.1");
88
+ const [tunnelCopied, setTunnelCopied] = useState(false);
89
+ // Subscribe to StateManager for unread error/warning count updates
90
+ useEffect(() => {
91
+ const stateManager = StateManager.getInstance();
92
+ const updateCounts = () => {
93
+ setUnreadErrorCount(stateManager.getUnreadErrorCount());
94
+ setUnreadWarningCount(stateManager.getUnreadWarningCount());
95
+ };
96
+ // Initial count
97
+ updateCounts();
98
+ // Subscribe to changes
99
+ const unsubscribe = stateManager.subscribe(updateCounts);
100
+ return () => {
101
+ unsubscribe();
102
+ };
103
+ }, []);
96
104
  // Panes state and persistence (skipLoading will be updated after actionSystem is initialized)
97
105
  const { panes, setPanes, isLoading, loadPanes, savePanes } = usePanes(panesFile, false);
98
106
  // Track intentionally closed panes to prevent race condition
99
107
  // When a user closes a pane, we add it to this set. If the worker detects
100
108
  // the pane is gone (which it will), we check this set first before re-saving.
101
109
  const intentionallyClosedPanes = React.useRef(new Set());
102
- // Action system
103
- const actionSystem = useActionSystem({
104
- panes,
105
- savePanes,
106
- sessionName,
107
- projectName,
108
- onPaneRemove: (paneId) => {
109
- // Mark this pane as intentionally closed
110
- intentionallyClosedPanes.current.add(paneId);
111
- const updated = panes.filter(p => p.id !== paneId);
112
- setPanes(updated);
113
- // Clean up the tracking after a delay (in case of race conditions)
114
- setTimeout(() => {
115
- intentionallyClosedPanes.current.delete(paneId);
116
- }, 5000);
117
- },
118
- });
119
110
  // Pane runner
120
- const { copyNonGitFiles, runCommandInternal, monitorTestOutput, monitorDevOutput, attachBackgroundWindow } = usePaneRunner({
111
+ const { copyNonGitFiles, runCommandInternal, monitorTestOutput, monitorDevOutput, attachBackgroundWindow, } = usePaneRunner({
121
112
  panes,
122
113
  savePanes,
123
114
  projectSettings,
@@ -126,7 +117,7 @@ const DmuxApp = ({ panesFile, projectName, sessionName, settingsFile, projectRoo
126
117
  });
127
118
  // Force repaint helper - shows spinner for a few frames to force full re-render
128
119
  const forceRepaint = () => {
129
- setForceRepaintTrigger(prev => prev + 1);
120
+ setForceRepaintTrigger((prev) => prev + 1);
130
121
  setShowRepaintSpinner(true);
131
122
  // Hide spinner after a few frames (enough to trigger multiple renders)
132
123
  setTimeout(() => setShowRepaintSpinner(false), 100);
@@ -137,13 +128,57 @@ const DmuxApp = ({ panesFile, projectName, sessionName, settingsFile, projectRoo
137
128
  // Small delay to ensure terminal is ready
138
129
  const timer = setTimeout(() => {
139
130
  try {
140
- execSync('tmux refresh-client', { stdio: 'pipe' });
131
+ execSync("tmux refresh-client", { stdio: "pipe" });
141
132
  }
142
133
  catch { }
143
134
  }, 50);
144
135
  return () => clearTimeout(timer);
145
136
  }
146
137
  }, [forceRepaintTrigger]);
138
+ // Get local network IP on mount
139
+ useEffect(() => {
140
+ try {
141
+ // Get local IP address (not 127.0.0.1)
142
+ const result = execSync(`hostname -I 2>/dev/null || ifconfig | grep "inet " | grep -v 127.0.0.1 | awk '{print $2}' | head -1`, {
143
+ encoding: "utf-8",
144
+ stdio: "pipe",
145
+ }).trim();
146
+ if (result) {
147
+ setLocalIp(result.split(" ")[0]); // Take first IP if multiple
148
+ }
149
+ }
150
+ catch {
151
+ // Fallback to 127.0.0.1
152
+ setLocalIp("127.0.0.1");
153
+ }
154
+ }, []);
155
+ // Spinner animation for tunnel creation
156
+ useEffect(() => {
157
+ if (!tunnelCreating)
158
+ return;
159
+ const spinnerInterval = setInterval(() => {
160
+ setTunnelSpinnerFrame((prev) => (prev + 1) % 10);
161
+ }, 80); // Update every 80ms
162
+ return () => clearInterval(spinnerInterval);
163
+ }, [tunnelCreating]);
164
+ // Get current git branch on mount (only for dev builds)
165
+ useEffect(() => {
166
+ const isDev = process.env.DMUX_DEV === "true" || __dirname.includes("dist") === false;
167
+ if (isDev) {
168
+ try {
169
+ const branch = execSync("git rev-parse --abbrev-ref HEAD", {
170
+ encoding: "utf-8",
171
+ stdio: "pipe",
172
+ cwd: projectRoot,
173
+ }).trim();
174
+ setCurrentBranch(branch);
175
+ }
176
+ catch {
177
+ // Not in a git repo or git not available
178
+ setCurrentBranch(null);
179
+ }
180
+ }
181
+ }, [projectRoot]);
147
182
  // Pane creation
148
183
  const { createNewPane: createNewPaneHook } = usePaneCreation({
149
184
  panes,
@@ -151,7 +186,6 @@ const DmuxApp = ({ panesFile, projectName, sessionName, settingsFile, projectRoo
151
186
  projectName,
152
187
  setIsCreatingPane,
153
188
  setStatusMessage,
154
- setNewPanePrompt,
155
189
  loadPanes,
156
190
  panesFile,
157
191
  availableAgents,
@@ -161,8 +195,8 @@ const DmuxApp = ({ panesFile, projectName, sessionName, settingsFile, projectRoo
161
195
  useEffect(() => {
162
196
  const statusDetector = getStatusDetector();
163
197
  const handleStatusUpdate = (event) => {
164
- setPanes(prevPanes => {
165
- const updatedPanes = prevPanes.map(pane => {
198
+ setPanes((prevPanes) => {
199
+ const updatedPanes = prevPanes.map((pane) => {
166
200
  if (pane.id === event.paneId) {
167
201
  const updated = {
168
202
  ...pane,
@@ -186,22 +220,23 @@ const DmuxApp = ({ panesFile, projectName, sessionName, settingsFile, projectRoo
186
220
  updated.analyzerError = event.analyzerError;
187
221
  }
188
222
  // Clear option dialog data when transitioning away from 'waiting' state
189
- if (event.status !== 'waiting' && pane.agentStatus === 'waiting') {
223
+ if (event.status !== "waiting" && pane.agentStatus === "waiting") {
190
224
  updated.optionsQuestion = undefined;
191
225
  updated.options = undefined;
192
226
  updated.potentialHarm = undefined;
193
227
  }
194
228
  // Clear summary when transitioning away from 'idle' state
195
- if (event.status !== 'idle' && pane.agentStatus === 'idle') {
229
+ if (event.status !== "idle" && pane.agentStatus === "idle") {
196
230
  updated.agentSummary = undefined;
197
231
  }
198
232
  // Clear analyzer error when successfully getting a new analysis
199
233
  // or when transitioning to 'working' status
200
- if (event.status === 'working') {
234
+ if (event.status === "working") {
201
235
  updated.analyzerError = undefined;
202
236
  }
203
- else if (event.status === 'waiting' || event.status === 'idle') {
204
- if (event.analyzerError === undefined && (event.optionsQuestion || event.summary)) {
237
+ else if (event.status === "waiting" || event.status === "idle") {
238
+ if (event.analyzerError === undefined &&
239
+ (event.optionsQuestion || event.summary)) {
205
240
  updated.analyzerError = undefined;
206
241
  }
207
242
  }
@@ -210,15 +245,15 @@ const DmuxApp = ({ panesFile, projectName, sessionName, settingsFile, projectRoo
210
245
  return pane;
211
246
  });
212
247
  // Persist to disk - ConfigWatcher will handle syncing to StateManager
213
- savePanes(updatedPanes).catch(err => {
214
- console.error('Failed to save panes after status update:', err);
248
+ savePanes(updatedPanes).catch((err) => {
249
+ console.error("Failed to save panes after status update:", err);
215
250
  });
216
251
  return updatedPanes;
217
252
  });
218
253
  };
219
- statusDetector.on('status-updated', handleStatusUpdate);
254
+ statusDetector.on("status-updated", handleStatusUpdate);
220
255
  return () => {
221
- statusDetector.off('status-updated', handleStatusUpdate);
256
+ statusDetector.off("status-updated", handleStatusUpdate);
222
257
  };
223
258
  }, [setPanes, savePanes]);
224
259
  // Note: No need to sync panes with StateManager here.
@@ -243,50 +278,936 @@ const DmuxApp = ({ panesFile, projectName, sessionName, settingsFile, projectRoo
243
278
  const handleTermination = () => {
244
279
  cleanExit();
245
280
  };
246
- process.on('SIGTERM', handleTermination);
247
- // Test debug message on mount
248
- StateManager.getInstance().setDebugMessage('Debug logging initialized - watching for AI activity...');
249
- setTimeout(() => {
250
- StateManager.getInstance().setDebugMessage('');
251
- }, 3000);
281
+ process.on("SIGTERM", handleTermination);
282
+ // Check if tmux supports popups (3.2+) and enable mouse mode for click-outside-to-close
283
+ const popupSupport = supportsPopups();
284
+ setPopupsSupported(popupSupport);
285
+ if (popupSupport) {
286
+ // Enable mouse mode only for this dmux session (not global)
287
+ }
252
288
  return () => {
253
- process.removeListener('SIGTERM', handleTermination);
289
+ process.removeListener("SIGTERM", handleTermination);
254
290
  };
255
291
  }, []);
256
- // Auto-show new pane dialog when starting with no panes
257
- useEffect(() => {
258
- // Only show the dialog if:
259
- // 1. Initial load is complete (!isLoading)
260
- // 2. We have no panes
261
- // 3. We're not already showing the dialog
262
- // 4. We're not showing any other dialogs or prompts
263
- if (!isLoading &&
264
- panes.length === 0 &&
265
- !showNewPaneDialog &&
266
- !actionSystem.actionState.showConfirmDialog &&
267
- !actionSystem.actionState.showChoiceDialog &&
268
- !actionSystem.actionState.showInputDialog &&
269
- !actionSystem.actionState.showProgressDialog &&
270
- !showCommandPrompt &&
271
- !showFileCopyPrompt &&
272
- !showAgentChoiceDialog &&
273
- !isCreatingPane &&
274
- !runningCommand &&
275
- !isUpdating) {
276
- setShowNewPaneDialog(true);
277
- }
278
- }, [isLoading, panes.length, showNewPaneDialog, actionSystem.actionState.showConfirmDialog, actionSystem.actionState.showChoiceDialog, actionSystem.actionState.showInputDialog, actionSystem.actionState.showProgressDialog, showCommandPrompt, showFileCopyPrompt, showAgentChoiceDialog, isCreatingPane, runningCommand, isUpdating]);
279
292
  // Update checking moved to useAutoUpdater
280
293
  // Set default agent choice when detection completes
281
294
  useEffect(() => {
282
295
  if (agentChoice == null && availableAgents.length > 0) {
283
- setAgentChoice(availableAgents[0] || 'claude');
296
+ setAgentChoice(availableAgents[0] || "claude");
284
297
  }
285
298
  }, [availableAgents]);
299
+ // Welcome pane is now fully event-based:
300
+ // - Created at startup (in src/index.ts)
301
+ // - Destroyed when first pane is created (in paneCreation.ts)
302
+ // - Recreated when last pane is closed (in paneActions.ts)
303
+ // No polling needed!
304
+ // loadPanes moved to usePanes
305
+ // getPanePositions moved to utils/tmux
306
+ // Navigation logic moved to hook
307
+ const { getCardGridPosition, findCardInDirection } = useNavigation(terminalWidth, panes.length, isLoading);
308
+ // findCardInDirection provided by useNavigation
309
+ // savePanes moved to usePanes
310
+ // applySmartLayout moved to utils/tmux
311
+ const launchNewPanePopup = async () => {
312
+ // Only launch popup if tmux supports it
313
+ if (!popupsSupported) {
314
+ setStatusMessage("Popups require tmux 3.2+");
315
+ setTimeout(() => setStatusMessage(""), 3000);
316
+ return;
317
+ }
318
+ try {
319
+ // Resolve the popup script path from the project root
320
+ // This handles both dev (tsx running from src) and prod (compiled to dist)
321
+ const projectRootForPopup = __dirname.includes("/dist")
322
+ ? path.resolve(__dirname, "..") // If in dist/, go up one level
323
+ : path.resolve(__dirname, ".."); // If in src/, go up one level
324
+ const popupScriptPath = path.join(projectRootForPopup, "dist", "popups", "newPanePopup.js");
325
+ // Calculate popup height as 80% of terminal height to allow room for file list
326
+ const popupHeight = Math.floor(terminalHeight * 0.8);
327
+ // Launch the popup non-blocking and track it
328
+ const popupHandle = launchNodePopupNonBlocking(popupScriptPath, [], {
329
+ ...POPUP_POSITIONING.centeredWithSidebar(SIDEBAR_WIDTH),
330
+ width: 90,
331
+ height: popupHeight,
332
+ title: " ✨ dmux - Create New Pane ",
333
+ });
334
+ LogService.getInstance().debug(`Popup created - PID: ${popupHandle.pid}, bounds: ${JSON.stringify(popupHandle.bounds)}`, "PopupTracking");
335
+ // Wait for the popup to close
336
+ const result = await popupHandle.resultPromise;
337
+ // Clear active popup tracking
338
+ LogService.getInstance().debug("Popup closed, clearing tracking", "PopupTracking");
339
+ // Ignore input briefly after popup closes to prevent buffered keys
340
+ setIgnoreInput(true);
341
+ setTimeout(() => setIgnoreInput(false), 100);
342
+ if (result.success && result.data) {
343
+ // User entered a prompt - now decide which agent to use
344
+ const promptValue = result.data;
345
+ const agents = availableAgents;
346
+ if (agents.length === 0) {
347
+ await createNewPaneHook(promptValue);
348
+ }
349
+ else if (agents.length === 1) {
350
+ await createNewPaneHook(promptValue, agents[0]);
351
+ }
352
+ else {
353
+ // Multiple agents available - check for default agent setting first
354
+ const settings = settingsManager.getSettings();
355
+ if (settings.defaultAgent && agents.includes(settings.defaultAgent)) {
356
+ // Use the default agent from settings
357
+ await createNewPaneHook(promptValue, settings.defaultAgent);
358
+ }
359
+ else {
360
+ // No default agent configured or default not available - show agent choice popup
361
+ const selectedAgent = await launchAgentChoicePopup(promptValue);
362
+ if (selectedAgent) {
363
+ await createNewPaneHook(promptValue, selectedAgent);
364
+ }
365
+ }
366
+ }
367
+ }
368
+ else if (result.cancelled) {
369
+ // User pressed ESC - do nothing
370
+ return;
371
+ }
372
+ else if (result.error) {
373
+ setStatusMessage(`Popup error: ${result.error}`);
374
+ setTimeout(() => setStatusMessage(""), 3000);
375
+ }
376
+ }
377
+ catch (error) {
378
+ setStatusMessage(`Failed to launch popup: ${error.message}`);
379
+ setTimeout(() => setStatusMessage(""), 3000);
380
+ }
381
+ };
382
+ const launchKebabMenuPopup = async (paneIndex) => {
383
+ // Only launch popup if tmux supports it
384
+ if (!popupsSupported) {
385
+ setStatusMessage("Popups require tmux 3.2+");
386
+ setTimeout(() => setStatusMessage(""), 3000);
387
+ return;
388
+ }
389
+ const selectedPane = panes[paneIndex];
390
+ if (!selectedPane) {
391
+ return;
392
+ }
393
+ try {
394
+ // Resolve the popup script path
395
+ const projectRootForPopup = __dirname.includes("/dist")
396
+ ? path.resolve(__dirname, "..") // If in dist/, go up one level
397
+ : path.resolve(__dirname, ".."); // If in src/, go up one level
398
+ const popupScriptPath = path.join(projectRootForPopup, "dist", "popups", "kebabMenuPopup.js");
399
+ // Get available actions for this pane
400
+ const actions = getAvailableActions(selectedPane, projectSettings);
401
+ const actionsJson = JSON.stringify(actions);
402
+ // Launch the popup non-blocking and track it
403
+ const popupHandle = launchNodePopupNonBlocking(popupScriptPath, [selectedPane.slug, actionsJson], {
404
+ ...POPUP_POSITIONING.standard(SIDEBAR_WIDTH),
405
+ width: 60,
406
+ height: Math.min(20, actions.length + 5),
407
+ title: `Menu: ${selectedPane.slug}`,
408
+ });
409
+ // Wait for the popup to close
410
+ const result = await popupHandle.resultPromise;
411
+ // Clear active popup tracking
412
+ // Log the entire result for debugging
413
+ LogService.getInstance().debug(`Kebab menu result: ${JSON.stringify(result)}`, "KebabMenu");
414
+ if (result.success && result.data) {
415
+ // User selected an action
416
+ const actionId = result.data;
417
+ LogService.getInstance().debug(`Action selected: ${actionId}`, "KebabMenu");
418
+ // Handle merge action with dedicated popup
419
+ if (actionId === PaneAction.MERGE) {
420
+ LogService.getInstance().debug(`Merge action selected for pane: ${selectedPane.slug}`, "MergeAction");
421
+ try {
422
+ await launchMergePopup(selectedPane);
423
+ LogService.getInstance().debug("Merge popup completed", "MergeAction");
424
+ }
425
+ catch (error) {
426
+ LogService.getInstance().error("Merge popup error", "MergeAction", selectedPane.id, error instanceof Error ? error : undefined);
427
+ setStatusMessage(`Merge popup failed: ${error}`);
428
+ setTimeout(() => setStatusMessage(""), 3000);
429
+ }
430
+ }
431
+ else {
432
+ // Execute other actions through action system
433
+ await actionSystem.executeAction(actionId, selectedPane, {
434
+ mainBranch: getMainBranch(),
435
+ });
436
+ }
437
+ }
438
+ else if (result.cancelled) {
439
+ // User pressed ESC - do nothing
440
+ LogService.getInstance().debug("Kebab menu cancelled", "KebabMenu");
441
+ return;
442
+ }
443
+ else if (result.error) {
444
+ LogService.getInstance().error(`Kebab menu error: ${result.error}`, "KebabMenu");
445
+ setStatusMessage(`Popup error: ${result.error}`);
446
+ setTimeout(() => setStatusMessage(""), 3000);
447
+ }
448
+ else {
449
+ LogService.getInstance().warn(`Unexpected kebab menu result: ${JSON.stringify(result)}`, "KebabMenu");
450
+ }
451
+ }
452
+ catch (error) {
453
+ setStatusMessage(`Failed to launch popup: ${error.message}`);
454
+ setTimeout(() => setStatusMessage(""), 3000);
455
+ }
456
+ };
457
+ const launchConfirmPopup = async (title, message, yesLabel, noLabel) => {
458
+ // Only launch popup if tmux supports it
459
+ if (!popupsSupported) {
460
+ setStatusMessage("Popups require tmux 3.2+");
461
+ setTimeout(() => setStatusMessage(""), 3000);
462
+ return false;
463
+ }
464
+ try {
465
+ // Resolve the popup script path
466
+ const projectRootForPopup = __dirname.includes("/dist")
467
+ ? path.resolve(__dirname, "..") // If in dist/, go up one level
468
+ : path.resolve(__dirname, ".."); // If in src/, go up one level
469
+ const popupScriptPath = path.join(projectRootForPopup, "dist", "popups", "confirmPopup.js");
470
+ // Write data to temp file to avoid shell escaping issues
471
+ const dataFile = `/tmp/dmux-confirm-${Date.now()}.json`;
472
+ const dataJson = JSON.stringify({ title, message, yesLabel, noLabel });
473
+ await fs.writeFile(dataFile, dataJson);
474
+ // Launch the popup non-blocking and track it
475
+ const popupHandle = launchNodePopupNonBlocking(popupScriptPath, [dataFile], {
476
+ ...POPUP_POSITIONING.standard(SIDEBAR_WIDTH),
477
+ width: 60,
478
+ height: 12,
479
+ title: title || "Confirm",
480
+ });
481
+ // Wait for the popup to close
482
+ const result = await popupHandle.resultPromise;
483
+ // Clear active popup tracking
484
+ // Clean up temp file
485
+ try {
486
+ await fs.unlink(dataFile);
487
+ }
488
+ catch { }
489
+ if (result.success && result.data !== undefined) {
490
+ return result.data;
491
+ }
492
+ else if (result.cancelled) {
493
+ return false;
494
+ }
495
+ else if (result.error) {
496
+ setStatusMessage(`Popup error: ${result.error}`);
497
+ setTimeout(() => setStatusMessage(""), 3000);
498
+ return false;
499
+ }
500
+ }
501
+ catch (error) {
502
+ setStatusMessage(`Failed to launch popup: ${error.message}`);
503
+ setTimeout(() => setStatusMessage(""), 3000);
504
+ }
505
+ return false;
506
+ };
507
+ const launchAgentChoicePopup = async (prompt) => {
508
+ // Only launch popup if tmux supports it
509
+ if (!popupsSupported) {
510
+ setStatusMessage("Popups require tmux 3.2+");
511
+ setTimeout(() => setStatusMessage(""), 3000);
512
+ return null;
513
+ }
514
+ try {
515
+ // Resolve the popup script path
516
+ const projectRootForPopup = __dirname.includes("/dist")
517
+ ? path.resolve(__dirname, "..") // If in dist/, go up one level
518
+ : path.resolve(__dirname, ".."); // If in src/, go up one level
519
+ const popupScriptPath = path.join(projectRootForPopup, "dist", "popups", "agentChoicePopup.js");
520
+ const agentsJson = JSON.stringify(availableAgents);
521
+ const defaultAgentArg = agentChoice || availableAgents[0] || "claude";
522
+ // Launch the popup non-blocking and track it
523
+ const popupHandle = launchNodePopupNonBlocking(popupScriptPath, [agentsJson, defaultAgentArg], {
524
+ ...POPUP_POSITIONING.standard(SIDEBAR_WIDTH),
525
+ width: 50,
526
+ height: 10,
527
+ title: "Select Agent",
528
+ });
529
+ // Wait for the popup to close
530
+ const result = await popupHandle.resultPromise;
531
+ // Clear active popup tracking
532
+ if (result.success && result.data) {
533
+ return result.data;
534
+ }
535
+ else if (result.cancelled) {
536
+ return null;
537
+ }
538
+ else if (result.error) {
539
+ setStatusMessage(`Popup error: ${result.error}`);
540
+ setTimeout(() => setStatusMessage(""), 3000);
541
+ return null;
542
+ }
543
+ }
544
+ catch (error) {
545
+ setStatusMessage(`Failed to launch popup: ${error.message}`);
546
+ setTimeout(() => setStatusMessage(""), 3000);
547
+ }
548
+ return null;
549
+ };
550
+ const launchHooksPopup = async () => {
551
+ // Only launch popup if tmux supports it
552
+ if (!popupsSupported) {
553
+ setStatusMessage("Popups require tmux 3.2+");
554
+ setTimeout(() => setStatusMessage(""), 3000);
555
+ return;
556
+ }
557
+ try {
558
+ // Resolve the popup script path
559
+ const projectRootForPopup = __dirname.includes("/dist")
560
+ ? path.resolve(__dirname, "..") // If in dist/, go up one level
561
+ : path.resolve(__dirname, ".."); // If in src/, go up one level
562
+ const popupScriptPath = path.join(projectRootForPopup, "dist", "popups", "hooksPopup.js");
563
+ // Get hooks data
564
+ const { hasHook } = await import("./utils/hooks.js");
565
+ const allHookTypes = [
566
+ "before_pane_create",
567
+ "pane_created",
568
+ "worktree_created",
569
+ "before_pane_close",
570
+ "pane_closed",
571
+ "before_worktree_remove",
572
+ "worktree_removed",
573
+ "pre_merge",
574
+ "post_merge",
575
+ "run_test",
576
+ "run_dev",
577
+ ];
578
+ const hooks = allHookTypes.map((hookName) => ({
579
+ name: hookName,
580
+ active: hasHook(projectRoot || process.cwd(), hookName),
581
+ }));
582
+ const hooksJson = JSON.stringify(hooks);
583
+ // Launch the popup non-blocking and track it
584
+ const popupHandle = launchNodePopupNonBlocking(popupScriptPath, [hooksJson], {
585
+ ...POPUP_POSITIONING.standard(SIDEBAR_WIDTH),
586
+ width: 70,
587
+ height: 24,
588
+ title: "🪝 Manage Hooks",
589
+ });
590
+ // Wait for the popup to close
591
+ const result = await popupHandle.resultPromise;
592
+ // Clear active popup tracking
593
+ if (result.success && result.data?.action === "edit") {
594
+ // Edit hooks using an agent
595
+ const prompt = "I would like to edit my dmux hooks in .dmux-hooks, please read the instructions in there and ask me what I want to edit";
596
+ // Choose agent
597
+ const agents = availableAgents;
598
+ if (agents.length === 0) {
599
+ await createNewPaneHook(prompt);
600
+ }
601
+ else if (agents.length === 1) {
602
+ await createNewPaneHook(prompt, agents[0]);
603
+ }
604
+ else {
605
+ // Multiple agents available - check for default agent setting first
606
+ const settings = settingsManager.getSettings();
607
+ if (settings.defaultAgent && agents.includes(settings.defaultAgent)) {
608
+ // Use the default agent from settings
609
+ await createNewPaneHook(prompt, settings.defaultAgent);
610
+ }
611
+ else {
612
+ // No default agent configured or default not available - show agent choice popup
613
+ const selectedAgent = await launchAgentChoicePopup(prompt);
614
+ if (selectedAgent) {
615
+ await createNewPaneHook(prompt, selectedAgent);
616
+ }
617
+ }
618
+ }
619
+ }
620
+ else if (result.success && result.data?.action === "view") {
621
+ // View hooks file in editor - could implement this later
622
+ setStatusMessage("View in editor not yet implemented");
623
+ setTimeout(() => setStatusMessage(""), 2000);
624
+ }
625
+ else if (result.cancelled) {
626
+ // User pressed ESC - do nothing
627
+ return;
628
+ }
629
+ else if (result.error) {
630
+ setStatusMessage(`Popup error: ${result.error}`);
631
+ setTimeout(() => setStatusMessage(""), 3000);
632
+ }
633
+ }
634
+ catch (error) {
635
+ setStatusMessage(`Failed to launch popup: ${error.message}`);
636
+ setTimeout(() => setStatusMessage(""), 3000);
637
+ }
638
+ };
639
+ const launchLogsPopup = async () => {
640
+ // Only launch popup if tmux supports it
641
+ if (!popupsSupported) {
642
+ setStatusMessage("Popups require tmux 3.2+");
643
+ setTimeout(() => setStatusMessage(""), 3000);
644
+ return;
645
+ }
646
+ try {
647
+ // Resolve the popup script path
648
+ const projectRootForPopup = __dirname.includes("/dist")
649
+ ? path.resolve(__dirname, "..") // If in dist/, go up one level
650
+ : path.resolve(__dirname, ".."); // If in src/, go up one level
651
+ const popupScriptPath = path.join(projectRootForPopup, "dist", "popups", "logsPopup.js");
652
+ // Get logs from StateManager and write to temp file
653
+ const stateManager = StateManager.getInstance();
654
+ const allLogs = stateManager.getLogs();
655
+ const stats = stateManager.getLogStats();
656
+ const logsData = { logs: allLogs, stats };
657
+ // Write data to temp file to avoid shell escaping issues with complex JSON
658
+ const dataFile = `/tmp/dmux-logs-${Date.now()}.json`;
659
+ const dataJson = JSON.stringify(logsData);
660
+ await fs.writeFile(dataFile, dataJson);
661
+ // Launch the popup with large positioning
662
+ // Get tmux client dimensions (not process.stdout which is just the sidebar)
663
+ const tmuxDims = execSync('tmux display-message -p "#{client_width},#{client_height}"', { encoding: "utf-8" }).trim();
664
+ const [termWidth, termHeight] = tmuxDims.split(",").map(Number);
665
+ // Launch the popup non-blocking and track it
666
+ const popupHandle = launchNodePopupNonBlocking(popupScriptPath, [dataFile], {
667
+ ...POPUP_POSITIONING.large(SIDEBAR_WIDTH, termWidth, termHeight),
668
+ title: "🪵 dmux Logs",
669
+ });
670
+ // Wait for the popup to close
671
+ const result = await popupHandle.resultPromise;
672
+ // Clear active popup tracking
673
+ // Clean up temp file
674
+ try {
675
+ await fs.unlink(dataFile);
676
+ }
677
+ catch (err) {
678
+ // Ignore cleanup errors
679
+ }
680
+ // Popup closed - mark all logs as read
681
+ if (result.success) {
682
+ stateManager.markAllLogsAsRead();
683
+ }
684
+ }
685
+ catch (error) {
686
+ setStatusMessage(`Failed to launch popup: ${error.message}`);
687
+ setTimeout(() => setStatusMessage(""), 3000);
688
+ }
689
+ };
690
+ const launchShortcutsPopup = async () => {
691
+ // Only launch popup if tmux supports it
692
+ if (!popupsSupported) {
693
+ setStatusMessage("Popups require tmux 3.2+");
694
+ setTimeout(() => setStatusMessage(""), 3000);
695
+ return;
696
+ }
697
+ try {
698
+ // Resolve the popup script path
699
+ const projectRootForPopup = __dirname.includes("/dist")
700
+ ? path.resolve(__dirname, "..") // If in dist/, go up one level
701
+ : path.resolve(__dirname, ".."); // If in src/, go up one level
702
+ const popupScriptPath = path.join(projectRootForPopup, "dist", "popups", "shortcutsPopup.js");
703
+ // Prepare data for shortcuts popup
704
+ const shortcutsData = {
705
+ hasSidebarLayout: !!controlPaneId,
706
+ showRemoteKey: !!server,
707
+ };
708
+ // Write data to temp file
709
+ const dataFile = `/tmp/dmux-shortcuts-${Date.now()}.json`;
710
+ const dataJson = JSON.stringify(shortcutsData);
711
+ await fs.writeFile(dataFile, dataJson);
712
+ // Launch the popup non-blocking and track it
713
+ const popupHandle = launchNodePopupNonBlocking(popupScriptPath, [dataFile], {
714
+ ...POPUP_POSITIONING.standard(SIDEBAR_WIDTH),
715
+ width: 50,
716
+ height: 20,
717
+ title: "⌨️ Keyboard Shortcuts",
718
+ });
719
+ // Wait for the popup to close
720
+ const result = await popupHandle.resultPromise;
721
+ // Clear active popup tracking
722
+ // Clean up temp file
723
+ try {
724
+ await fs.unlink(dataFile);
725
+ }
726
+ catch (err) {
727
+ // Ignore cleanup errors
728
+ }
729
+ }
730
+ catch (error) {
731
+ setStatusMessage(`Failed to launch popup: ${error.message}`);
732
+ setTimeout(() => setStatusMessage(""), 3000);
733
+ }
734
+ };
735
+ const launchRemotePopup = async () => {
736
+ // Only launch popup if tmux supports it
737
+ if (!popupsSupported) {
738
+ setStatusMessage("Popups require tmux 3.2+");
739
+ setTimeout(() => setStatusMessage(""), 3000);
740
+ return;
741
+ }
742
+ // Check if server is available and tunnel URL exists
743
+ if (!server || !serverPort || !tunnelUrl) {
744
+ setStatusMessage("Tunnel not ready");
745
+ setTimeout(() => setStatusMessage(""), 3000);
746
+ return;
747
+ }
748
+ try {
749
+ // Resolve the popup script path
750
+ const projectRootForPopup = __dirname.includes("/dist")
751
+ ? path.resolve(__dirname, "..") // If in dist/, go up one level
752
+ : path.resolve(__dirname, ".."); // If in src/, go up one level
753
+ const popupScriptPath = path.join(projectRootForPopup, "dist", "popups", "remotePopup.js");
754
+ // Prepare status file with existing tunnel URL
755
+ const tunnelStatusFile = `/tmp/dmux-tunnel-status-${Date.now()}.json`;
756
+ await fs.writeFile(tunnelStatusFile, JSON.stringify({ url: tunnelUrl }));
757
+ // Prepare data for remote popup
758
+ const remoteData = {
759
+ loading: false,
760
+ serverPort: serverPort,
761
+ statusFile: tunnelStatusFile,
762
+ };
763
+ // Write data to temp file
764
+ const dataFile = `/tmp/dmux-remote-${Date.now()}.json`;
765
+ await fs.writeFile(dataFile, JSON.stringify(remoteData));
766
+ // Launch the popup non-blocking and track it
767
+ const popupHandle = launchNodePopupNonBlocking(popupScriptPath, [dataFile], {
768
+ ...POPUP_POSITIONING.centeredWithSidebar(SIDEBAR_WIDTH),
769
+ width: 60,
770
+ height: 30,
771
+ title: "🌐 Remote Access",
772
+ });
773
+ // Wait for the popup to close
774
+ const result = await popupHandle.resultPromise;
775
+ // Clear active popup tracking
776
+ // Show "Copied!" message if URL was copied
777
+ // Note: result is the parsed JSON directly, not wrapped in PopupResult.data
778
+ if (result && result.copied) {
779
+ setTunnelCopied(true);
780
+ setTimeout(() => setTunnelCopied(false), 2000);
781
+ }
782
+ // Clean up temp files
783
+ try {
784
+ await fs.unlink(dataFile);
785
+ }
786
+ catch (err) {
787
+ // Ignore cleanup errors
788
+ }
789
+ try {
790
+ await fs.unlink(tunnelStatusFile);
791
+ }
792
+ catch (err) {
793
+ // Ignore cleanup errors (file might not exist yet)
794
+ }
795
+ }
796
+ catch (error) {
797
+ setStatusMessage(`Failed to launch popup: ${error.message}`);
798
+ setTimeout(() => setStatusMessage(""), 3000);
799
+ }
800
+ };
801
+ const launchSettingsPopup = async () => {
802
+ // Only launch popup if tmux supports it
803
+ if (!popupsSupported) {
804
+ setStatusMessage("Popups require tmux 3.2+");
805
+ setTimeout(() => setStatusMessage(""), 3000);
806
+ return;
807
+ }
808
+ try {
809
+ // Resolve the popup script path
810
+ const projectRootForPopup = __dirname.includes("/dist")
811
+ ? path.resolve(__dirname, "..") // If in dist/, go up one level
812
+ : path.resolve(__dirname, ".."); // If in src/, go up one level
813
+ const popupScriptPath = path.join(projectRootForPopup, "dist", "popups", "settingsPopup.js");
814
+ // Prepare settings data for popup
815
+ const settingsData = {
816
+ settingDefinitions: SETTING_DEFINITIONS,
817
+ settings: settingsManager.getSettings(),
818
+ globalSettings: settingsManager.getGlobalSettings(),
819
+ projectSettings: settingsManager.getProjectSettings(),
820
+ };
821
+ const settingsJson = JSON.stringify(settingsData);
822
+ // Launch the popup non-blocking and track it
823
+ const popupHandle = launchNodePopupNonBlocking(popupScriptPath, [settingsJson], {
824
+ ...POPUP_POSITIONING.standard(SIDEBAR_WIDTH),
825
+ width: 70,
826
+ height: Math.min(25, SETTING_DEFINITIONS.length + 8),
827
+ title: "⚙️ Settings",
828
+ });
829
+ // Wait for the popup to close
830
+ const result = await popupHandle.resultPromise;
831
+ // Clear active popup tracking
832
+ if (result.success) {
833
+ // Check if this is an action result (action field at top level)
834
+ if (result.action) {
835
+ // Action type setting (like 'hooks')
836
+ if (result.action === "hooks") {
837
+ // Launch hooks popup
838
+ await launchHooksPopup();
839
+ }
840
+ }
841
+ else if (result.data) {
842
+ // Regular setting change (result.data contains the setting)
843
+ const { key, value, scope } = result.data;
844
+ settingsManager.updateSetting(key, value, scope);
845
+ setStatusMessage(`Setting saved (${scope})`);
846
+ setTimeout(() => setStatusMessage(""), 2000);
847
+ }
848
+ }
849
+ else if (result.cancelled) {
850
+ // User pressed ESC - do nothing
851
+ return;
852
+ }
853
+ else if (result.error) {
854
+ setStatusMessage(`Popup error: ${result.error}`);
855
+ setTimeout(() => setStatusMessage(""), 3000);
856
+ }
857
+ }
858
+ catch (error) {
859
+ setStatusMessage(`Failed to launch popup: ${error.message}`);
860
+ setTimeout(() => setStatusMessage(""), 3000);
861
+ }
862
+ };
863
+ const launchMergePopup = async (pane) => {
864
+ LogService.getInstance().debug(`launchMergePopup called for pane: ${pane.slug}`, "MergePopup");
865
+ // Only launch popup if tmux supports it
866
+ if (!popupsSupported) {
867
+ LogService.getInstance().warn("Popups not supported", "MergePopup");
868
+ setStatusMessage("Popups require tmux 3.2+");
869
+ setTimeout(() => setStatusMessage(""), 3000);
870
+ return;
871
+ }
872
+ if (!pane.worktreePath) {
873
+ LogService.getInstance().warn("No worktree path for pane", "MergePopup", pane.id);
874
+ setStatusMessage("This pane has no worktree to merge");
875
+ setTimeout(() => setStatusMessage(""), 3000);
876
+ return;
877
+ }
878
+ try {
879
+ // Resolve the popup script path
880
+ const projectRootForPopup = __dirname.includes("/dist")
881
+ ? path.resolve(__dirname, "..") // If in dist/, go up one level
882
+ : path.resolve(__dirname, ".."); // If in src/, go up one level
883
+ const popupScriptPath = path.join(projectRootForPopup, "dist", "popups", "mergePopup.js");
884
+ LogService.getInstance().debug(`Popup script path: ${popupScriptPath}`, "MergePopup");
885
+ // Check if popup script exists
886
+ try {
887
+ await fs.access(popupScriptPath);
888
+ LogService.getInstance().debug("Popup script exists", "MergePopup");
889
+ }
890
+ catch {
891
+ LogService.getInstance().error(`Popup script NOT found at: ${popupScriptPath}`, "MergePopup");
892
+ setStatusMessage(`Merge popup script not found: ${popupScriptPath}`);
893
+ setTimeout(() => setStatusMessage(""), 5000);
894
+ return;
895
+ }
896
+ // Prepare merge data
897
+ const mainRepoPath = pane.worktreePath.replace(/\/\.dmux\/worktrees\/[^/]+$/, "");
898
+ const mergeData = {
899
+ paneSlug: pane.slug,
900
+ worktreePath: pane.worktreePath,
901
+ mainRepoPath,
902
+ mainBranch: getMainBranch(),
903
+ };
904
+ // Write data to temp file
905
+ const dataFile = `/tmp/dmux-merge-${Date.now()}.json`;
906
+ await fs.writeFile(dataFile, JSON.stringify(mergeData));
907
+ LogService.getInstance().debug(`Merge data written to: ${dataFile}`, "MergePopup");
908
+ LogService.getInstance().debug(`Merge data: ${JSON.stringify(mergeData)}`, "MergePopup");
909
+ // Launch the popup non-blocking and track it
910
+ LogService.getInstance().debug("Launching merge popup...", "MergePopup");
911
+ const popupHandle = launchNodePopupNonBlocking(popupScriptPath, [dataFile], {
912
+ ...POPUP_POSITIONING.large(SIDEBAR_WIDTH, terminalWidth, terminalHeight),
913
+ width: 80,
914
+ height: 30,
915
+ title: `🔀 Merge: ${pane.slug}`,
916
+ });
917
+ LogService.getInstance().debug(`Popup launched, PID: ${popupHandle.pid}`, "MergePopup");
918
+ // Wait for the popup to close
919
+ const result = await popupHandle.resultPromise;
920
+ // Clear active popup tracking
921
+ // Clean up temp file
922
+ try {
923
+ await fs.unlink(dataFile);
924
+ }
925
+ catch { }
926
+ if (result.success && result.data?.merged) {
927
+ if (result.data.closedPane) {
928
+ // Pane was closed during merge, refresh pane list
929
+ await loadPanes();
930
+ setStatusMessage("Merge complete, pane closed");
931
+ }
932
+ else {
933
+ setStatusMessage("Merge complete");
934
+ }
935
+ setTimeout(() => setStatusMessage(""), 3000);
936
+ }
937
+ else if (result.cancelled) {
938
+ // User cancelled
939
+ return;
940
+ }
941
+ else if (result.error) {
942
+ setStatusMessage(`Merge error: ${result.error}`);
943
+ setTimeout(() => setStatusMessage(""), 3000);
944
+ }
945
+ }
946
+ catch (error) {
947
+ setStatusMessage(`Failed to launch merge popup: ${error.message}`);
948
+ setTimeout(() => setStatusMessage(""), 3000);
949
+ }
950
+ };
951
+ const launchChoicePopup = async (title, message, options) => {
952
+ // Only launch popup if tmux supports it
953
+ if (!popupsSupported) {
954
+ setStatusMessage("Popups require tmux 3.2+");
955
+ setTimeout(() => setStatusMessage(""), 3000);
956
+ return null;
957
+ }
958
+ try {
959
+ // Resolve the popup script path
960
+ const projectRootForPopup = __dirname.includes("/dist")
961
+ ? path.resolve(__dirname, "..") // If in dist/, go up one level
962
+ : path.resolve(__dirname, ".."); // If in src/, go up one level
963
+ const popupScriptPath = path.join(projectRootForPopup, "dist", "popups", "choicePopup.js");
964
+ // Write data to temp file to avoid shell escaping issues
965
+ const dataFile = `/tmp/dmux-choice-${Date.now()}.json`;
966
+ const dataJson = JSON.stringify({ title, message, options });
967
+ await fs.writeFile(dataFile, dataJson);
968
+ // Launch the popup non-blocking and track it
969
+ const popupHandle = launchNodePopupNonBlocking(popupScriptPath, [dataFile], {
970
+ ...POPUP_POSITIONING.standard(SIDEBAR_WIDTH),
971
+ width: 70,
972
+ height: Math.min(25, options.length * 3 + 8),
973
+ title: title || "Choose Option",
974
+ });
975
+ // Wait for the popup to close
976
+ const result = await popupHandle.resultPromise;
977
+ // Clear active popup tracking
978
+ // Clean up temp file
979
+ try {
980
+ await fs.unlink(dataFile);
981
+ }
982
+ catch { }
983
+ if (result.success && result.data) {
984
+ return result.data;
985
+ }
986
+ else if (result.cancelled) {
987
+ return null;
988
+ }
989
+ else if (result.error) {
990
+ setStatusMessage(`Popup error: ${result.error}`);
991
+ setTimeout(() => setStatusMessage(""), 3000);
992
+ return null;
993
+ }
994
+ }
995
+ catch (error) {
996
+ setStatusMessage(`Failed to launch popup: ${error.message}`);
997
+ setTimeout(() => setStatusMessage(""), 3000);
998
+ }
999
+ return null;
1000
+ };
1001
+ const launchInputPopup = async (title, message, placeholder, defaultValue) => {
1002
+ // Only launch popup if tmux supports it
1003
+ if (!popupsSupported) {
1004
+ setStatusMessage("Popups require tmux 3.2+");
1005
+ setTimeout(() => setStatusMessage(""), 3000);
1006
+ return null;
1007
+ }
1008
+ try {
1009
+ // Resolve the popup script path
1010
+ const projectRootForPopup = __dirname.includes("/dist")
1011
+ ? path.resolve(__dirname, "..") // If in dist/, go up one level
1012
+ : path.resolve(__dirname, ".."); // If in src/, go up one level
1013
+ const popupScriptPath = path.join(projectRootForPopup, "dist", "popups", "inputPopup.js");
1014
+ // Write data to temp file to avoid shell escaping issues
1015
+ const dataFile = `/tmp/dmux-input-${Date.now()}.json`;
1016
+ const dataJson = JSON.stringify({
1017
+ title,
1018
+ message,
1019
+ placeholder,
1020
+ defaultValue,
1021
+ });
1022
+ await fs.writeFile(dataFile, dataJson);
1023
+ // Launch the popup non-blocking and track it
1024
+ const popupHandle = launchNodePopupNonBlocking(popupScriptPath, [dataFile], {
1025
+ ...POPUP_POSITIONING.standard(SIDEBAR_WIDTH),
1026
+ width: 70,
1027
+ height: 15,
1028
+ title: title || "Input",
1029
+ });
1030
+ // Wait for the popup to close
1031
+ const result = await popupHandle.resultPromise;
1032
+ // Clear active popup tracking
1033
+ // Clean up temp file
1034
+ try {
1035
+ await fs.unlink(dataFile);
1036
+ }
1037
+ catch { }
1038
+ if (result.success && result.data !== undefined) {
1039
+ return result.data;
1040
+ }
1041
+ else if (result.cancelled) {
1042
+ return null;
1043
+ }
1044
+ else if (result.error) {
1045
+ setStatusMessage(`Popup error: ${result.error}`);
1046
+ setTimeout(() => setStatusMessage(""), 3000);
1047
+ return null;
1048
+ }
1049
+ }
1050
+ catch (error) {
1051
+ setStatusMessage(`Failed to launch popup: ${error.message}`);
1052
+ setTimeout(() => setStatusMessage(""), 3000);
1053
+ }
1054
+ return null;
1055
+ };
1056
+ const launchProgressPopup = async (message, type = "info", timeout = 2000) => {
1057
+ // Only launch popup if tmux supports it
1058
+ if (!popupsSupported) {
1059
+ setStatusMessage(message);
1060
+ setTimeout(() => setStatusMessage(""), timeout);
1061
+ return;
1062
+ }
1063
+ try {
1064
+ // Resolve the popup script path
1065
+ const projectRootForPopup = __dirname.includes("/dist")
1066
+ ? path.resolve(__dirname, "..") // If in dist/, go up one level
1067
+ : path.resolve(__dirname, ".."); // If in src/, go up one level
1068
+ const popupScriptPath = path.join(projectRootForPopup, "dist", "popups", "progressPopup.js");
1069
+ // Write data to temp file to avoid shell escaping issues
1070
+ const dataFile = `/tmp/dmux-progress-${Date.now()}.json`;
1071
+ const dataJson = JSON.stringify({ message, type, timeout });
1072
+ await fs.writeFile(dataFile, dataJson);
1073
+ // Launch the popup - position at top, 1 char right of sidebar
1074
+ // Height depends on message length
1075
+ const lines = Math.ceil(message.length / 60) + 3; // Estimate lines needed
1076
+ const titleText = type === "success"
1077
+ ? "✓ Success"
1078
+ : type === "error"
1079
+ ? "✗ Error"
1080
+ : type === "info"
1081
+ ? "ℹ Info"
1082
+ : "Progress";
1083
+ // Launch the popup non-blocking and track it
1084
+ const popupHandle = launchNodePopupNonBlocking(popupScriptPath, [dataFile], {
1085
+ ...POPUP_POSITIONING.standard(SIDEBAR_WIDTH),
1086
+ width: 70,
1087
+ height: Math.min(15, lines + 4),
1088
+ title: titleText,
1089
+ });
1090
+ // Wait for the popup to close
1091
+ await popupHandle.resultPromise;
1092
+ // Clear active popup tracking
1093
+ // Clean up temp file
1094
+ try {
1095
+ await fs.unlink(dataFile);
1096
+ }
1097
+ catch { }
1098
+ }
1099
+ catch (error) {
1100
+ // Fallback to inline message
1101
+ setStatusMessage(message);
1102
+ setTimeout(() => setStatusMessage(""), timeout);
1103
+ }
1104
+ };
1105
+ // Action system - initialized after popup launchers are defined
1106
+ const actionSystem = useActionSystem({
1107
+ panes,
1108
+ savePanes,
1109
+ sessionName,
1110
+ projectName,
1111
+ onPaneRemove: (paneId) => {
1112
+ // Mark the pane as intentionally closed to prevent race condition with worker
1113
+ intentionallyClosedPanes.current.add(paneId);
1114
+ // Remove from panes list
1115
+ const updatedPanes = panes.filter((p) => p.paneId !== paneId);
1116
+ savePanes(updatedPanes);
1117
+ // Clean up after a delay
1118
+ setTimeout(() => {
1119
+ intentionallyClosedPanes.current.delete(paneId);
1120
+ }, 5000);
1121
+ },
1122
+ forceRepaint,
1123
+ popupLaunchers: popupsSupported
1124
+ ? {
1125
+ launchConfirmPopup,
1126
+ launchChoicePopup,
1127
+ launchInputPopup,
1128
+ launchProgressPopup,
1129
+ }
1130
+ : undefined,
1131
+ });
1132
+ // Auto-show new pane dialog removed - users can press 'n' to create panes via popup
1133
+ // Periodic enforcement of control pane size and content pane rebalancing (left sidebar at 40 chars)
1134
+ useEffect(() => {
1135
+ if (!controlPaneId) {
1136
+ return; // No sidebar layout configured
1137
+ }
1138
+ // Enforce sidebar width immediately on mount
1139
+ enforceControlPaneSize(controlPaneId, SIDEBAR_WIDTH);
1140
+ // Debounce resize handler to prevent infinite loops
1141
+ let resizeTimeout = null;
1142
+ let isApplyingLayout = false;
1143
+ const handleResize = () => {
1144
+ // Skip if we're already applying a layout (prevents loops)
1145
+ if (isApplyingLayout) {
1146
+ return;
1147
+ }
1148
+ // Clear any pending resize
1149
+ if (resizeTimeout) {
1150
+ clearTimeout(resizeTimeout);
1151
+ }
1152
+ // Debounce: wait 500ms after last resize event (prevents excessive recalculations)
1153
+ resizeTimeout = setTimeout(() => {
1154
+ // Only enforce if not showing dialogs (to avoid interference)
1155
+ const hasActiveDialog = actionSystem.actionState.showConfirmDialog ||
1156
+ actionSystem.actionState.showChoiceDialog ||
1157
+ actionSystem.actionState.showInputDialog ||
1158
+ actionSystem.actionState.showProgressDialog ||
1159
+ !!showCommandPrompt ||
1160
+ showFileCopyPrompt ||
1161
+ isCreatingPane ||
1162
+ runningCommand ||
1163
+ isUpdating;
1164
+ if (!hasActiveDialog) {
1165
+ isApplyingLayout = true;
1166
+ // Only enforce sidebar width when terminal resizes
1167
+ enforceControlPaneSize(controlPaneId, SIDEBAR_WIDTH);
1168
+ // Force Ink to repaint after resize to prevent blank dmux pane
1169
+ forceRepaint();
1170
+ // Reset flag after a brief delay
1171
+ setTimeout(() => {
1172
+ isApplyingLayout = false;
1173
+ }, 100);
1174
+ }
1175
+ }, 500);
1176
+ };
1177
+ // Listen to stdout resize events
1178
+ process.stdout.on("resize", handleResize);
1179
+ // Also listen for SIGWINCH and SIGUSR1 (tmux hook sends USR1)
1180
+ process.on("SIGWINCH", handleResize);
1181
+ process.on("SIGUSR1", handleResize);
1182
+ return () => {
1183
+ process.stdout.off("resize", handleResize);
1184
+ process.off("SIGWINCH", handleResize);
1185
+ process.off("SIGUSR1", handleResize);
1186
+ if (resizeTimeout) {
1187
+ clearTimeout(resizeTimeout);
1188
+ }
1189
+ };
1190
+ }, [
1191
+ controlPaneId,
1192
+ actionSystem.actionState.showConfirmDialog,
1193
+ actionSystem.actionState.showChoiceDialog,
1194
+ actionSystem.actionState.showInputDialog,
1195
+ actionSystem.actionState.showProgressDialog,
1196
+ showCommandPrompt,
1197
+ showFileCopyPrompt,
1198
+ isCreatingPane,
1199
+ runningCommand,
1200
+ isUpdating,
1201
+ ]);
286
1202
  // Monitor agent status across panes (returns a map of pane ID to status)
287
1203
  const agentStatuses = useAgentStatus({
288
1204
  panes,
289
- suspend: showNewPaneDialog || actionSystem.actionState.showConfirmDialog || actionSystem.actionState.showChoiceDialog || actionSystem.actionState.showInputDialog || actionSystem.actionState.showProgressDialog || !!showCommandPrompt || showFileCopyPrompt,
1205
+ suspend: actionSystem.actionState.showConfirmDialog ||
1206
+ actionSystem.actionState.showChoiceDialog ||
1207
+ actionSystem.actionState.showInputDialog ||
1208
+ actionSystem.actionState.showProgressDialog ||
1209
+ !!showCommandPrompt ||
1210
+ showFileCopyPrompt,
290
1211
  onPaneRemoved: (paneId) => {
291
1212
  // Check if this pane was intentionally closed
292
1213
  // If so, don't re-save - the close action already handled it
@@ -295,28 +1216,21 @@ const DmuxApp = ({ panesFile, projectName, sessionName, settingsFile, projectRoo
295
1216
  }
296
1217
  // Pane was removed unexpectedly (e.g., user killed tmux pane manually)
297
1218
  // Remove it from our tracking
298
- const updatedPanes = panes.filter(p => p.id !== paneId);
1219
+ const updatedPanes = panes.filter((p) => p.id !== paneId);
299
1220
  savePanes(updatedPanes);
300
1221
  },
301
1222
  });
302
- // loadPanes moved to usePanes
303
- // getPanePositions moved to utils/tmux
304
- // Navigation logic moved to hook
305
- const { getCardGridPosition, findCardInDirection } = useNavigation(terminalWidth, panes.length, isLoading);
306
- // findCardInDirection provided by useNavigation
307
- // savePanes moved to usePanes
308
- // applySmartLayout moved to utils/tmux
309
1223
  const createNewPane = async (prompt, agent) => {
310
1224
  setIsCreatingPane(true);
311
- setStatusMessage('Generating slug...');
1225
+ setStatusMessage("Generating slug...");
312
1226
  const slug = await generateSlug(prompt);
313
1227
  setStatusMessage(`Creating worktree: ${slug}...`);
314
1228
  // Get git root directory for consistent worktree placement
315
1229
  let projectRoot;
316
1230
  try {
317
- projectRoot = execSync('git rev-parse --show-toplevel', {
318
- encoding: 'utf-8',
319
- stdio: 'pipe'
1231
+ projectRoot = execSync("git rev-parse --show-toplevel", {
1232
+ encoding: "utf-8",
1233
+ stdio: "pipe",
320
1234
  }).trim();
321
1235
  }
322
1236
  catch {
@@ -324,83 +1238,83 @@ const DmuxApp = ({ panesFile, projectName, sessionName, settingsFile, projectRoo
324
1238
  projectRoot = process.cwd();
325
1239
  }
326
1240
  // Create worktree path inside .dmux/worktrees directory
327
- const worktreePath = path.join(projectRoot, '.dmux', 'worktrees', slug);
1241
+ const worktreePath = path.join(projectRoot, ".dmux", "worktrees", slug);
328
1242
  // Get the original pane ID (where dmux is running) before clearing
329
- const originalPaneId = execSync('tmux display-message -p "#{pane_id}"', { encoding: 'utf-8' }).trim();
330
- // Multiple clearing strategies to prevent artifacts
331
- // 1. Clear screen with ANSI codes
332
- process.stdout.write('\x1b[2J\x1b[H');
333
- // 2. Fill with blank lines to push content off screen
334
- process.stdout.write('\n'.repeat(100));
335
- // 3. Clear tmux history and send clear command
336
- try {
337
- execSync('tmux clear-history', { stdio: 'pipe' });
338
- execSync('tmux send-keys C-l', { stdio: 'pipe' });
339
- }
340
- catch { }
341
- // Wait a bit for clearing to settle
342
- await new Promise(resolve => setTimeout(resolve, 100));
343
- // 4. Force tmux to refresh the display
344
- try {
345
- execSync('tmux refresh-client', { stdio: 'pipe' });
346
- }
347
- catch { }
1243
+ const originalPaneId = execSync('tmux display-message -p "#{pane_id}"', {
1244
+ encoding: "utf-8",
1245
+ }).trim();
1246
+ // Minimal clearing to avoid layout shifts
1247
+ process.stdout.write("\x1b[2J\x1b[H");
348
1248
  // Get current pane count to determine layout
349
- const paneCount = parseInt(execSync('tmux list-panes | wc -l', { encoding: 'utf-8' }).trim());
1249
+ const paneCount = parseInt(execSync("tmux list-panes | wc -l", { encoding: "utf-8" }).trim());
350
1250
  // Enable pane borders to show titles
351
1251
  try {
352
- execSync(`tmux set-option -g pane-border-status top`, { stdio: 'pipe' });
1252
+ execSync(`tmux set-option -g pane-border-status top`, { stdio: "pipe" });
353
1253
  }
354
1254
  catch {
355
1255
  // Ignore if already set or fails
356
1256
  }
357
1257
  // Create new pane
358
- const paneInfo = execSync(`tmux split-window -h -P -F '#{pane_id}'`, { encoding: 'utf-8' }).trim();
1258
+ const paneInfo = execSync(`tmux split-window -h -P -F '#{pane_id}'`, {
1259
+ encoding: "utf-8",
1260
+ }).trim();
359
1261
  // Wait for pane creation to settle
360
- await new Promise(resolve => setTimeout(resolve, 500));
1262
+ await new Promise((resolve) => setTimeout(resolve, 500));
361
1263
  // Set pane title to match the slug
362
1264
  try {
363
- execSync(`tmux select-pane -t '${paneInfo}' -T "${slug}"`, { stdio: 'pipe' });
1265
+ execSync(`tmux select-pane -t '${paneInfo}' -T "${slug}"`, {
1266
+ stdio: "pipe",
1267
+ });
364
1268
  }
365
1269
  catch {
366
1270
  // Ignore if setting title fails
367
1271
  }
368
- // Apply smart layout based on pane count
369
- const newPaneCount = paneCount + 1;
370
- applySmartLayout(newPaneCount);
1272
+ // Don't apply global layouts - let content panes arrange naturally
1273
+ // Only enforce sidebar width
1274
+ try {
1275
+ const controlPaneId = execSync('tmux display-message -p "#{pane_id}"', {
1276
+ encoding: "utf-8",
1277
+ }).trim();
1278
+ enforceControlPaneSize(controlPaneId, SIDEBAR_WIDTH);
1279
+ }
1280
+ catch { }
371
1281
  // Create git worktree and cd into it
372
1282
  // This MUST happen before launching Claude to ensure we're in the right directory
373
1283
  try {
374
1284
  // First, create the worktree and cd into it as a single command
375
1285
  // Use ; instead of && to ensure cd runs even if worktree already exists
376
1286
  const worktreeCmd = `git worktree add "${worktreePath}" -b ${slug} 2>/dev/null ; cd "${worktreePath}"`;
377
- execSync(`tmux send-keys -t '${paneInfo}' '${worktreeCmd}' Enter`, { stdio: 'pipe' });
1287
+ execSync(`tmux send-keys -t '${paneInfo}' '${worktreeCmd}' Enter`, {
1288
+ stdio: "pipe",
1289
+ });
378
1290
  // Wait longer for worktree creation and cd to complete
379
1291
  // This is critical - if we don't wait long enough, Claude will start in the wrong directory
380
- await new Promise(resolve => setTimeout(resolve, 2500));
381
- // Verify we're in the worktree directory by sending pwd command
382
- execSync(`tmux send-keys -t '${paneInfo}' 'echo "Worktree created at:" && pwd' Enter`, { stdio: 'pipe' });
383
- await new Promise(resolve => setTimeout(resolve, 500));
384
- setStatusMessage(agent ? `Worktree created, launching ${agent === 'opencode' ? 'opencode' : 'Claude'}...` : 'Worktree created.');
1292
+ await new Promise((resolve) => setTimeout(resolve, 2500));
1293
+ // Verify we're in the worktree directory by sending pwd command
1294
+ execSync(`tmux send-keys -t '${paneInfo}' 'echo "Worktree created at:" && pwd' Enter`, { stdio: "pipe" });
1295
+ await new Promise((resolve) => setTimeout(resolve, 500));
1296
+ setStatusMessage(agent
1297
+ ? `Worktree created, launching ${agent === "opencode" ? "opencode" : "Claude"}...`
1298
+ : "Worktree created.");
385
1299
  }
386
1300
  catch (error) {
387
1301
  // Log error but continue - worktree creation is essential
388
1302
  setStatusMessage(`Warning: Worktree issue: ${error}`);
389
1303
  // Even if worktree creation failed, try to cd to the directory in case it exists
390
- execSync(`tmux send-keys -t '${paneInfo}' 'cd "${worktreePath}" 2>/dev/null || (echo "ERROR: Failed to create/enter worktree ${slug}" && pwd)' Enter`, { stdio: 'pipe' });
391
- await new Promise(resolve => setTimeout(resolve, 1000));
1304
+ execSync(`tmux send-keys -t '${paneInfo}' 'cd "${worktreePath}" 2>/dev/null || (echo "ERROR: Failed to create/enter worktree ${slug}" && pwd)' Enter`, { stdio: "pipe" });
1305
+ await new Promise((resolve) => setTimeout(resolve, 1000));
392
1306
  }
393
1307
  // Prepare and send the agent command
394
- let escapedCmd = '';
395
- if (agent === 'claude') {
1308
+ let escapedCmd = "";
1309
+ if (agent === "claude") {
396
1310
  // Claude should always be launched AFTER we're in the worktree directory
397
1311
  let claudeCmd;
398
1312
  if (prompt && prompt.trim()) {
399
1313
  const escapedPrompt = prompt
400
- .replace(/\\/g, '\\\\')
1314
+ .replace(/\\/g, "\\\\")
401
1315
  .replace(/"/g, '\\"')
402
- .replace(/`/g, '\\`')
403
- .replace(/\$/g, '\\$');
1316
+ .replace(/`/g, "\\`")
1317
+ .replace(/\$/g, "\\$");
404
1318
  claudeCmd = `claude "${escapedPrompt}" --permission-mode=acceptEdits`;
405
1319
  }
406
1320
  else {
@@ -408,34 +1322,42 @@ const DmuxApp = ({ panesFile, projectName, sessionName, settingsFile, projectRoo
408
1322
  }
409
1323
  // Send Claude command to new pane
410
1324
  escapedCmd = claudeCmd.replace(/'/g, "'\\''");
411
- execSync(`tmux send-keys -t '${paneInfo}' '${escapedCmd}'`, { stdio: 'pipe' });
412
- execSync(`tmux send-keys -t '${paneInfo}' Enter`, { stdio: 'pipe' });
1325
+ execSync(`tmux send-keys -t '${paneInfo}' '${escapedCmd}'`, {
1326
+ stdio: "pipe",
1327
+ });
1328
+ execSync(`tmux send-keys -t '${paneInfo}' Enter`, { stdio: "pipe" });
413
1329
  }
414
- else if (agent === 'opencode') {
1330
+ else if (agent === "opencode") {
415
1331
  // opencode: start the TUI, then paste the prompt and submit
416
1332
  const openCoderCmd = `opencode`;
417
1333
  const escapedOpenCmd = openCoderCmd.replace(/'/g, "'\\''");
418
- execSync(`tmux send-keys -t '${paneInfo}' '${escapedOpenCmd}'`, { stdio: 'pipe' });
419
- execSync(`tmux send-keys -t '${paneInfo}' Enter`, { stdio: 'pipe' });
1334
+ execSync(`tmux send-keys -t '${paneInfo}' '${escapedOpenCmd}'`, {
1335
+ stdio: "pipe",
1336
+ });
1337
+ execSync(`tmux send-keys -t '${paneInfo}' Enter`, { stdio: "pipe" });
420
1338
  if (prompt && prompt.trim()) {
421
- await new Promise(resolve => setTimeout(resolve, 1500));
1339
+ await new Promise((resolve) => setTimeout(resolve, 1500));
422
1340
  const bufName = `dmux_prompt_${Date.now()}`;
423
- const promptEsc = prompt.replace(/\\/g, '\\\\').replace(/'/g, "'\\''");
424
- execSync(`tmux set-buffer -b '${bufName}' -- '${promptEsc}'`, { stdio: 'pipe' });
425
- execSync(`tmux paste-buffer -b '${bufName}' -t '${paneInfo}'`, { stdio: 'pipe' });
426
- await new Promise(resolve => setTimeout(resolve, 200));
427
- execSync(`tmux delete-buffer -b '${bufName}'`, { stdio: 'pipe' });
428
- execSync(`tmux send-keys -t '${paneInfo}' Enter`, { stdio: 'pipe' });
1341
+ const promptEsc = prompt.replace(/\\/g, "\\\\").replace(/'/g, "'\\''");
1342
+ execSync(`tmux set-buffer -b '${bufName}' -- '${promptEsc}'`, {
1343
+ stdio: "pipe",
1344
+ });
1345
+ execSync(`tmux paste-buffer -b '${bufName}' -t '${paneInfo}'`, {
1346
+ stdio: "pipe",
1347
+ });
1348
+ await new Promise((resolve) => setTimeout(resolve, 200));
1349
+ execSync(`tmux delete-buffer -b '${bufName}'`, { stdio: "pipe" });
1350
+ execSync(`tmux send-keys -t '${paneInfo}' Enter`, { stdio: "pipe" });
429
1351
  }
430
1352
  }
431
- if (agent === 'claude') {
1353
+ if (agent === "claude") {
432
1354
  // Monitor for Claude Code trust prompt and auto-respond
433
1355
  const autoApproveTrust = async () => {
434
1356
  // Wait for Claude to start up before checking for prompts
435
- await new Promise(resolve => setTimeout(resolve, 800));
1357
+ await new Promise((resolve) => setTimeout(resolve, 800));
436
1358
  const maxChecks = 100; // 100 checks * 100ms = 10 seconds total
437
1359
  const checkInterval = 100; // Check every 100ms
438
- let lastContent = '';
1360
+ let lastContent = "";
439
1361
  let stableContentCount = 0;
440
1362
  let promptHandled = false;
441
1363
  // More comprehensive trust prompt patterns
@@ -464,14 +1386,15 @@ const DmuxApp = ({ panesFile, projectName, sessionName, settingsFile, projectRoo
464
1386
  /❯\s*1\.\s*Yes,\s*proceed/i, // New Claude numbered menu format
465
1387
  /Enter to confirm.*Esc to exit/i, // New Claude confirmation format
466
1388
  /1\.\s*Yes,\s*proceed/i, // Yes proceed option
467
- /2\.\s*No,\s*exit/i // No exit option
1389
+ /2\.\s*No,\s*exit/i, // No exit option
468
1390
  ];
469
1391
  for (let i = 0; i < maxChecks; i++) {
470
- await new Promise(resolve => setTimeout(resolve, checkInterval));
1392
+ await new Promise((resolve) => setTimeout(resolve, checkInterval));
471
1393
  try {
472
1394
  // Capture the pane content
473
1395
  const paneContent = capturePaneContent(paneInfo, 30);
474
- if (i % 10 === 0) { // Log every 10 checks (every second)
1396
+ if (i % 10 === 0) {
1397
+ // Log every 10 checks (every second)
475
1398
  }
476
1399
  // Check if content has stabilized (same for 3 checks = prompt is waiting)
477
1400
  if (paneContent === lastContent) {
@@ -482,14 +1405,15 @@ const DmuxApp = ({ panesFile, projectName, sessionName, settingsFile, projectRoo
482
1405
  lastContent = paneContent;
483
1406
  }
484
1407
  // Look for trust prompt in the current content
485
- const hasTrustPrompt = trustPromptPatterns.some(pattern => pattern.test(paneContent));
1408
+ const hasTrustPrompt = trustPromptPatterns.some((pattern) => pattern.test(paneContent));
486
1409
  // Also check if we see specific Claude permission text
487
- const hasClaudePermissionPrompt = paneContent.includes('Do you trust') ||
488
- paneContent.includes('trust the files') ||
489
- paneContent.includes('permission') ||
490
- paneContent.includes('allow') ||
491
- (paneContent.includes('folder') && paneContent.includes('?'));
492
- if ((hasTrustPrompt || hasClaudePermissionPrompt) && !promptHandled) {
1410
+ const hasClaudePermissionPrompt = paneContent.includes("Do you trust") ||
1411
+ paneContent.includes("trust the files") ||
1412
+ paneContent.includes("permission") ||
1413
+ paneContent.includes("allow") ||
1414
+ (paneContent.includes("folder") && paneContent.includes("?"));
1415
+ if ((hasTrustPrompt || hasClaudePermissionPrompt) &&
1416
+ !promptHandled) {
493
1417
  // Content is stable and we found a prompt
494
1418
  if (stableContentCount >= 2) {
495
1419
  // Check if this is the new Claude numbered menu format
@@ -497,44 +1421,57 @@ const DmuxApp = ({ panesFile, projectName, sessionName, settingsFile, projectRoo
497
1421
  /Enter to confirm.*Esc to exit/i.test(paneContent);
498
1422
  if (isNewClaudeFormat) {
499
1423
  // For new Claude format, just press Enter to confirm default "Yes, proceed"
500
- execSync(`tmux send-keys -t '${paneInfo}' Enter`, { stdio: 'pipe' });
1424
+ execSync(`tmux send-keys -t '${paneInfo}' Enter`, {
1425
+ stdio: "pipe",
1426
+ });
501
1427
  }
502
1428
  else {
503
1429
  // Try multiple response methods for older formats
504
1430
  // Method 1: Send 'y' followed by Enter (most explicit)
505
- execSync(`tmux send-keys -t '${paneInfo}' 'y'`, { stdio: 'pipe' });
506
- await new Promise(resolve => setTimeout(resolve, 50));
507
- execSync(`tmux send-keys -t '${paneInfo}' Enter`, { stdio: 'pipe' });
1431
+ execSync(`tmux send-keys -t '${paneInfo}' 'y'`, {
1432
+ stdio: "pipe",
1433
+ });
1434
+ await new Promise((resolve) => setTimeout(resolve, 50));
1435
+ execSync(`tmux send-keys -t '${paneInfo}' Enter`, {
1436
+ stdio: "pipe",
1437
+ });
508
1438
  // Method 2: Just Enter (if it's a yes/no with default yes)
509
- await new Promise(resolve => setTimeout(resolve, 100));
510
- execSync(`tmux send-keys -t '${paneInfo}' Enter`, { stdio: 'pipe' });
1439
+ await new Promise((resolve) => setTimeout(resolve, 100));
1440
+ execSync(`tmux send-keys -t '${paneInfo}' Enter`, {
1441
+ stdio: "pipe",
1442
+ });
511
1443
  }
512
1444
  // Mark as handled to avoid duplicate responses
513
1445
  promptHandled = true;
514
1446
  // Wait and check if prompt is gone
515
- await new Promise(resolve => setTimeout(resolve, 500));
1447
+ await new Promise((resolve) => setTimeout(resolve, 500));
516
1448
  // Verify the prompt is gone
517
1449
  const updatedContent = capturePaneContent(paneInfo, 10);
518
1450
  // If trust prompt is gone, check if we need to resend the Claude command
519
- const promptGone = !trustPromptPatterns.some(p => p.test(updatedContent));
1451
+ const promptGone = !trustPromptPatterns.some((p) => p.test(updatedContent));
520
1452
  if (promptGone) {
521
1453
  // Check if Claude is running or if we need to restart it
522
- const claudeRunning = updatedContent.includes('Claude') ||
523
- updatedContent.includes('claude') ||
524
- updatedContent.includes('Assistant') ||
525
- (prompt && updatedContent.includes(prompt.substring(0, Math.min(20, prompt.length))));
526
- if (!claudeRunning && !updatedContent.includes('$')) {
527
- await new Promise(resolve => setTimeout(resolve, 300));
528
- execSync(`tmux send-keys -t '${paneInfo}' '${escapedCmd}'`, { stdio: 'pipe' });
529
- execSync(`tmux send-keys -t '${paneInfo}' Enter`, { stdio: 'pipe' });
1454
+ const claudeRunning = updatedContent.includes("Claude") ||
1455
+ updatedContent.includes("claude") ||
1456
+ updatedContent.includes("Assistant") ||
1457
+ (prompt &&
1458
+ updatedContent.includes(prompt.substring(0, Math.min(20, prompt.length))));
1459
+ if (!claudeRunning && !updatedContent.includes("$")) {
1460
+ await new Promise((resolve) => setTimeout(resolve, 300));
1461
+ execSync(`tmux send-keys -t '${paneInfo}' '${escapedCmd}'`, { stdio: "pipe" });
1462
+ execSync(`tmux send-keys -t '${paneInfo}' Enter`, {
1463
+ stdio: "pipe",
1464
+ });
530
1465
  }
531
1466
  break;
532
1467
  }
533
1468
  }
534
1469
  }
535
1470
  // If we see Claude is already running without prompts, we're done
536
- if (!hasTrustPrompt && !hasClaudePermissionPrompt &&
537
- (paneContent.includes('Claude') || paneContent.includes('Assistant'))) {
1471
+ if (!hasTrustPrompt &&
1472
+ !hasClaudePermissionPrompt &&
1473
+ (paneContent.includes("Claude") ||
1474
+ paneContent.includes("Assistant"))) {
538
1475
  break;
539
1476
  }
540
1477
  }
@@ -544,37 +1481,35 @@ const DmuxApp = ({ panesFile, projectName, sessionName, settingsFile, projectRoo
544
1481
  }
545
1482
  };
546
1483
  // Start monitoring for trust prompt in background
547
- autoApproveTrust().catch(err => {
548
- });
1484
+ autoApproveTrust().catch((err) => { });
549
1485
  }
550
1486
  // Keep focus on the new pane
551
- execSync(`tmux select-pane -t '${paneInfo}'`, { stdio: 'pipe' });
1487
+ execSync(`tmux select-pane -t '${paneInfo}'`, { stdio: "pipe" });
552
1488
  // Save pane info
553
1489
  const newPane = {
554
1490
  id: `dmux-${Date.now()}`,
555
1491
  slug,
556
- prompt: prompt || 'No initial prompt',
1492
+ prompt: prompt || "No initial prompt",
557
1493
  paneId: paneInfo,
558
1494
  worktreePath,
559
- agent
1495
+ agent,
560
1496
  };
561
1497
  const updatedPanes = [...panes, newPane];
562
1498
  await savePanes(updatedPanes);
563
1499
  // Switch back to the original pane (where dmux is running)
564
- execSync(`tmux select-pane -t '${originalPaneId}'`, { stdio: 'pipe' });
1500
+ execSync(`tmux select-pane -t '${originalPaneId}'`, { stdio: "pipe" });
565
1501
  // Re-set the title for the dmux pane
566
1502
  try {
567
- execSync(`tmux select-pane -t '${originalPaneId}' -T "dmux-${projectName}"`, { stdio: 'pipe' });
1503
+ execSync(`tmux select-pane -t '${originalPaneId}' -T "dmux v${packageJson.version} - ${projectName}"`, { stdio: "pipe" });
568
1504
  }
569
1505
  catch {
570
1506
  // Ignore if setting title fails
571
1507
  }
572
1508
  // Clear the screen and redraw the UI
573
- process.stdout.write('\x1b[2J\x1b[H');
1509
+ process.stdout.write("\x1b[2J\x1b[H");
574
1510
  // Reset the creating pane flag and refresh
575
1511
  setIsCreatingPane(false);
576
- setStatusMessage('');
577
- setNewPanePrompt('');
1512
+ setStatusMessage("");
578
1513
  // Force a reload of panes to ensure UI is up to date
579
1514
  await loadPanes();
580
1515
  };
@@ -582,30 +1517,32 @@ const DmuxApp = ({ panesFile, projectName, sessionName, settingsFile, projectRoo
582
1517
  try {
583
1518
  // Enable pane borders to show titles (if not already enabled)
584
1519
  try {
585
- execSync(`tmux set-option -g pane-border-status top`, { stdio: 'pipe' });
1520
+ execSync(`tmux set-option -g pane-border-status top`, { stdio: "pipe" });
586
1521
  }
587
1522
  catch {
588
1523
  // Ignore if already set or fails
589
1524
  }
590
- execSync(`tmux select-pane -t '${paneId}'`, { stdio: 'pipe' });
1525
+ execSync(`tmux select-pane -t '${paneId}'`, { stdio: "pipe" });
591
1526
  // Clear screen after jump to remove artifacts
592
1527
  clearScreen();
593
- setStatusMessage('Jumped to pane');
594
- setTimeout(() => setStatusMessage(''), 2000);
1528
+ setStatusMessage("Jumped to pane");
1529
+ setTimeout(() => setStatusMessage(""), 2000);
595
1530
  }
596
1531
  catch {
597
- setStatusMessage('Failed to jump - pane may be closed');
598
- setTimeout(() => setStatusMessage(''), 2000);
1532
+ setStatusMessage("Failed to jump - pane may be closed");
1533
+ setTimeout(() => setStatusMessage(""), 2000);
599
1534
  }
600
1535
  };
601
1536
  const runCommand = async (type, pane) => {
602
1537
  if (!pane.worktreePath) {
603
- setStatusMessage('No worktree path for this pane');
604
- setTimeout(() => setStatusMessage(''), 2000);
1538
+ setStatusMessage("No worktree path for this pane");
1539
+ setTimeout(() => setStatusMessage(""), 2000);
605
1540
  return;
606
1541
  }
607
- const command = type === 'test' ? projectSettings.testCommand : projectSettings.devCommand;
608
- const isFirstRun = type === 'test' ? !projectSettings.firstTestRun : !projectSettings.firstDevRun;
1542
+ const command = type === "test" ? projectSettings.testCommand : projectSettings.devCommand;
1543
+ const isFirstRun = type === "test"
1544
+ ? !projectSettings.firstTestRun
1545
+ : !projectSettings.firstDevRun;
609
1546
  if (!command) {
610
1547
  // No command configured, prompt user
611
1548
  setShowCommandPrompt(type);
@@ -624,35 +1561,37 @@ const DmuxApp = ({ panesFile, projectName, sessionName, settingsFile, projectRoo
624
1561
  setRunningCommand(true);
625
1562
  setStatusMessage(`Starting ${type} in background window...`);
626
1563
  // Kill existing window if present
627
- const existingWindowId = type === 'test' ? pane.testWindowId : pane.devWindowId;
1564
+ const existingWindowId = type === "test" ? pane.testWindowId : pane.devWindowId;
628
1565
  if (existingWindowId) {
629
1566
  try {
630
- execSync(`tmux kill-window -t '${existingWindowId}'`, { stdio: 'pipe' });
1567
+ execSync(`tmux kill-window -t '${existingWindowId}'`, {
1568
+ stdio: "pipe",
1569
+ });
631
1570
  }
632
1571
  catch { }
633
1572
  }
634
1573
  // Create a new background window for the command
635
1574
  const windowName = `${pane.slug}-${type}`;
636
- const windowId = execSync(`tmux new-window -d -n '${windowName}' -P -F '#{window_id}'`, { encoding: 'utf-8', stdio: 'pipe' }).trim();
1575
+ const windowId = execSync(`tmux new-window -d -n '${windowName}' -P -F '#{window_id}'`, { encoding: "utf-8", stdio: "pipe" }).trim();
637
1576
  // Create a log file to capture output
638
1577
  const logFile = `/tmp/dmux-${pane.id}-${type}.log`;
639
1578
  // Build the command with output capture
640
- const fullCommand = type === 'test'
1579
+ const fullCommand = type === "test"
641
1580
  ? `cd "${pane.worktreePath}" && ${command} 2>&1 | tee ${logFile}`
642
1581
  : `cd "${pane.worktreePath}" && ${command} 2>&1 | tee ${logFile}`;
643
1582
  // Send the command to the new window
644
- execSync(`tmux send-keys -t '${windowId}' '${fullCommand.replace(/'/g, "'\\''")}'`, { stdio: 'pipe' });
645
- execSync(`tmux send-keys -t '${windowId}' Enter`, { stdio: 'pipe' });
1583
+ execSync(`tmux send-keys -t '${windowId}' '${fullCommand.replace(/'/g, "'\\''")}'`, { stdio: "pipe" });
1584
+ execSync(`tmux send-keys -t '${windowId}' Enter`, { stdio: "pipe" });
646
1585
  // Update pane with window info
647
1586
  const updatedPane = {
648
1587
  ...pane,
649
- [type === 'test' ? 'testWindowId' : 'devWindowId']: windowId,
650
- [type === 'test' ? 'testStatus' : 'devStatus']: 'running'
1588
+ [type === "test" ? "testWindowId" : "devWindowId"]: windowId,
1589
+ [type === "test" ? "testStatus" : "devStatus"]: "running",
651
1590
  };
652
- const updatedPanes = panes.map(p => p.id === pane.id ? updatedPane : p);
1591
+ const updatedPanes = panes.map((p) => p.id === pane.id ? updatedPane : p);
653
1592
  await savePanes(updatedPanes);
654
1593
  // Start monitoring the output
655
- if (type === 'test') {
1594
+ if (type === "test") {
656
1595
  // For tests, monitor for completion
657
1596
  setTimeout(() => monitorTestOutput(pane.id, logFile), 2000);
658
1597
  }
@@ -661,13 +1600,13 @@ const DmuxApp = ({ panesFile, projectName, sessionName, settingsFile, projectRoo
661
1600
  setTimeout(() => monitorDevOutput(pane.id, logFile), 2000);
662
1601
  }
663
1602
  setRunningCommand(false);
664
- setStatusMessage(`${type === 'test' ? 'Test' : 'Dev server'} started in background`);
665
- setTimeout(() => setStatusMessage(''), 3000);
1603
+ setStatusMessage(`${type === "test" ? "Test" : "Dev server"} started in background`);
1604
+ setTimeout(() => setStatusMessage(""), 3000);
666
1605
  }
667
1606
  catch (error) {
668
1607
  setRunningCommand(false);
669
1608
  setStatusMessage(`Failed to run ${type} command`);
670
- setTimeout(() => setStatusMessage(''), 3000);
1609
+ setTimeout(() => setStatusMessage(""), 3000);
671
1610
  }
672
1611
  };
673
1612
  // Update handling moved to useAutoUpdater
@@ -675,50 +1614,58 @@ const DmuxApp = ({ panesFile, projectName, sessionName, settingsFile, projectRoo
675
1614
  const clearScreen = () => {
676
1615
  // CRITICAL: Force Ink to re-render FIRST, before clearing
677
1616
  // This prevents blank screen by ensuring React starts rendering immediately
678
- setForceRepaintTrigger(prev => prev + 1);
1617
+ setForceRepaintTrigger((prev) => prev + 1);
679
1618
  // Multiple clearing strategies to prevent artifacts
680
1619
  // 1. Clear screen with ANSI codes
681
- process.stdout.write('\x1b[2J\x1b[H');
1620
+ process.stdout.write("\x1b[2J\x1b[H");
682
1621
  // 2. Clear tmux history
683
1622
  try {
684
- execSync('tmux clear-history', { stdio: 'pipe' });
1623
+ execSync("tmux clear-history", { stdio: "pipe" });
685
1624
  }
686
1625
  catch { }
687
1626
  // 3. Force tmux to refresh the display
688
1627
  try {
689
- execSync('tmux refresh-client', { stdio: 'pipe' });
1628
+ execSync("tmux refresh-client", { stdio: "pipe" });
690
1629
  }
691
1630
  catch { }
692
1631
  };
693
1632
  // Cleanup function for exit
694
1633
  const cleanExit = () => {
695
1634
  // Clear screen before exiting Ink
696
- process.stdout.write('\x1b[2J\x1b[3J\x1b[H');
1635
+ process.stdout.write("\x1b[2J\x1b[3J\x1b[H");
697
1636
  // Exit the Ink app (this cleans up the React tree)
698
1637
  exit();
699
1638
  // Give Ink a moment to clean up its rendering, then do final cleanup
700
1639
  setTimeout(() => {
701
1640
  // Multiple aggressive clearing strategies
702
- process.stdout.write('\x1b[2J\x1b[H'); // Clear screen and move cursor to home
703
- process.stdout.write('\x1b[3J'); // Clear scrollback buffer
704
- process.stdout.write('\x1b[0m'); // Reset all attributes
1641
+ process.stdout.write("\x1b[2J\x1b[H"); // Clear screen and move cursor to home
1642
+ process.stdout.write("\x1b[3J"); // Clear scrollback buffer
1643
+ process.stdout.write("\x1b[0m"); // Reset all attributes
705
1644
  // Clear tmux history and pane
706
1645
  try {
707
- execSync('tmux clear-history', { stdio: 'pipe' });
708
- execSync('tmux send-keys C-l', { stdio: 'pipe' });
1646
+ execSync("tmux clear-history", { stdio: "pipe" });
1647
+ execSync("tmux send-keys C-l", { stdio: "pipe" });
709
1648
  }
710
1649
  catch { }
711
1650
  // One more final clear
712
- process.stdout.write('\x1b[2J\x1b[H');
1651
+ process.stdout.write("\x1b[2J\x1b[H");
713
1652
  // Show clean goodbye message
714
- process.stdout.write('\n Run dmux again to resume. Goodbye 👋\n\n');
1653
+ process.stdout.write("\n Run dmux again to resume. Goodbye 👋\n\n");
715
1654
  // Exit process
716
1655
  process.exit(0);
717
1656
  }, 100);
718
1657
  };
719
1658
  useInput(async (input, key) => {
1659
+ const logService = LogService.getInstance();
1660
+ // Log all input for debugging (only first 50 chars to avoid spam)
1661
+ const inputPreview = input.length > 50 ? input.substring(0, 50) + "..." : input;
1662
+ logService.debug(`Input: "${inputPreview}"`, "InputDebug");
1663
+ // Ignore input temporarily after popup operations (prevents buffered keys from being processed)
1664
+ if (ignoreInput) {
1665
+ return;
1666
+ }
720
1667
  // Handle Ctrl+C for quit confirmation (must be first, before any other checks)
721
- if (key.ctrl && input === 'c') {
1668
+ if (key.ctrl && input === "c") {
722
1669
  if (quitConfirmMode) {
723
1670
  // Second Ctrl+C - actually quit
724
1671
  cleanExit();
@@ -733,45 +1680,10 @@ const DmuxApp = ({ panesFile, projectName, sessionName, settingsFile, projectRoo
733
1680
  }
734
1681
  return;
735
1682
  }
736
- if (isCreatingPane || runningCommand || isUpdating || isLoading || isCreatingTunnel) {
1683
+ if (isCreatingPane || runningCommand || isUpdating || isLoading) {
737
1684
  // Disable input while performing operations or loading
738
1685
  return;
739
1686
  }
740
- // Handle kebab menu navigation
741
- if (showKebabMenu && kebabMenuPaneIndex !== null) {
742
- const currentPane = panes[kebabMenuPaneIndex];
743
- const availableActions = kebabMenuActions;
744
- if (key.escape) {
745
- setShowKebabMenu(false);
746
- setKebabMenuPaneIndex(null);
747
- setKebabMenuOption(0);
748
- setKebabMenuActions([]);
749
- return;
750
- }
751
- else if (key.upArrow) {
752
- setKebabMenuOption(Math.max(0, kebabMenuOption - 1));
753
- return;
754
- }
755
- else if (key.downArrow) {
756
- setKebabMenuOption(Math.min(availableActions.length - 1, kebabMenuOption + 1));
757
- return;
758
- }
759
- else if (key.return) {
760
- // Execute the selected menu action
761
- setShowKebabMenu(false);
762
- const selectedAction = availableActions[kebabMenuOption];
763
- if (selectedAction) {
764
- // Use the action system to execute
765
- actionSystem.executeAction(selectedAction.id, currentPane, { mainBranch: getMainBranch() });
766
- }
767
- setKebabMenuPaneIndex(null);
768
- setKebabMenuOption(0);
769
- setKebabMenuActions([]);
770
- return;
771
- }
772
- // Don't process other inputs while menu is open
773
- return;
774
- }
775
1687
  // Handle quit confirm mode - ESC cancels it
776
1688
  if (quitConfirmMode) {
777
1689
  if (key.escape) {
@@ -780,288 +1692,8 @@ const DmuxApp = ({ panesFile, projectName, sessionName, settingsFile, projectRoo
780
1692
  }
781
1693
  // Allow other inputs to continue (don't return early)
782
1694
  }
783
- // Handle QR code view
784
- if (showQRCode) {
785
- if (key.escape) {
786
- setShowQRCode(false);
787
- }
788
- return;
789
- }
790
- // Handle hooks dialog
791
- if (showHooksDialog) {
792
- if (key.escape) {
793
- setShowHooksDialog(false);
794
- setHooksSelectedIndex(0);
795
- // Go back to settings dialog
796
- setShowSettingsDialog(true);
797
- setSettingsMode('list');
798
- return;
799
- }
800
- else if (key.upArrow) {
801
- setHooksSelectedIndex(Math.max(0, hooksSelectedIndex - 1));
802
- return;
803
- }
804
- else if (key.downArrow) {
805
- setHooksSelectedIndex(Math.min(hooksData.length - 1, hooksSelectedIndex + 1));
806
- return;
807
- }
808
- else if (input === 'e') {
809
- // Edit hooks using an agent
810
- setShowHooksDialog(false);
811
- setHooksSelectedIndex(0);
812
- const prompt = "I would like to edit my dmux hooks in .dmux-hooks, please read the instructions in there and ask me what I want to edit";
813
- setPendingPrompt(prompt);
814
- setNewPanePrompt(prompt);
815
- // Choose agent
816
- const agents = availableAgents;
817
- if (agents.length === 0) {
818
- createNewPaneHook(prompt);
819
- }
820
- else if (agents.length === 1) {
821
- createNewPaneHook(prompt, agents[0]);
822
- }
823
- else {
824
- setShowAgentChoiceDialog(true);
825
- setAgentChoice(agentChoice || 'claude');
826
- }
827
- return;
828
- }
829
- return;
830
- }
831
- // Handle settings dialog
832
- if (showSettingsDialog) {
833
- if (key.escape) {
834
- if (settingsMode === 'list') {
835
- // Close settings dialog
836
- setShowSettingsDialog(false);
837
- setSettingsMode('list');
838
- setSettingsSelectedIndex(0);
839
- setSettingsEditingKey(undefined);
840
- }
841
- else {
842
- // Go back to list
843
- setSettingsMode('list');
844
- setSettingsEditingKey(undefined);
845
- setSettingsEditingValueIndex(0);
846
- setSettingsScopeIndex(0);
847
- }
848
- return;
849
- }
850
- else if (key.upArrow) {
851
- if (settingsMode === 'list') {
852
- setSettingsSelectedIndex(Math.max(0, settingsSelectedIndex - 1));
853
- }
854
- else if (settingsMode === 'edit') {
855
- const currentDef = SETTING_DEFINITIONS[settingsSelectedIndex];
856
- const maxIndex = currentDef.type === 'boolean' ? 1 : (currentDef.options?.length || 1) - 1;
857
- setSettingsEditingValueIndex(Math.max(0, settingsEditingValueIndex - 1));
858
- }
859
- else if (settingsMode === 'scope') {
860
- setSettingsScopeIndex(Math.max(0, settingsScopeIndex - 1));
861
- }
862
- return;
863
- }
864
- else if (key.downArrow) {
865
- if (settingsMode === 'list') {
866
- setSettingsSelectedIndex(Math.min(SETTING_DEFINITIONS.length - 1, settingsSelectedIndex + 1));
867
- }
868
- else if (settingsMode === 'edit') {
869
- const currentDef = SETTING_DEFINITIONS[settingsSelectedIndex];
870
- const maxIndex = currentDef.type === 'boolean' ? 1 : (currentDef.options?.length || 1) - 1;
871
- setSettingsEditingValueIndex(Math.min(maxIndex, settingsEditingValueIndex + 1));
872
- }
873
- else if (settingsMode === 'scope') {
874
- setSettingsScopeIndex(Math.min(1, settingsScopeIndex + 1));
875
- }
876
- return;
877
- }
878
- else if (key.return) {
879
- if (settingsMode === 'list') {
880
- const currentDef = SETTING_DEFINITIONS[settingsSelectedIndex];
881
- // Handle action type - trigger the action instead of editing
882
- if (currentDef.type === 'action') {
883
- if (currentDef.key === 'hooks') {
884
- // Show hooks dialog
885
- setShowSettingsDialog(false);
886
- const { hasHook } = await import('./utils/hooks.js');
887
- const allHookTypes = [
888
- 'before_pane_create',
889
- 'pane_created',
890
- 'worktree_created',
891
- 'before_pane_close',
892
- 'pane_closed',
893
- 'before_worktree_remove',
894
- 'worktree_removed',
895
- 'pre_merge',
896
- 'post_merge',
897
- 'run_test',
898
- 'run_dev',
899
- ];
900
- const hooks = allHookTypes.map(hookName => ({
901
- name: hookName,
902
- active: hasHook(projectRoot || process.cwd(), hookName)
903
- }));
904
- setHooksData(hooks);
905
- setShowHooksDialog(true);
906
- setHooksSelectedIndex(0);
907
- }
908
- return;
909
- }
910
- // Enter edit mode for regular settings
911
- setSettingsEditingKey(currentDef.key);
912
- setSettingsMode('edit');
913
- // Set initial value index based on current setting
914
- const currentValue = settingsManager.getSetting(currentDef.key);
915
- if (currentDef.type === 'boolean') {
916
- setSettingsEditingValueIndex(currentValue ? 0 : 1);
917
- }
918
- else if (currentDef.type === 'select' && currentDef.options) {
919
- const optIndex = currentDef.options.findIndex(o => o.value === currentValue);
920
- setSettingsEditingValueIndex(Math.max(0, optIndex));
921
- }
922
- }
923
- else if (settingsMode === 'edit') {
924
- // Go to scope selection
925
- setSettingsMode('scope');
926
- setSettingsScopeIndex(0);
927
- }
928
- else if (settingsMode === 'scope') {
929
- // Save the setting
930
- const currentDef = SETTING_DEFINITIONS[settingsSelectedIndex];
931
- const scope = settingsScopeIndex === 0 ? 'global' : 'project';
932
- // Only save if this is not an action type (actions don't have values)
933
- if (currentDef.type !== 'action') {
934
- // Calculate the new value
935
- let newValue;
936
- if (currentDef.type === 'boolean') {
937
- newValue = settingsEditingValueIndex === 0;
938
- }
939
- else if (currentDef.type === 'select' && currentDef.options) {
940
- newValue = currentDef.options[settingsEditingValueIndex]?.value || '';
941
- }
942
- // Update the setting - cast key to proper type since we know it's not an action
943
- settingsManager.updateSetting(currentDef.key, newValue, scope);
944
- }
945
- // Reset to list view
946
- setSettingsMode('list');
947
- setSettingsEditingKey(undefined);
948
- setSettingsEditingValueIndex(0);
949
- setSettingsScopeIndex(0);
950
- setStatusMessage(`Setting saved (${scope})`);
951
- setTimeout(() => setStatusMessage(''), 2000);
952
- }
953
- return;
954
- }
955
- return;
956
- }
957
- // Handle action system confirm dialog
958
- if (actionSystem.actionState.showConfirmDialog) {
959
- if (key.escape) {
960
- if (actionSystem.actionState.onConfirmNo) {
961
- actionSystem.executeCallback(actionSystem.actionState.onConfirmNo);
962
- }
963
- else {
964
- actionSystem.setActionState(prev => ({ ...prev, showConfirmDialog: false }));
965
- }
966
- return;
967
- }
968
- else if (key.upArrow) {
969
- actionSystem.setActionState(prev => ({
970
- ...prev,
971
- confirmSelectedIndex: Math.max(0, prev.confirmSelectedIndex - 1)
972
- }));
973
- return;
974
- }
975
- else if (key.downArrow) {
976
- actionSystem.setActionState(prev => ({
977
- ...prev,
978
- confirmSelectedIndex: Math.min(1, prev.confirmSelectedIndex + 1)
979
- }));
980
- return;
981
- }
982
- else if (key.return) {
983
- // Execute based on selected index
984
- if (actionSystem.actionState.confirmSelectedIndex === 0) {
985
- if (actionSystem.actionState.onConfirmYes) {
986
- actionSystem.executeCallback(actionSystem.actionState.onConfirmYes);
987
- }
988
- }
989
- else {
990
- if (actionSystem.actionState.onConfirmNo) {
991
- actionSystem.executeCallback(actionSystem.actionState.onConfirmNo);
992
- }
993
- else {
994
- actionSystem.setActionState(prev => ({ ...prev, showConfirmDialog: false }));
995
- }
996
- }
997
- return;
998
- }
999
- else if (input === 'y' || input === 'Y') {
1000
- // Shortcut: yes
1001
- if (actionSystem.actionState.onConfirmYes) {
1002
- actionSystem.executeCallback(actionSystem.actionState.onConfirmYes);
1003
- }
1004
- return;
1005
- }
1006
- else if (input === 'n' || input === 'N') {
1007
- // Shortcut: no
1008
- if (actionSystem.actionState.onConfirmNo) {
1009
- actionSystem.executeCallback(actionSystem.actionState.onConfirmNo);
1010
- }
1011
- else {
1012
- actionSystem.setActionState(prev => ({ ...prev, showConfirmDialog: false }));
1013
- }
1014
- return;
1015
- }
1016
- return;
1017
- }
1018
- // Handle action system input dialog
1019
- if (actionSystem.actionState.showInputDialog) {
1020
- if (key.escape) {
1021
- actionSystem.setActionState(prev => ({ ...prev, showInputDialog: false }));
1022
- return;
1023
- }
1024
- else if (key.return) {
1025
- if (actionSystem.actionState.onInputSubmit) {
1026
- actionSystem.executeCallback(async () => actionSystem.actionState.onInputSubmit(actionSystem.actionState.inputValue));
1027
- }
1028
- return;
1029
- }
1030
- // Let CleanTextInput handle all other key events
1031
- return;
1032
- }
1033
- // Handle action system choice dialog
1034
- if (actionSystem.actionState.showChoiceDialog) {
1035
- if (key.escape) {
1036
- actionSystem.setActionState(prev => ({ ...prev, showChoiceDialog: false }));
1037
- return;
1038
- }
1039
- else if (key.upArrow) {
1040
- actionSystem.setActionState(prev => ({
1041
- ...prev,
1042
- choiceSelectedIndex: Math.max(0, prev.choiceSelectedIndex - 1)
1043
- }));
1044
- return;
1045
- }
1046
- else if (key.downArrow) {
1047
- const maxIndex = actionSystem.actionState.choiceOptions.length - 1;
1048
- actionSystem.setActionState(prev => ({
1049
- ...prev,
1050
- choiceSelectedIndex: Math.min(maxIndex, prev.choiceSelectedIndex + 1)
1051
- }));
1052
- return;
1053
- }
1054
- else if (key.return) {
1055
- const selectedOption = actionSystem.actionState.choiceOptions[actionSystem.actionState.choiceSelectedIndex];
1056
- if (selectedOption && actionSystem.actionState.onChoiceSelect) {
1057
- actionSystem.executeCallback(async () => actionSystem.actionState.onChoiceSelect(selectedOption.id));
1058
- }
1059
- return;
1060
- }
1061
- return;
1062
- }
1063
1695
  if (showFileCopyPrompt) {
1064
- if (input === 'y' || input === 'Y') {
1696
+ if (input === "y" || input === "Y") {
1065
1697
  setShowFileCopyPrompt(false);
1066
1698
  const selectedPane = panes[selectedIndex];
1067
1699
  if (selectedPane && selectedPane.worktreePath && currentCommandType) {
@@ -1069,7 +1701,7 @@ const DmuxApp = ({ panesFile, projectName, sessionName, settingsFile, projectRoo
1069
1701
  // Mark as not first run and continue with command
1070
1702
  const newSettings = {
1071
1703
  ...projectSettings,
1072
- [currentCommandType === 'test' ? 'firstTestRun' : 'firstDevRun']: true
1704
+ [currentCommandType === "test" ? "firstTestRun" : "firstDevRun"]: true,
1073
1705
  };
1074
1706
  await saveSettings(newSettings);
1075
1707
  // Now run the actual command
@@ -1077,14 +1709,14 @@ const DmuxApp = ({ panesFile, projectName, sessionName, settingsFile, projectRoo
1077
1709
  }
1078
1710
  setCurrentCommandType(null);
1079
1711
  }
1080
- else if (input === 'n' || input === 'N' || key.escape) {
1712
+ else if (input === "n" || input === "N" || key.escape) {
1081
1713
  setShowFileCopyPrompt(false);
1082
1714
  const selectedPane = panes[selectedIndex];
1083
1715
  if (selectedPane && currentCommandType) {
1084
1716
  // Mark as not first run and continue without copying
1085
1717
  const newSettings = {
1086
1718
  ...projectSettings,
1087
- [currentCommandType === 'test' ? 'firstTestRun' : 'firstDevRun']: true
1719
+ [currentCommandType === "test" ? "firstTestRun" : "firstDevRun"]: true,
1088
1720
  };
1089
1721
  await saveSettings(newSettings);
1090
1722
  // Now run the actual command
@@ -1094,36 +1726,13 @@ const DmuxApp = ({ panesFile, projectName, sessionName, settingsFile, projectRoo
1094
1726
  }
1095
1727
  return;
1096
1728
  }
1097
- if (showAgentChoiceDialog) {
1098
- if (key.escape) {
1099
- setShowAgentChoiceDialog(false);
1100
- setShowNewPaneDialog(true);
1101
- setNewPanePrompt(pendingPrompt);
1102
- setPendingPrompt('');
1103
- }
1104
- else if (key.leftArrow || input === '1' || (input && input.toLowerCase() === 'c')) {
1105
- setAgentChoice('claude');
1106
- }
1107
- else if (key.rightArrow || input === '2' || (input && input.toLowerCase() === 'o')) {
1108
- setAgentChoice('opencode');
1109
- }
1110
- else if (key.return) {
1111
- const chosen = agentChoice || (availableAgents[0] || 'claude');
1112
- const promptValue = pendingPrompt;
1113
- setShowAgentChoiceDialog(false);
1114
- setPendingPrompt('');
1115
- await createNewPaneHook(promptValue, chosen);
1116
- setNewPanePrompt('');
1117
- }
1118
- return;
1119
- }
1120
1729
  if (showCommandPrompt) {
1121
1730
  if (key.escape) {
1122
1731
  setShowCommandPrompt(null);
1123
- setCommandInput('');
1732
+ setCommandInput("");
1124
1733
  }
1125
1734
  else if (key.return) {
1126
- if (commandInput.trim() === '') {
1735
+ if (commandInput.trim() === "") {
1127
1736
  // If empty, suggest a default command based on package manager
1128
1737
  const suggested = await suggestCommand(showCommandPrompt);
1129
1738
  if (suggested) {
@@ -1134,13 +1743,15 @@ const DmuxApp = ({ panesFile, projectName, sessionName, settingsFile, projectRoo
1134
1743
  // User provided manual command
1135
1744
  const newSettings = {
1136
1745
  ...projectSettings,
1137
- [showCommandPrompt === 'test' ? 'testCommand' : 'devCommand']: commandInput.trim()
1746
+ [showCommandPrompt === "test" ? "testCommand" : "devCommand"]: commandInput.trim(),
1138
1747
  };
1139
1748
  await saveSettings(newSettings);
1140
1749
  const selectedPane = panes[selectedIndex];
1141
1750
  if (selectedPane) {
1142
1751
  // Check if first run
1143
- const isFirstRun = showCommandPrompt === 'test' ? !projectSettings.firstTestRun : !projectSettings.firstDevRun;
1752
+ const isFirstRun = showCommandPrompt === "test"
1753
+ ? !projectSettings.firstTestRun
1754
+ : !projectSettings.firstDevRun;
1144
1755
  if (isFirstRun) {
1145
1756
  setCurrentCommandType(showCommandPrompt);
1146
1757
  setShowCommandPrompt(null);
@@ -1149,173 +1760,210 @@ const DmuxApp = ({ panesFile, projectName, sessionName, settingsFile, projectRoo
1149
1760
  else {
1150
1761
  await runCommandInternal(showCommandPrompt, selectedPane);
1151
1762
  setShowCommandPrompt(null);
1152
- setCommandInput('');
1763
+ setCommandInput("");
1153
1764
  }
1154
1765
  }
1155
1766
  else {
1156
1767
  setShowCommandPrompt(null);
1157
- setCommandInput('');
1768
+ setCommandInput("");
1158
1769
  }
1159
1770
  }
1160
1771
  }
1161
1772
  return;
1162
1773
  }
1163
- if (showNewPaneDialog) {
1164
- if (key.escape) {
1165
- setShowNewPaneDialog(false);
1166
- setNewPanePrompt('');
1167
- }
1168
- // TextInput handles other input events
1169
- return;
1170
- }
1171
1774
  // Handle directional navigation with spatial awareness based on card grid layout
1172
1775
  if (key.upArrow || key.downArrow || key.leftArrow || key.rightArrow) {
1173
1776
  let targetIndex = null;
1174
1777
  if (key.upArrow) {
1175
- targetIndex = findCardInDirection(selectedIndex, 'up');
1778
+ targetIndex = findCardInDirection(selectedIndex, "up");
1176
1779
  }
1177
1780
  else if (key.downArrow) {
1178
- targetIndex = findCardInDirection(selectedIndex, 'down');
1781
+ targetIndex = findCardInDirection(selectedIndex, "down");
1179
1782
  }
1180
1783
  else if (key.leftArrow) {
1181
- targetIndex = findCardInDirection(selectedIndex, 'left');
1784
+ targetIndex = findCardInDirection(selectedIndex, "left");
1182
1785
  }
1183
1786
  else if (key.rightArrow) {
1184
- targetIndex = findCardInDirection(selectedIndex, 'right');
1787
+ targetIndex = findCardInDirection(selectedIndex, "right");
1185
1788
  }
1186
1789
  if (targetIndex !== null) {
1187
1790
  setSelectedIndex(targetIndex);
1188
1791
  }
1189
1792
  return;
1190
1793
  }
1191
- if ((input === 'm' || key.return) && selectedIndex < panes.length) {
1192
- // Open kebab menu for selected pane
1193
- const selectedPane = panes[selectedIndex];
1194
- const actions = getAvailableActions(selectedPane, projectSettings);
1195
- setKebabMenuActions(actions);
1196
- setShowKebabMenu(true);
1197
- setKebabMenuPaneIndex(selectedIndex);
1198
- setKebabMenuOption(0);
1199
- }
1200
- else if (input === 's') {
1201
- // Open settings dialog
1202
- setShowSettingsDialog(true);
1203
- setSettingsMode('list');
1204
- setSettingsSelectedIndex(0);
1205
- }
1206
- else if (input === 'q') {
1794
+ if (input === "m" && selectedIndex < panes.length) {
1795
+ // Open kebab menu popup for selected pane
1796
+ await launchKebabMenuPopup(selectedIndex);
1797
+ }
1798
+ else if (input === "s") {
1799
+ // Open settings popup
1800
+ await launchSettingsPopup();
1801
+ }
1802
+ else if (input === "l") {
1803
+ // Open logs popup
1804
+ await launchLogsPopup();
1805
+ }
1806
+ else if (input === "?") {
1807
+ // Open keyboard shortcuts popup
1808
+ await launchShortcutsPopup();
1809
+ }
1810
+ else if (input === "L" && controlPaneId) {
1811
+ // Reset layout to sidebar configuration (Shift+L)
1812
+ enforceControlPaneSize(controlPaneId, SIDEBAR_WIDTH);
1813
+ setStatusMessage("Layout reset");
1814
+ setTimeout(() => setStatusMessage(""), 2000);
1815
+ }
1816
+ else if (input === "q") {
1207
1817
  cleanExit();
1208
1818
  }
1209
- else if (input === 'r' && server) {
1210
- // Create tunnel if not already created, then show QR code
1211
- if (!tunnelUrl) {
1212
- setIsCreatingTunnel(true);
1213
- setStatusMessage('Creating tunnel...');
1214
- try {
1215
- const url = await server.startTunnel();
1216
- setTunnelUrl(url);
1217
- setStatusMessage('');
1218
- setShowQRCode(true);
1219
- }
1220
- catch (error) {
1221
- setStatusMessage('Failed to create tunnel');
1222
- setTimeout(() => setStatusMessage(''), 3000);
1223
- }
1224
- finally {
1225
- setIsCreatingTunnel(false);
1226
- }
1819
+ else if (input === "r" && server) {
1820
+ // Handle remote tunnel
1821
+ if (tunnelUrl) {
1822
+ // Tunnel exists - open popup with QR code
1823
+ await launchRemotePopup();
1227
1824
  }
1228
- else {
1229
- // Tunnel already exists, just show the QR code
1230
- setShowQRCode(true);
1825
+ else if (!tunnelCreating) {
1826
+ // Start tunnel creation
1827
+ setTunnelCreating(true);
1828
+ (async () => {
1829
+ try {
1830
+ const url = await server.startTunnel();
1831
+ setTunnelUrl(url);
1832
+ }
1833
+ catch (error) {
1834
+ setStatusMessage(`Failed to create tunnel: ${error.message}`);
1835
+ setTimeout(() => setStatusMessage(""), 3000);
1836
+ }
1837
+ finally {
1838
+ setTunnelCreating(false);
1839
+ }
1840
+ })();
1231
1841
  }
1842
+ // If tunnelCreating is true, do nothing (already creating)
1843
+ return;
1844
+ }
1845
+ else if (!isLoading &&
1846
+ (input === "n" || (key.return && selectedIndex === panes.length))) {
1847
+ // Launch popup modal for new pane
1848
+ await launchNewPanePopup();
1849
+ return;
1232
1850
  }
1233
- else if (!isLoading && (input === 'n' || (key.return && selectedIndex === panes.length))) {
1234
- // Clear the prompt and show dialog in next tick to prevent 'n' bleeding through
1235
- setNewPanePrompt('');
1236
- setShowNewPaneDialog(true);
1237
- return; // Consume the 'n' keystroke so it doesn't propagate
1851
+ else if (!isLoading &&
1852
+ (input === "t" || (key.return && selectedIndex === panes.length + 1))) {
1853
+ // Create a new terminal pane without an agent
1854
+ try {
1855
+ setIsCreatingPane(true);
1856
+ setStatusMessage("Creating terminal pane...");
1857
+ // Create a simple tmux pane split
1858
+ const paneInfo = execSync(`tmux split-window -h -P -F '#{pane_id}'`, {
1859
+ encoding: "utf-8",
1860
+ }).trim();
1861
+ // Wait for pane creation to settle
1862
+ await new Promise((resolve) => setTimeout(resolve, 300));
1863
+ // The shell pane will be automatically detected by the shell pane detection system
1864
+ // No need to manually add it to the panes array
1865
+ setIsCreatingPane(false);
1866
+ setStatusMessage("Terminal pane created");
1867
+ setTimeout(() => setStatusMessage(""), 2000);
1868
+ // Force a reload to pick up the new shell pane
1869
+ await loadPanes();
1870
+ }
1871
+ catch (error) {
1872
+ setIsCreatingPane(false);
1873
+ setStatusMessage(`Failed to create terminal pane: ${error.message}`);
1874
+ setTimeout(() => setStatusMessage(""), 3000);
1875
+ }
1876
+ return;
1238
1877
  }
1239
- else if (input === 'j' && selectedIndex < panes.length) {
1878
+ else if (input === "j" && selectedIndex < panes.length) {
1240
1879
  // Jump to pane (NEW: using action system)
1241
1880
  StateManager.getInstance().setDebugMessage(`Jumping to pane: ${panes[selectedIndex].slug}`);
1242
- setTimeout(() => StateManager.getInstance().setDebugMessage(''), 2000);
1881
+ setTimeout(() => StateManager.getInstance().setDebugMessage(""), 2000);
1243
1882
  actionSystem.executeAction(PaneAction.VIEW, panes[selectedIndex]);
1244
1883
  }
1245
- else if (input === 'x' && selectedIndex < panes.length) {
1884
+ else if (input === "x" && selectedIndex < panes.length) {
1246
1885
  // Close pane (NEW: using action system)
1247
1886
  StateManager.getInstance().setDebugMessage(`Closing pane: ${panes[selectedIndex].slug}`);
1248
- setTimeout(() => StateManager.getInstance().setDebugMessage(''), 2000);
1887
+ setTimeout(() => StateManager.getInstance().setDebugMessage(""), 2000);
1249
1888
  actionSystem.executeAction(PaneAction.CLOSE, panes[selectedIndex]);
1250
1889
  }
1251
1890
  else if (key.return && selectedIndex < panes.length) {
1252
1891
  // Jump to pane (NEW: using action system)
1253
1892
  StateManager.getInstance().setDebugMessage(`Jumping to pane: ${panes[selectedIndex].slug}`);
1254
- setTimeout(() => StateManager.getInstance().setDebugMessage(''), 2000);
1893
+ setTimeout(() => StateManager.getInstance().setDebugMessage(""), 2000);
1255
1894
  actionSystem.executeAction(PaneAction.VIEW, panes[selectedIndex]);
1256
1895
  }
1257
1896
  });
1258
- // If showing QR code, render only that
1259
- if (showQRCode && tunnelUrl) {
1260
- return (React.createElement(Box, { flexDirection: "column" },
1261
- React.createElement(Box, { marginBottom: 1 },
1262
- React.createElement(Text, { bold: true, color: "cyan" }, "dmux - Remote Access")),
1263
- React.createElement(QRCode, { url: tunnelUrl }),
1264
- React.createElement(Box, { marginTop: 1 },
1265
- React.createElement(Text, { dimColor: true }, "Press ESC to return to pane list"))));
1897
+ // Calculate available height for content (terminal height - footer lines - active status messages)
1898
+ // Footer height varies based on state:
1899
+ // - Quit confirm mode: 2 lines (marginTop + 1 text line)
1900
+ // - Normal mode calculation:
1901
+ // - Base: 4 lines (marginTop + logs divider + logs line + keyboard shortcuts)
1902
+ // - Network section: +4 lines (divider, local IP, remote tunnel, divider) if serverPort exists
1903
+ // - Debug info: +1 line if DEBUG_DMUX
1904
+ // - Status line: +1 line if updateAvailable/currentBranch/debugMessage
1905
+ // - Status messages: +1 line per active message
1906
+ let footerLines = 2;
1907
+ if (quitConfirmMode) {
1908
+ footerLines = 2;
1266
1909
  }
1267
- return (React.createElement(Box, { flexDirection: "column" },
1910
+ else {
1911
+ // Base footer (logs divider + logs + shortcuts - always shown)
1912
+ footerLines = 4; // marginTop + logs divider + logs + shortcuts
1913
+ // Add network section (now 2 lines for local IP + remote tunnel, plus 2 dividers)
1914
+ if (serverPort && serverPort > 0) {
1915
+ footerLines += 4;
1916
+ }
1917
+ // Add debug info
1918
+ if (process.env.DEBUG_DMUX) {
1919
+ footerLines += 1;
1920
+ }
1921
+ // Add status line
1922
+ if (updateAvailable || currentBranch || debugMessage) {
1923
+ footerLines += 1;
1924
+ }
1925
+ // Add line for each active status message
1926
+ if (statusMessage) {
1927
+ footerLines += 1;
1928
+ }
1929
+ if (actionSystem.actionState.statusMessage) {
1930
+ footerLines += 1;
1931
+ }
1932
+ }
1933
+ const contentHeight = Math.max(terminalHeight - footerLines, 10);
1934
+ return (React.createElement(Box, { flexDirection: "column", height: terminalHeight },
1268
1935
  showRepaintSpinner && (React.createElement(Box, { marginTop: -10, marginLeft: -100 },
1269
1936
  React.createElement(Text, null, "\u27F3"))),
1270
- React.createElement(PanesGrid, { panes: panes, selectedIndex: selectedIndex, isLoading: isLoading, showNewPaneDialog: showNewPaneDialog, agentStatuses: agentStatuses, kebabMenuPaneIndex: kebabMenuPaneIndex ?? undefined }),
1271
- isLoading && (React.createElement(LoadingIndicator, null)),
1272
- showNewPaneDialog && !showAgentChoiceDialog && (React.createElement(NewPaneDialog, { value: newPanePrompt, onChange: setNewPanePrompt, onSubmit: (value) => {
1273
- const promptValue = value;
1274
- const agents = availableAgents;
1275
- if (agents.length === 0) {
1276
- setShowNewPaneDialog(false);
1277
- setNewPanePrompt('');
1278
- createNewPaneHook(promptValue);
1279
- }
1280
- else if (agents.length === 1) {
1281
- setShowNewPaneDialog(false);
1282
- setNewPanePrompt('');
1283
- createNewPaneHook(promptValue, agents[0]);
1284
- }
1285
- else {
1286
- setPendingPrompt(promptValue);
1287
- setShowNewPaneDialog(false);
1288
- setNewPanePrompt('');
1289
- setShowAgentChoiceDialog(true);
1290
- setAgentChoice(agentChoice || 'claude');
1291
- }
1292
- } })),
1293
- showAgentChoiceDialog && (React.createElement(AgentChoiceDialog, { agentChoice: agentChoice })),
1294
- isCreatingPane && (React.createElement(CreatingIndicator, { message: statusMessage })),
1295
- showCommandPrompt && (React.createElement(CommandPromptDialog, { type: showCommandPrompt, value: commandInput, onChange: setCommandInput })),
1296
- showFileCopyPrompt && (React.createElement(FileCopyPrompt, null)),
1297
- showKebabMenu && kebabMenuPaneIndex !== null && panes[kebabMenuPaneIndex] && (React.createElement(KebabMenu, { selectedOption: kebabMenuOption, actions: kebabMenuActions, paneName: panes[kebabMenuPaneIndex].slug })),
1298
- showSettingsDialog && (React.createElement(SettingsDialog, { settings: settingsManager.getSettings(), globalSettings: settingsManager.getGlobalSettings(), projectSettings: settingsManager.getProjectSettings(), settingDefinitions: SETTING_DEFINITIONS, selectedIndex: settingsSelectedIndex, mode: settingsMode, editingKey: settingsEditingKey, editingValueIndex: settingsEditingValueIndex, scopeIndex: settingsScopeIndex })),
1299
- showHooksDialog && (React.createElement(HooksDialog, { hooks: hooksData, selectedIndex: hooksSelectedIndex })),
1300
- actionSystem.actionState.showConfirmDialog && (React.createElement(ActionConfirmDialog, { key: "confirm-dialog", title: actionSystem.actionState.confirmTitle, message: actionSystem.actionState.confirmMessage, yesLabel: actionSystem.actionState.confirmYesLabel, noLabel: actionSystem.actionState.confirmNoLabel, selectedIndex: actionSystem.actionState.confirmSelectedIndex })),
1301
- actionSystem.actionState.showChoiceDialog && (React.createElement(ActionChoiceDialog, { key: "choice-dialog", title: actionSystem.actionState.choiceTitle, message: actionSystem.actionState.choiceMessage, options: actionSystem.actionState.choiceOptions, selectedIndex: actionSystem.actionState.choiceSelectedIndex })),
1302
- actionSystem.actionState.showInputDialog && (React.createElement(ActionInputDialog, { key: "input-dialog", title: actionSystem.actionState.inputTitle, message: actionSystem.actionState.inputMessage, placeholder: actionSystem.actionState.inputPlaceholder, value: actionSystem.actionState.inputValue, onValueChange: (value) => {
1303
- actionSystem.setActionState(prev => ({ ...prev, inputValue: value }));
1304
- } })),
1305
- actionSystem.actionState.showProgressDialog && (React.createElement(ActionProgressDialog, { key: "progress-dialog", message: actionSystem.actionState.progressMessage, percent: actionSystem.actionState.progressPercent })),
1306
- runningCommand && (React.createElement(RunningIndicator, null)),
1307
- isUpdating && (React.createElement(UpdatingIndicator, null)),
1308
- isCreatingTunnel && (React.createElement(Box, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", padding: 1, marginTop: 1 },
1309
- React.createElement(Text, { bold: true, color: "cyan" }, "Creating tunnel..."),
1310
- React.createElement(Box, { marginTop: 1 },
1311
- React.createElement(Text, { dimColor: true }, "This may take a few moments...")))),
1312
- statusMessage && (React.createElement(Box, { marginTop: 1 },
1937
+ React.createElement(Box, { flexDirection: "column", height: contentHeight, overflow: "hidden" },
1938
+ React.createElement(PanesGrid, { panes: panes, selectedIndex: selectedIndex, isLoading: isLoading, agentStatuses: agentStatuses }),
1939
+ isLoading && React.createElement(LoadingIndicator, null),
1940
+ showCommandPrompt && (React.createElement(CommandPromptDialog, { type: showCommandPrompt, value: commandInput, onChange: setCommandInput })),
1941
+ showFileCopyPrompt && React.createElement(FileCopyPrompt, null),
1942
+ runningCommand && React.createElement(RunningIndicator, null),
1943
+ isUpdating && React.createElement(UpdatingIndicator, null)),
1944
+ statusMessage && (React.createElement(Box, null,
1313
1945
  React.createElement(Text, { color: "green" }, statusMessage))),
1314
- actionSystem.actionState.statusMessage && (React.createElement(Box, { marginTop: 1 },
1315
- React.createElement(Text, { color: actionSystem.actionState.statusType === 'error' ? 'red' :
1316
- actionSystem.actionState.statusType === 'success' ? 'green' :
1317
- 'cyan' }, actionSystem.actionState.statusMessage))),
1318
- React.createElement(FooterHelp, { show: !showNewPaneDialog && !showCommandPrompt, showRemoteKey: !!server, quitConfirmMode: quitConfirmMode, gridInfo: (() => {
1946
+ actionSystem.actionState.statusMessage && (React.createElement(Box, null,
1947
+ React.createElement(Text, { color: actionSystem.actionState.statusType === "error"
1948
+ ? "red"
1949
+ : actionSystem.actionState.statusType === "success"
1950
+ ? "green"
1951
+ : "cyan" }, actionSystem.actionState.statusMessage))),
1952
+ React.createElement(FooterHelp, { show: !showCommandPrompt, showRemoteKey: !!server, quitConfirmMode: quitConfirmMode, hasSidebarLayout: !!controlPaneId, serverPort: serverPort, unreadErrorCount: unreadErrorCount, unreadWarningCount: unreadWarningCount, localIp: localIp, tunnelUrl: tunnelUrl, tunnelCreating: tunnelCreating, tunnelCopied: tunnelCopied, tunnelSpinner: (() => {
1953
+ const spinnerFrames = [
1954
+ "⠋",
1955
+ "⠙",
1956
+ "⠹",
1957
+ "⠸",
1958
+ "⠼",
1959
+ "⠴",
1960
+ "⠦",
1961
+ "⠧",
1962
+ "⠇",
1963
+ "⠏",
1964
+ ];
1965
+ return spinnerFrames[tunnelSpinnerFrame];
1966
+ })(), gridInfo: (() => {
1319
1967
  if (!process.env.DEBUG_DMUX)
1320
1968
  return undefined;
1321
1969
  const cols = Math.max(1, Math.floor(terminalWidth / 37));
@@ -1323,16 +1971,14 @@ const DmuxApp = ({ panesFile, projectName, sessionName, settingsFile, projectRoo
1323
1971
  const pos = getCardGridPosition(selectedIndex);
1324
1972
  return `Grid: ${cols} cols × ${rows} rows | Selected: row ${pos.row}, col ${pos.col} | Terminal: ${terminalWidth}w`;
1325
1973
  })() }),
1326
- React.createElement(Text, { dimColor: true },
1327
- updateAvailable && updateInfo && (React.createElement(Text, { color: "red", bold: true }, "Update available: npm i -g dmux@latest ")),
1328
- "v",
1329
- packageJson.version,
1330
- serverPort && serverPort > 0 && (React.createElement(Text, { dimColor: true },
1331
- " \u2022 ",
1332
- React.createElement(Text, { color: "cyan" },
1333
- "http://127.0.0.1:",
1334
- serverPort))),
1335
- debugMessage && (React.createElement(Text, { dimColor: true },
1974
+ (updateAvailable || currentBranch || debugMessage) && (React.createElement(Text, { dimColor: true },
1975
+ updateAvailable && updateInfo && (React.createElement(Text, { color: "red", bold: true },
1976
+ "Update available: npm i -g dmux@latest",
1977
+ " ")),
1978
+ currentBranch && (React.createElement(Text, { color: "magenta", bold: true },
1979
+ "branch: ",
1980
+ currentBranch)),
1981
+ debugMessage && React.createElement(Text, { dimColor: true },
1336
1982
  " \u2022 ",
1337
1983
  debugMessage)))));
1338
1984
  };