dmux 3.1.0 → 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 (233) 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 +1408 -751
  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 +30 -29
  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 +2077 -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 +3 -3
  192. package/dist/utils/hooks.d.ts.map +1 -1
  193. package/dist/utils/hooks.js +70 -41
  194. package/dist/utils/hooks.js.map +1 -1
  195. package/dist/utils/hooksDocs.d.ts +1 -1
  196. package/dist/utils/layoutManager.d.ts +45 -0
  197. package/dist/utils/layoutManager.d.ts.map +1 -0
  198. package/dist/utils/layoutManager.js +500 -0
  199. package/dist/utils/layoutManager.js.map +1 -0
  200. package/dist/utils/paneCreation.d.ts.map +1 -1
  201. package/dist/utils/paneCreation.js +125 -11
  202. package/dist/utils/paneCreation.js.map +1 -1
  203. package/dist/utils/popup.d.ts +97 -0
  204. package/dist/utils/popup.d.ts.map +1 -0
  205. package/dist/utils/popup.js +509 -0
  206. package/dist/utils/popup.js.map +1 -0
  207. package/dist/utils/postPaneCleanup.d.ts +12 -0
  208. package/dist/utils/postPaneCleanup.d.ts.map +1 -0
  209. package/dist/utils/postPaneCleanup.js +53 -0
  210. package/dist/utils/postPaneCleanup.js.map +1 -0
  211. package/dist/utils/shellPaneDetection.d.ts +44 -0
  212. package/dist/utils/shellPaneDetection.d.ts.map +1 -0
  213. package/dist/utils/shellPaneDetection.js +175 -0
  214. package/dist/utils/shellPaneDetection.js.map +1 -0
  215. package/dist/utils/tmux.d.ts +53 -1
  216. package/dist/utils/tmux.d.ts.map +1 -1
  217. package/dist/utils/tmux.js +352 -84
  218. package/dist/utils/tmux.js.map +1 -1
  219. package/dist/utils/welcomePane.d.ts +22 -0
  220. package/dist/utils/welcomePane.d.ts.map +1 -0
  221. package/dist/utils/welcomePane.js +119 -0
  222. package/dist/utils/welcomePane.js.map +1 -0
  223. package/dist/utils/welcomePaneManager.d.ts +36 -0
  224. package/dist/utils/welcomePaneManager.d.ts.map +1 -0
  225. package/dist/utils/welcomePaneManager.js +160 -0
  226. package/dist/utils/welcomePaneManager.js.map +1 -0
  227. package/dist/workers/PaneWorker.js +2 -0
  228. package/dist/workers/PaneWorker.js.map +1 -1
  229. package/package.json +5 -2
  230. package/dist/components/NewPaneDialog.d.ts +0 -9
  231. package/dist/components/NewPaneDialog.d.ts.map +0 -1
  232. package/dist/components/NewPaneDialog.js +0 -11
  233. package/dist/components/NewPaneDialog.js.map +0 -1
package/dist/DmuxApp.js CHANGED
@@ -1,142 +1,184 @@
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);
55
+ // Spinner state - shows for a few frames to force render
56
+ const [showRepaintSpinner, setShowRepaintSpinner] = useState(false);
67
57
  const { projectSettings, saveSettings } = useProjectSettings(settingsFile);
68
- // Hooks management state
69
- const [showHooksDialog, setShowHooksDialog] = useState(false);
70
- const [hooksSelectedIndex, setHooksSelectedIndex] = useState(0);
71
- const [hooksData, setHooksData] = useState([]);
72
58
  const [showCommandPrompt, setShowCommandPrompt] = useState(null);
73
- const [commandInput, setCommandInput] = useState('');
59
+ const [commandInput, setCommandInput] = useState("");
74
60
  const [showFileCopyPrompt, setShowFileCopyPrompt] = useState(false);
75
61
  const [currentCommandType, setCurrentCommandType] = useState(null);
76
62
  const [runningCommand, setRunningCommand] = useState(false);
77
63
  const [quitConfirmMode, setQuitConfirmMode] = useState(false);
78
- const [showKebabMenu, setShowKebabMenu] = useState(false);
79
- const [kebabMenuPaneIndex, setKebabMenuPaneIndex] = useState(null);
80
- const [kebabMenuOption, setKebabMenuOption] = useState(0);
81
- const [kebabMenuActions, setKebabMenuActions] = useState([]);
82
64
  // Debug message state - for temporary logging messages
83
- const [debugMessage, setDebugMessage] = useState('');
65
+ const [debugMessage, setDebugMessage] = useState("");
66
+ // Current git branch state (for dev builds)
67
+ const [currentBranch, setCurrentBranch] = useState(null);
84
68
  // Update state handled by hook
85
- const { updateInfo, showUpdateDialog, isUpdating, performUpdate, skipUpdate, dismissUpdate, updateAvailable } = useAutoUpdater(autoUpdater, setStatusMessage);
69
+ const { updateInfo, showUpdateDialog, isUpdating, performUpdate, skipUpdate, dismissUpdate, updateAvailable, } = useAutoUpdater(autoUpdater, setStatusMessage);
86
70
  const { exit } = useApp();
71
+ // Flag to ignore input temporarily after popup closes (prevents buffered keys)
72
+ const [ignoreInput, setIgnoreInput] = useState(false);
87
73
  // Agent selection state
88
74
  const { availableAgents } = useAgentDetection();
89
- const [showAgentChoiceDialog, setShowAgentChoiceDialog] = useState(false);
90
75
  const [agentChoice, setAgentChoice] = useState(null);
91
- const [pendingPrompt, setPendingPrompt] = useState('');
76
+ // Popup support detection
77
+ const [popupsSupported, setPopupsSupported] = useState(false);
92
78
  // Track terminal dimensions for responsive layout
93
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
+ }, []);
94
104
  // Panes state and persistence (skipLoading will be updated after actionSystem is initialized)
95
105
  const { panes, setPanes, isLoading, loadPanes, savePanes } = usePanes(panesFile, false);
96
106
  // Track intentionally closed panes to prevent race condition
97
107
  // When a user closes a pane, we add it to this set. If the worker detects
98
108
  // the pane is gone (which it will), we check this set first before re-saving.
99
109
  const intentionallyClosedPanes = React.useRef(new Set());
100
- // Action system
101
- const actionSystem = useActionSystem({
102
- panes,
103
- savePanes,
104
- sessionName,
105
- projectName,
106
- onPaneRemove: (paneId) => {
107
- // Mark this pane as intentionally closed
108
- intentionallyClosedPanes.current.add(paneId);
109
- const updated = panes.filter(p => p.id !== paneId);
110
- setPanes(updated);
111
- // Clean up the tracking after a delay (in case of race conditions)
112
- setTimeout(() => {
113
- intentionallyClosedPanes.current.delete(paneId);
114
- }, 5000);
115
- },
116
- });
117
110
  // Pane runner
118
- const { copyNonGitFiles, runCommandInternal, monitorTestOutput, monitorDevOutput, attachBackgroundWindow } = usePaneRunner({
111
+ const { copyNonGitFiles, runCommandInternal, monitorTestOutput, monitorDevOutput, attachBackgroundWindow, } = usePaneRunner({
119
112
  panes,
120
113
  savePanes,
121
114
  projectSettings,
122
115
  setStatusMessage,
123
116
  setRunningCommand,
124
117
  });
125
- // Force repaint helper
126
- const forceRepaint = () => setForceRepaintTrigger(prev => prev + 1);
118
+ // Force repaint helper - shows spinner for a few frames to force full re-render
119
+ const forceRepaint = () => {
120
+ setForceRepaintTrigger((prev) => prev + 1);
121
+ setShowRepaintSpinner(true);
122
+ // Hide spinner after a few frames (enough to trigger multiple renders)
123
+ setTimeout(() => setShowRepaintSpinner(false), 100);
124
+ };
127
125
  // Force repaint effect - ensures Ink re-renders when trigger changes
128
126
  useEffect(() => {
129
127
  if (forceRepaintTrigger > 0) {
130
128
  // Small delay to ensure terminal is ready
131
129
  const timer = setTimeout(() => {
132
130
  try {
133
- execSync('tmux refresh-client', { stdio: 'pipe' });
131
+ execSync("tmux refresh-client", { stdio: "pipe" });
134
132
  }
135
133
  catch { }
136
134
  }, 50);
137
135
  return () => clearTimeout(timer);
138
136
  }
139
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]);
140
182
  // Pane creation
141
183
  const { createNewPane: createNewPaneHook } = usePaneCreation({
142
184
  panes,
@@ -144,7 +186,6 @@ const DmuxApp = ({ panesFile, projectName, sessionName, settingsFile, projectRoo
144
186
  projectName,
145
187
  setIsCreatingPane,
146
188
  setStatusMessage,
147
- setNewPanePrompt,
148
189
  loadPanes,
149
190
  panesFile,
150
191
  availableAgents,
@@ -154,8 +195,8 @@ const DmuxApp = ({ panesFile, projectName, sessionName, settingsFile, projectRoo
154
195
  useEffect(() => {
155
196
  const statusDetector = getStatusDetector();
156
197
  const handleStatusUpdate = (event) => {
157
- setPanes(prevPanes => {
158
- const updatedPanes = prevPanes.map(pane => {
198
+ setPanes((prevPanes) => {
199
+ const updatedPanes = prevPanes.map((pane) => {
159
200
  if (pane.id === event.paneId) {
160
201
  const updated = {
161
202
  ...pane,
@@ -179,22 +220,23 @@ const DmuxApp = ({ panesFile, projectName, sessionName, settingsFile, projectRoo
179
220
  updated.analyzerError = event.analyzerError;
180
221
  }
181
222
  // Clear option dialog data when transitioning away from 'waiting' state
182
- if (event.status !== 'waiting' && pane.agentStatus === 'waiting') {
223
+ if (event.status !== "waiting" && pane.agentStatus === "waiting") {
183
224
  updated.optionsQuestion = undefined;
184
225
  updated.options = undefined;
185
226
  updated.potentialHarm = undefined;
186
227
  }
187
228
  // Clear summary when transitioning away from 'idle' state
188
- if (event.status !== 'idle' && pane.agentStatus === 'idle') {
229
+ if (event.status !== "idle" && pane.agentStatus === "idle") {
189
230
  updated.agentSummary = undefined;
190
231
  }
191
232
  // Clear analyzer error when successfully getting a new analysis
192
233
  // or when transitioning to 'working' status
193
- if (event.status === 'working') {
234
+ if (event.status === "working") {
194
235
  updated.analyzerError = undefined;
195
236
  }
196
- else if (event.status === 'waiting' || event.status === 'idle') {
197
- 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)) {
198
240
  updated.analyzerError = undefined;
199
241
  }
200
242
  }
@@ -203,15 +245,15 @@ const DmuxApp = ({ panesFile, projectName, sessionName, settingsFile, projectRoo
203
245
  return pane;
204
246
  });
205
247
  // Persist to disk - ConfigWatcher will handle syncing to StateManager
206
- savePanes(updatedPanes).catch(err => {
207
- 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);
208
250
  });
209
251
  return updatedPanes;
210
252
  });
211
253
  };
212
- statusDetector.on('status-updated', handleStatusUpdate);
254
+ statusDetector.on("status-updated", handleStatusUpdate);
213
255
  return () => {
214
- statusDetector.off('status-updated', handleStatusUpdate);
256
+ statusDetector.off("status-updated", handleStatusUpdate);
215
257
  };
216
258
  }, [setPanes, savePanes]);
217
259
  // Note: No need to sync panes with StateManager here.
@@ -236,50 +278,936 @@ const DmuxApp = ({ panesFile, projectName, sessionName, settingsFile, projectRoo
236
278
  const handleTermination = () => {
237
279
  cleanExit();
238
280
  };
239
- process.on('SIGTERM', handleTermination);
240
- // Test debug message on mount
241
- StateManager.getInstance().setDebugMessage('Debug logging initialized - watching for AI activity...');
242
- setTimeout(() => {
243
- StateManager.getInstance().setDebugMessage('');
244
- }, 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
+ }
245
288
  return () => {
246
- process.removeListener('SIGTERM', handleTermination);
289
+ process.removeListener("SIGTERM", handleTermination);
247
290
  };
248
291
  }, []);
249
- // Auto-show new pane dialog when starting with no panes
250
- useEffect(() => {
251
- // Only show the dialog if:
252
- // 1. Initial load is complete (!isLoading)
253
- // 2. We have no panes
254
- // 3. We're not already showing the dialog
255
- // 4. We're not showing any other dialogs or prompts
256
- if (!isLoading &&
257
- panes.length === 0 &&
258
- !showNewPaneDialog &&
259
- !actionSystem.actionState.showConfirmDialog &&
260
- !actionSystem.actionState.showChoiceDialog &&
261
- !actionSystem.actionState.showInputDialog &&
262
- !actionSystem.actionState.showProgressDialog &&
263
- !showCommandPrompt &&
264
- !showFileCopyPrompt &&
265
- !showAgentChoiceDialog &&
266
- !isCreatingPane &&
267
- !runningCommand &&
268
- !isUpdating) {
269
- setShowNewPaneDialog(true);
270
- }
271
- }, [isLoading, panes.length, showNewPaneDialog, actionSystem.actionState.showConfirmDialog, actionSystem.actionState.showChoiceDialog, actionSystem.actionState.showInputDialog, actionSystem.actionState.showProgressDialog, showCommandPrompt, showFileCopyPrompt, showAgentChoiceDialog, isCreatingPane, runningCommand, isUpdating]);
272
292
  // Update checking moved to useAutoUpdater
273
293
  // Set default agent choice when detection completes
274
294
  useEffect(() => {
275
295
  if (agentChoice == null && availableAgents.length > 0) {
276
- setAgentChoice(availableAgents[0] || 'claude');
296
+ setAgentChoice(availableAgents[0] || "claude");
277
297
  }
278
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
+ ]);
279
1202
  // Monitor agent status across panes (returns a map of pane ID to status)
280
1203
  const agentStatuses = useAgentStatus({
281
1204
  panes,
282
- 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,
283
1211
  onPaneRemoved: (paneId) => {
284
1212
  // Check if this pane was intentionally closed
285
1213
  // If so, don't re-save - the close action already handled it
@@ -288,28 +1216,21 @@ const DmuxApp = ({ panesFile, projectName, sessionName, settingsFile, projectRoo
288
1216
  }
289
1217
  // Pane was removed unexpectedly (e.g., user killed tmux pane manually)
290
1218
  // Remove it from our tracking
291
- const updatedPanes = panes.filter(p => p.id !== paneId);
1219
+ const updatedPanes = panes.filter((p) => p.id !== paneId);
292
1220
  savePanes(updatedPanes);
293
1221
  },
294
1222
  });
295
- // loadPanes moved to usePanes
296
- // getPanePositions moved to utils/tmux
297
- // Navigation logic moved to hook
298
- const { getCardGridPosition, findCardInDirection } = useNavigation(terminalWidth, panes.length, isLoading);
299
- // findCardInDirection provided by useNavigation
300
- // savePanes moved to usePanes
301
- // applySmartLayout moved to utils/tmux
302
1223
  const createNewPane = async (prompt, agent) => {
303
1224
  setIsCreatingPane(true);
304
- setStatusMessage('Generating slug...');
1225
+ setStatusMessage("Generating slug...");
305
1226
  const slug = await generateSlug(prompt);
306
1227
  setStatusMessage(`Creating worktree: ${slug}...`);
307
1228
  // Get git root directory for consistent worktree placement
308
1229
  let projectRoot;
309
1230
  try {
310
- projectRoot = execSync('git rev-parse --show-toplevel', {
311
- encoding: 'utf-8',
312
- stdio: 'pipe'
1231
+ projectRoot = execSync("git rev-parse --show-toplevel", {
1232
+ encoding: "utf-8",
1233
+ stdio: "pipe",
313
1234
  }).trim();
314
1235
  }
315
1236
  catch {
@@ -317,83 +1238,83 @@ const DmuxApp = ({ panesFile, projectName, sessionName, settingsFile, projectRoo
317
1238
  projectRoot = process.cwd();
318
1239
  }
319
1240
  // Create worktree path inside .dmux/worktrees directory
320
- const worktreePath = path.join(projectRoot, '.dmux', 'worktrees', slug);
1241
+ const worktreePath = path.join(projectRoot, ".dmux", "worktrees", slug);
321
1242
  // Get the original pane ID (where dmux is running) before clearing
322
- const originalPaneId = execSync('tmux display-message -p "#{pane_id}"', { encoding: 'utf-8' }).trim();
323
- // Multiple clearing strategies to prevent artifacts
324
- // 1. Clear screen with ANSI codes
325
- process.stdout.write('\x1b[2J\x1b[H');
326
- // 2. Fill with blank lines to push content off screen
327
- process.stdout.write('\n'.repeat(100));
328
- // 3. Clear tmux history and send clear command
329
- try {
330
- execSync('tmux clear-history', { stdio: 'pipe' });
331
- execSync('tmux send-keys C-l', { stdio: 'pipe' });
332
- }
333
- catch { }
334
- // Wait a bit for clearing to settle
335
- await new Promise(resolve => setTimeout(resolve, 100));
336
- // 4. Force tmux to refresh the display
337
- try {
338
- execSync('tmux refresh-client', { stdio: 'pipe' });
339
- }
340
- 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");
341
1248
  // Get current pane count to determine layout
342
- 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());
343
1250
  // Enable pane borders to show titles
344
1251
  try {
345
- execSync(`tmux set-option -g pane-border-status top`, { stdio: 'pipe' });
1252
+ execSync(`tmux set-option -g pane-border-status top`, { stdio: "pipe" });
346
1253
  }
347
1254
  catch {
348
1255
  // Ignore if already set or fails
349
1256
  }
350
1257
  // Create new pane
351
- 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();
352
1261
  // Wait for pane creation to settle
353
- await new Promise(resolve => setTimeout(resolve, 500));
1262
+ await new Promise((resolve) => setTimeout(resolve, 500));
354
1263
  // Set pane title to match the slug
355
1264
  try {
356
- execSync(`tmux select-pane -t '${paneInfo}' -T "${slug}"`, { stdio: 'pipe' });
1265
+ execSync(`tmux select-pane -t '${paneInfo}' -T "${slug}"`, {
1266
+ stdio: "pipe",
1267
+ });
357
1268
  }
358
1269
  catch {
359
1270
  // Ignore if setting title fails
360
1271
  }
361
- // Apply smart layout based on pane count
362
- const newPaneCount = paneCount + 1;
363
- 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 { }
364
1281
  // Create git worktree and cd into it
365
1282
  // This MUST happen before launching Claude to ensure we're in the right directory
366
1283
  try {
367
1284
  // First, create the worktree and cd into it as a single command
368
1285
  // Use ; instead of && to ensure cd runs even if worktree already exists
369
1286
  const worktreeCmd = `git worktree add "${worktreePath}" -b ${slug} 2>/dev/null ; cd "${worktreePath}"`;
370
- execSync(`tmux send-keys -t '${paneInfo}' '${worktreeCmd}' Enter`, { stdio: 'pipe' });
1287
+ execSync(`tmux send-keys -t '${paneInfo}' '${worktreeCmd}' Enter`, {
1288
+ stdio: "pipe",
1289
+ });
371
1290
  // Wait longer for worktree creation and cd to complete
372
1291
  // This is critical - if we don't wait long enough, Claude will start in the wrong directory
373
- await new Promise(resolve => setTimeout(resolve, 2500));
374
- // Verify we're in the worktree directory by sending pwd command
375
- execSync(`tmux send-keys -t '${paneInfo}' 'echo "Worktree created at:" && pwd' Enter`, { stdio: 'pipe' });
376
- await new Promise(resolve => setTimeout(resolve, 500));
377
- 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.");
378
1299
  }
379
1300
  catch (error) {
380
1301
  // Log error but continue - worktree creation is essential
381
1302
  setStatusMessage(`Warning: Worktree issue: ${error}`);
382
1303
  // Even if worktree creation failed, try to cd to the directory in case it exists
383
- execSync(`tmux send-keys -t '${paneInfo}' 'cd "${worktreePath}" 2>/dev/null || (echo "ERROR: Failed to create/enter worktree ${slug}" && pwd)' Enter`, { stdio: 'pipe' });
384
- 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));
385
1306
  }
386
1307
  // Prepare and send the agent command
387
- let escapedCmd = '';
388
- if (agent === 'claude') {
1308
+ let escapedCmd = "";
1309
+ if (agent === "claude") {
389
1310
  // Claude should always be launched AFTER we're in the worktree directory
390
1311
  let claudeCmd;
391
1312
  if (prompt && prompt.trim()) {
392
1313
  const escapedPrompt = prompt
393
- .replace(/\\/g, '\\\\')
1314
+ .replace(/\\/g, "\\\\")
394
1315
  .replace(/"/g, '\\"')
395
- .replace(/`/g, '\\`')
396
- .replace(/\$/g, '\\$');
1316
+ .replace(/`/g, "\\`")
1317
+ .replace(/\$/g, "\\$");
397
1318
  claudeCmd = `claude "${escapedPrompt}" --permission-mode=acceptEdits`;
398
1319
  }
399
1320
  else {
@@ -401,34 +1322,42 @@ const DmuxApp = ({ panesFile, projectName, sessionName, settingsFile, projectRoo
401
1322
  }
402
1323
  // Send Claude command to new pane
403
1324
  escapedCmd = claudeCmd.replace(/'/g, "'\\''");
404
- execSync(`tmux send-keys -t '${paneInfo}' '${escapedCmd}'`, { stdio: 'pipe' });
405
- 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" });
406
1329
  }
407
- else if (agent === 'opencode') {
1330
+ else if (agent === "opencode") {
408
1331
  // opencode: start the TUI, then paste the prompt and submit
409
1332
  const openCoderCmd = `opencode`;
410
1333
  const escapedOpenCmd = openCoderCmd.replace(/'/g, "'\\''");
411
- execSync(`tmux send-keys -t '${paneInfo}' '${escapedOpenCmd}'`, { stdio: 'pipe' });
412
- 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" });
413
1338
  if (prompt && prompt.trim()) {
414
- await new Promise(resolve => setTimeout(resolve, 1500));
1339
+ await new Promise((resolve) => setTimeout(resolve, 1500));
415
1340
  const bufName = `dmux_prompt_${Date.now()}`;
416
- const promptEsc = prompt.replace(/\\/g, '\\\\').replace(/'/g, "'\\''");
417
- execSync(`tmux set-buffer -b '${bufName}' -- '${promptEsc}'`, { stdio: 'pipe' });
418
- execSync(`tmux paste-buffer -b '${bufName}' -t '${paneInfo}'`, { stdio: 'pipe' });
419
- await new Promise(resolve => setTimeout(resolve, 200));
420
- execSync(`tmux delete-buffer -b '${bufName}'`, { stdio: 'pipe' });
421
- 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" });
422
1351
  }
423
1352
  }
424
- if (agent === 'claude') {
1353
+ if (agent === "claude") {
425
1354
  // Monitor for Claude Code trust prompt and auto-respond
426
1355
  const autoApproveTrust = async () => {
427
1356
  // Wait for Claude to start up before checking for prompts
428
- await new Promise(resolve => setTimeout(resolve, 800));
1357
+ await new Promise((resolve) => setTimeout(resolve, 800));
429
1358
  const maxChecks = 100; // 100 checks * 100ms = 10 seconds total
430
1359
  const checkInterval = 100; // Check every 100ms
431
- let lastContent = '';
1360
+ let lastContent = "";
432
1361
  let stableContentCount = 0;
433
1362
  let promptHandled = false;
434
1363
  // More comprehensive trust prompt patterns
@@ -457,14 +1386,15 @@ const DmuxApp = ({ panesFile, projectName, sessionName, settingsFile, projectRoo
457
1386
  /❯\s*1\.\s*Yes,\s*proceed/i, // New Claude numbered menu format
458
1387
  /Enter to confirm.*Esc to exit/i, // New Claude confirmation format
459
1388
  /1\.\s*Yes,\s*proceed/i, // Yes proceed option
460
- /2\.\s*No,\s*exit/i // No exit option
1389
+ /2\.\s*No,\s*exit/i, // No exit option
461
1390
  ];
462
1391
  for (let i = 0; i < maxChecks; i++) {
463
- await new Promise(resolve => setTimeout(resolve, checkInterval));
1392
+ await new Promise((resolve) => setTimeout(resolve, checkInterval));
464
1393
  try {
465
1394
  // Capture the pane content
466
1395
  const paneContent = capturePaneContent(paneInfo, 30);
467
- if (i % 10 === 0) { // Log every 10 checks (every second)
1396
+ if (i % 10 === 0) {
1397
+ // Log every 10 checks (every second)
468
1398
  }
469
1399
  // Check if content has stabilized (same for 3 checks = prompt is waiting)
470
1400
  if (paneContent === lastContent) {
@@ -475,14 +1405,15 @@ const DmuxApp = ({ panesFile, projectName, sessionName, settingsFile, projectRoo
475
1405
  lastContent = paneContent;
476
1406
  }
477
1407
  // Look for trust prompt in the current content
478
- const hasTrustPrompt = trustPromptPatterns.some(pattern => pattern.test(paneContent));
1408
+ const hasTrustPrompt = trustPromptPatterns.some((pattern) => pattern.test(paneContent));
479
1409
  // Also check if we see specific Claude permission text
480
- const hasClaudePermissionPrompt = paneContent.includes('Do you trust') ||
481
- paneContent.includes('trust the files') ||
482
- paneContent.includes('permission') ||
483
- paneContent.includes('allow') ||
484
- (paneContent.includes('folder') && paneContent.includes('?'));
485
- 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) {
486
1417
  // Content is stable and we found a prompt
487
1418
  if (stableContentCount >= 2) {
488
1419
  // Check if this is the new Claude numbered menu format
@@ -490,44 +1421,57 @@ const DmuxApp = ({ panesFile, projectName, sessionName, settingsFile, projectRoo
490
1421
  /Enter to confirm.*Esc to exit/i.test(paneContent);
491
1422
  if (isNewClaudeFormat) {
492
1423
  // For new Claude format, just press Enter to confirm default "Yes, proceed"
493
- execSync(`tmux send-keys -t '${paneInfo}' Enter`, { stdio: 'pipe' });
1424
+ execSync(`tmux send-keys -t '${paneInfo}' Enter`, {
1425
+ stdio: "pipe",
1426
+ });
494
1427
  }
495
1428
  else {
496
1429
  // Try multiple response methods for older formats
497
1430
  // Method 1: Send 'y' followed by Enter (most explicit)
498
- execSync(`tmux send-keys -t '${paneInfo}' 'y'`, { stdio: 'pipe' });
499
- await new Promise(resolve => setTimeout(resolve, 50));
500
- 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
+ });
501
1438
  // Method 2: Just Enter (if it's a yes/no with default yes)
502
- await new Promise(resolve => setTimeout(resolve, 100));
503
- 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
+ });
504
1443
  }
505
1444
  // Mark as handled to avoid duplicate responses
506
1445
  promptHandled = true;
507
1446
  // Wait and check if prompt is gone
508
- await new Promise(resolve => setTimeout(resolve, 500));
1447
+ await new Promise((resolve) => setTimeout(resolve, 500));
509
1448
  // Verify the prompt is gone
510
1449
  const updatedContent = capturePaneContent(paneInfo, 10);
511
1450
  // If trust prompt is gone, check if we need to resend the Claude command
512
- const promptGone = !trustPromptPatterns.some(p => p.test(updatedContent));
1451
+ const promptGone = !trustPromptPatterns.some((p) => p.test(updatedContent));
513
1452
  if (promptGone) {
514
1453
  // Check if Claude is running or if we need to restart it
515
- const claudeRunning = updatedContent.includes('Claude') ||
516
- updatedContent.includes('claude') ||
517
- updatedContent.includes('Assistant') ||
518
- (prompt && updatedContent.includes(prompt.substring(0, Math.min(20, prompt.length))));
519
- if (!claudeRunning && !updatedContent.includes('$')) {
520
- await new Promise(resolve => setTimeout(resolve, 300));
521
- execSync(`tmux send-keys -t '${paneInfo}' '${escapedCmd}'`, { stdio: 'pipe' });
522
- 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
+ });
523
1465
  }
524
1466
  break;
525
1467
  }
526
1468
  }
527
1469
  }
528
1470
  // If we see Claude is already running without prompts, we're done
529
- if (!hasTrustPrompt && !hasClaudePermissionPrompt &&
530
- (paneContent.includes('Claude') || paneContent.includes('Assistant'))) {
1471
+ if (!hasTrustPrompt &&
1472
+ !hasClaudePermissionPrompt &&
1473
+ (paneContent.includes("Claude") ||
1474
+ paneContent.includes("Assistant"))) {
531
1475
  break;
532
1476
  }
533
1477
  }
@@ -537,37 +1481,35 @@ const DmuxApp = ({ panesFile, projectName, sessionName, settingsFile, projectRoo
537
1481
  }
538
1482
  };
539
1483
  // Start monitoring for trust prompt in background
540
- autoApproveTrust().catch(err => {
541
- });
1484
+ autoApproveTrust().catch((err) => { });
542
1485
  }
543
1486
  // Keep focus on the new pane
544
- execSync(`tmux select-pane -t '${paneInfo}'`, { stdio: 'pipe' });
1487
+ execSync(`tmux select-pane -t '${paneInfo}'`, { stdio: "pipe" });
545
1488
  // Save pane info
546
1489
  const newPane = {
547
1490
  id: `dmux-${Date.now()}`,
548
1491
  slug,
549
- prompt: prompt || 'No initial prompt',
1492
+ prompt: prompt || "No initial prompt",
550
1493
  paneId: paneInfo,
551
1494
  worktreePath,
552
- agent
1495
+ agent,
553
1496
  };
554
1497
  const updatedPanes = [...panes, newPane];
555
1498
  await savePanes(updatedPanes);
556
1499
  // Switch back to the original pane (where dmux is running)
557
- execSync(`tmux select-pane -t '${originalPaneId}'`, { stdio: 'pipe' });
1500
+ execSync(`tmux select-pane -t '${originalPaneId}'`, { stdio: "pipe" });
558
1501
  // Re-set the title for the dmux pane
559
1502
  try {
560
- 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" });
561
1504
  }
562
1505
  catch {
563
1506
  // Ignore if setting title fails
564
1507
  }
565
1508
  // Clear the screen and redraw the UI
566
- process.stdout.write('\x1b[2J\x1b[H');
1509
+ process.stdout.write("\x1b[2J\x1b[H");
567
1510
  // Reset the creating pane flag and refresh
568
1511
  setIsCreatingPane(false);
569
- setStatusMessage('');
570
- setNewPanePrompt('');
1512
+ setStatusMessage("");
571
1513
  // Force a reload of panes to ensure UI is up to date
572
1514
  await loadPanes();
573
1515
  };
@@ -575,28 +1517,32 @@ const DmuxApp = ({ panesFile, projectName, sessionName, settingsFile, projectRoo
575
1517
  try {
576
1518
  // Enable pane borders to show titles (if not already enabled)
577
1519
  try {
578
- execSync(`tmux set-option -g pane-border-status top`, { stdio: 'pipe' });
1520
+ execSync(`tmux set-option -g pane-border-status top`, { stdio: "pipe" });
579
1521
  }
580
1522
  catch {
581
1523
  // Ignore if already set or fails
582
1524
  }
583
- execSync(`tmux select-pane -t '${paneId}'`, { stdio: 'pipe' });
584
- setStatusMessage('Jumped to pane');
585
- setTimeout(() => setStatusMessage(''), 2000);
1525
+ execSync(`tmux select-pane -t '${paneId}'`, { stdio: "pipe" });
1526
+ // Clear screen after jump to remove artifacts
1527
+ clearScreen();
1528
+ setStatusMessage("Jumped to pane");
1529
+ setTimeout(() => setStatusMessage(""), 2000);
586
1530
  }
587
1531
  catch {
588
- setStatusMessage('Failed to jump - pane may be closed');
589
- setTimeout(() => setStatusMessage(''), 2000);
1532
+ setStatusMessage("Failed to jump - pane may be closed");
1533
+ setTimeout(() => setStatusMessage(""), 2000);
590
1534
  }
591
1535
  };
592
1536
  const runCommand = async (type, pane) => {
593
1537
  if (!pane.worktreePath) {
594
- setStatusMessage('No worktree path for this pane');
595
- setTimeout(() => setStatusMessage(''), 2000);
1538
+ setStatusMessage("No worktree path for this pane");
1539
+ setTimeout(() => setStatusMessage(""), 2000);
596
1540
  return;
597
1541
  }
598
- const command = type === 'test' ? projectSettings.testCommand : projectSettings.devCommand;
599
- 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;
600
1546
  if (!command) {
601
1547
  // No command configured, prompt user
602
1548
  setShowCommandPrompt(type);
@@ -615,35 +1561,37 @@ const DmuxApp = ({ panesFile, projectName, sessionName, settingsFile, projectRoo
615
1561
  setRunningCommand(true);
616
1562
  setStatusMessage(`Starting ${type} in background window...`);
617
1563
  // Kill existing window if present
618
- const existingWindowId = type === 'test' ? pane.testWindowId : pane.devWindowId;
1564
+ const existingWindowId = type === "test" ? pane.testWindowId : pane.devWindowId;
619
1565
  if (existingWindowId) {
620
1566
  try {
621
- execSync(`tmux kill-window -t '${existingWindowId}'`, { stdio: 'pipe' });
1567
+ execSync(`tmux kill-window -t '${existingWindowId}'`, {
1568
+ stdio: "pipe",
1569
+ });
622
1570
  }
623
1571
  catch { }
624
1572
  }
625
1573
  // Create a new background window for the command
626
1574
  const windowName = `${pane.slug}-${type}`;
627
- 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();
628
1576
  // Create a log file to capture output
629
1577
  const logFile = `/tmp/dmux-${pane.id}-${type}.log`;
630
1578
  // Build the command with output capture
631
- const fullCommand = type === 'test'
1579
+ const fullCommand = type === "test"
632
1580
  ? `cd "${pane.worktreePath}" && ${command} 2>&1 | tee ${logFile}`
633
1581
  : `cd "${pane.worktreePath}" && ${command} 2>&1 | tee ${logFile}`;
634
1582
  // Send the command to the new window
635
- execSync(`tmux send-keys -t '${windowId}' '${fullCommand.replace(/'/g, "'\\''")}'`, { stdio: 'pipe' });
636
- 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" });
637
1585
  // Update pane with window info
638
1586
  const updatedPane = {
639
1587
  ...pane,
640
- [type === 'test' ? 'testWindowId' : 'devWindowId']: windowId,
641
- [type === 'test' ? 'testStatus' : 'devStatus']: 'running'
1588
+ [type === "test" ? "testWindowId" : "devWindowId"]: windowId,
1589
+ [type === "test" ? "testStatus" : "devStatus"]: "running",
642
1590
  };
643
- const updatedPanes = panes.map(p => p.id === pane.id ? updatedPane : p);
1591
+ const updatedPanes = panes.map((p) => p.id === pane.id ? updatedPane : p);
644
1592
  await savePanes(updatedPanes);
645
1593
  // Start monitoring the output
646
- if (type === 'test') {
1594
+ if (type === "test") {
647
1595
  // For tests, monitor for completion
648
1596
  setTimeout(() => monitorTestOutput(pane.id, logFile), 2000);
649
1597
  }
@@ -652,13 +1600,13 @@ const DmuxApp = ({ panesFile, projectName, sessionName, settingsFile, projectRoo
652
1600
  setTimeout(() => monitorDevOutput(pane.id, logFile), 2000);
653
1601
  }
654
1602
  setRunningCommand(false);
655
- setStatusMessage(`${type === 'test' ? 'Test' : 'Dev server'} started in background`);
656
- setTimeout(() => setStatusMessage(''), 3000);
1603
+ setStatusMessage(`${type === "test" ? "Test" : "Dev server"} started in background`);
1604
+ setTimeout(() => setStatusMessage(""), 3000);
657
1605
  }
658
1606
  catch (error) {
659
1607
  setRunningCommand(false);
660
1608
  setStatusMessage(`Failed to run ${type} command`);
661
- setTimeout(() => setStatusMessage(''), 3000);
1609
+ setTimeout(() => setStatusMessage(""), 3000);
662
1610
  }
663
1611
  };
664
1612
  // Update handling moved to useAutoUpdater
@@ -666,50 +1614,58 @@ const DmuxApp = ({ panesFile, projectName, sessionName, settingsFile, projectRoo
666
1614
  const clearScreen = () => {
667
1615
  // CRITICAL: Force Ink to re-render FIRST, before clearing
668
1616
  // This prevents blank screen by ensuring React starts rendering immediately
669
- setForceRepaintTrigger(prev => prev + 1);
1617
+ setForceRepaintTrigger((prev) => prev + 1);
670
1618
  // Multiple clearing strategies to prevent artifacts
671
1619
  // 1. Clear screen with ANSI codes
672
- process.stdout.write('\x1b[2J\x1b[H');
1620
+ process.stdout.write("\x1b[2J\x1b[H");
673
1621
  // 2. Clear tmux history
674
1622
  try {
675
- execSync('tmux clear-history', { stdio: 'pipe' });
1623
+ execSync("tmux clear-history", { stdio: "pipe" });
676
1624
  }
677
1625
  catch { }
678
1626
  // 3. Force tmux to refresh the display
679
1627
  try {
680
- execSync('tmux refresh-client', { stdio: 'pipe' });
1628
+ execSync("tmux refresh-client", { stdio: "pipe" });
681
1629
  }
682
1630
  catch { }
683
1631
  };
684
1632
  // Cleanup function for exit
685
1633
  const cleanExit = () => {
686
1634
  // Clear screen before exiting Ink
687
- process.stdout.write('\x1b[2J\x1b[3J\x1b[H');
1635
+ process.stdout.write("\x1b[2J\x1b[3J\x1b[H");
688
1636
  // Exit the Ink app (this cleans up the React tree)
689
1637
  exit();
690
1638
  // Give Ink a moment to clean up its rendering, then do final cleanup
691
1639
  setTimeout(() => {
692
1640
  // Multiple aggressive clearing strategies
693
- process.stdout.write('\x1b[2J\x1b[H'); // Clear screen and move cursor to home
694
- process.stdout.write('\x1b[3J'); // Clear scrollback buffer
695
- 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
696
1644
  // Clear tmux history and pane
697
1645
  try {
698
- execSync('tmux clear-history', { stdio: 'pipe' });
699
- execSync('tmux send-keys C-l', { stdio: 'pipe' });
1646
+ execSync("tmux clear-history", { stdio: "pipe" });
1647
+ execSync("tmux send-keys C-l", { stdio: "pipe" });
700
1648
  }
701
1649
  catch { }
702
1650
  // One more final clear
703
- process.stdout.write('\x1b[2J\x1b[H');
1651
+ process.stdout.write("\x1b[2J\x1b[H");
704
1652
  // Show clean goodbye message
705
- 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");
706
1654
  // Exit process
707
1655
  process.exit(0);
708
1656
  }, 100);
709
1657
  };
710
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
+ }
711
1667
  // Handle Ctrl+C for quit confirmation (must be first, before any other checks)
712
- if (key.ctrl && input === 'c') {
1668
+ if (key.ctrl && input === "c") {
713
1669
  if (quitConfirmMode) {
714
1670
  // Second Ctrl+C - actually quit
715
1671
  cleanExit();
@@ -724,45 +1680,10 @@ const DmuxApp = ({ panesFile, projectName, sessionName, settingsFile, projectRoo
724
1680
  }
725
1681
  return;
726
1682
  }
727
- if (isCreatingPane || runningCommand || isUpdating || isLoading || isCreatingTunnel) {
1683
+ if (isCreatingPane || runningCommand || isUpdating || isLoading) {
728
1684
  // Disable input while performing operations or loading
729
1685
  return;
730
1686
  }
731
- // Handle kebab menu navigation
732
- if (showKebabMenu && kebabMenuPaneIndex !== null) {
733
- const currentPane = panes[kebabMenuPaneIndex];
734
- const availableActions = kebabMenuActions;
735
- if (key.escape) {
736
- setShowKebabMenu(false);
737
- setKebabMenuPaneIndex(null);
738
- setKebabMenuOption(0);
739
- setKebabMenuActions([]);
740
- return;
741
- }
742
- else if (key.upArrow) {
743
- setKebabMenuOption(Math.max(0, kebabMenuOption - 1));
744
- return;
745
- }
746
- else if (key.downArrow) {
747
- setKebabMenuOption(Math.min(availableActions.length - 1, kebabMenuOption + 1));
748
- return;
749
- }
750
- else if (key.return) {
751
- // Execute the selected menu action
752
- setShowKebabMenu(false);
753
- const selectedAction = availableActions[kebabMenuOption];
754
- if (selectedAction) {
755
- // Use the action system to execute
756
- actionSystem.executeAction(selectedAction.id, currentPane, { mainBranch: getMainBranch() });
757
- }
758
- setKebabMenuPaneIndex(null);
759
- setKebabMenuOption(0);
760
- setKebabMenuActions([]);
761
- return;
762
- }
763
- // Don't process other inputs while menu is open
764
- return;
765
- }
766
1687
  // Handle quit confirm mode - ESC cancels it
767
1688
  if (quitConfirmMode) {
768
1689
  if (key.escape) {
@@ -771,288 +1692,8 @@ const DmuxApp = ({ panesFile, projectName, sessionName, settingsFile, projectRoo
771
1692
  }
772
1693
  // Allow other inputs to continue (don't return early)
773
1694
  }
774
- // Handle QR code view
775
- if (showQRCode) {
776
- if (key.escape) {
777
- setShowQRCode(false);
778
- }
779
- return;
780
- }
781
- // Handle hooks dialog
782
- if (showHooksDialog) {
783
- if (key.escape) {
784
- setShowHooksDialog(false);
785
- setHooksSelectedIndex(0);
786
- // Go back to settings dialog
787
- setShowSettingsDialog(true);
788
- setSettingsMode('list');
789
- return;
790
- }
791
- else if (key.upArrow) {
792
- setHooksSelectedIndex(Math.max(0, hooksSelectedIndex - 1));
793
- return;
794
- }
795
- else if (key.downArrow) {
796
- setHooksSelectedIndex(Math.min(hooksData.length - 1, hooksSelectedIndex + 1));
797
- return;
798
- }
799
- else if (input === 'e') {
800
- // Edit hooks using an agent
801
- setShowHooksDialog(false);
802
- setHooksSelectedIndex(0);
803
- 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";
804
- setPendingPrompt(prompt);
805
- setNewPanePrompt(prompt);
806
- // Choose agent
807
- const agents = availableAgents;
808
- if (agents.length === 0) {
809
- createNewPaneHook(prompt);
810
- }
811
- else if (agents.length === 1) {
812
- createNewPaneHook(prompt, agents[0]);
813
- }
814
- else {
815
- setShowAgentChoiceDialog(true);
816
- setAgentChoice(agentChoice || 'claude');
817
- }
818
- return;
819
- }
820
- return;
821
- }
822
- // Handle settings dialog
823
- if (showSettingsDialog) {
824
- if (key.escape) {
825
- if (settingsMode === 'list') {
826
- // Close settings dialog
827
- setShowSettingsDialog(false);
828
- setSettingsMode('list');
829
- setSettingsSelectedIndex(0);
830
- setSettingsEditingKey(undefined);
831
- }
832
- else {
833
- // Go back to list
834
- setSettingsMode('list');
835
- setSettingsEditingKey(undefined);
836
- setSettingsEditingValueIndex(0);
837
- setSettingsScopeIndex(0);
838
- }
839
- return;
840
- }
841
- else if (key.upArrow) {
842
- if (settingsMode === 'list') {
843
- setSettingsSelectedIndex(Math.max(0, settingsSelectedIndex - 1));
844
- }
845
- else if (settingsMode === 'edit') {
846
- const currentDef = SETTING_DEFINITIONS[settingsSelectedIndex];
847
- const maxIndex = currentDef.type === 'boolean' ? 1 : (currentDef.options?.length || 1) - 1;
848
- setSettingsEditingValueIndex(Math.max(0, settingsEditingValueIndex - 1));
849
- }
850
- else if (settingsMode === 'scope') {
851
- setSettingsScopeIndex(Math.max(0, settingsScopeIndex - 1));
852
- }
853
- return;
854
- }
855
- else if (key.downArrow) {
856
- if (settingsMode === 'list') {
857
- setSettingsSelectedIndex(Math.min(SETTING_DEFINITIONS.length - 1, settingsSelectedIndex + 1));
858
- }
859
- else if (settingsMode === 'edit') {
860
- const currentDef = SETTING_DEFINITIONS[settingsSelectedIndex];
861
- const maxIndex = currentDef.type === 'boolean' ? 1 : (currentDef.options?.length || 1) - 1;
862
- setSettingsEditingValueIndex(Math.min(maxIndex, settingsEditingValueIndex + 1));
863
- }
864
- else if (settingsMode === 'scope') {
865
- setSettingsScopeIndex(Math.min(1, settingsScopeIndex + 1));
866
- }
867
- return;
868
- }
869
- else if (key.return) {
870
- if (settingsMode === 'list') {
871
- const currentDef = SETTING_DEFINITIONS[settingsSelectedIndex];
872
- // Handle action type - trigger the action instead of editing
873
- if (currentDef.type === 'action') {
874
- if (currentDef.key === 'hooks') {
875
- // Show hooks dialog
876
- setShowSettingsDialog(false);
877
- const { hasHook } = await import('./utils/hooks.js');
878
- const allHookTypes = [
879
- 'before_pane_create',
880
- 'pane_created',
881
- 'worktree_created',
882
- 'before_pane_close',
883
- 'pane_closed',
884
- 'before_worktree_remove',
885
- 'worktree_removed',
886
- 'pre_merge',
887
- 'post_merge',
888
- 'run_test',
889
- 'run_dev',
890
- ];
891
- const hooks = allHookTypes.map(hookName => ({
892
- name: hookName,
893
- active: hasHook(projectRoot || process.cwd(), hookName)
894
- }));
895
- setHooksData(hooks);
896
- setShowHooksDialog(true);
897
- setHooksSelectedIndex(0);
898
- }
899
- return;
900
- }
901
- // Enter edit mode for regular settings
902
- setSettingsEditingKey(currentDef.key);
903
- setSettingsMode('edit');
904
- // Set initial value index based on current setting
905
- const currentValue = settingsManager.getSetting(currentDef.key);
906
- if (currentDef.type === 'boolean') {
907
- setSettingsEditingValueIndex(currentValue ? 0 : 1);
908
- }
909
- else if (currentDef.type === 'select' && currentDef.options) {
910
- const optIndex = currentDef.options.findIndex(o => o.value === currentValue);
911
- setSettingsEditingValueIndex(Math.max(0, optIndex));
912
- }
913
- }
914
- else if (settingsMode === 'edit') {
915
- // Go to scope selection
916
- setSettingsMode('scope');
917
- setSettingsScopeIndex(0);
918
- }
919
- else if (settingsMode === 'scope') {
920
- // Save the setting
921
- const currentDef = SETTING_DEFINITIONS[settingsSelectedIndex];
922
- const scope = settingsScopeIndex === 0 ? 'global' : 'project';
923
- // Only save if this is not an action type (actions don't have values)
924
- if (currentDef.type !== 'action') {
925
- // Calculate the new value
926
- let newValue;
927
- if (currentDef.type === 'boolean') {
928
- newValue = settingsEditingValueIndex === 0;
929
- }
930
- else if (currentDef.type === 'select' && currentDef.options) {
931
- newValue = currentDef.options[settingsEditingValueIndex]?.value || '';
932
- }
933
- // Update the setting - cast key to proper type since we know it's not an action
934
- settingsManager.updateSetting(currentDef.key, newValue, scope);
935
- }
936
- // Reset to list view
937
- setSettingsMode('list');
938
- setSettingsEditingKey(undefined);
939
- setSettingsEditingValueIndex(0);
940
- setSettingsScopeIndex(0);
941
- setStatusMessage(`Setting saved (${scope})`);
942
- setTimeout(() => setStatusMessage(''), 2000);
943
- }
944
- return;
945
- }
946
- return;
947
- }
948
- // Handle action system confirm dialog
949
- if (actionSystem.actionState.showConfirmDialog) {
950
- if (key.escape) {
951
- if (actionSystem.actionState.onConfirmNo) {
952
- actionSystem.executeCallback(actionSystem.actionState.onConfirmNo);
953
- }
954
- else {
955
- actionSystem.setActionState(prev => ({ ...prev, showConfirmDialog: false }));
956
- }
957
- return;
958
- }
959
- else if (key.upArrow) {
960
- actionSystem.setActionState(prev => ({
961
- ...prev,
962
- confirmSelectedIndex: Math.max(0, prev.confirmSelectedIndex - 1)
963
- }));
964
- return;
965
- }
966
- else if (key.downArrow) {
967
- actionSystem.setActionState(prev => ({
968
- ...prev,
969
- confirmSelectedIndex: Math.min(1, prev.confirmSelectedIndex + 1)
970
- }));
971
- return;
972
- }
973
- else if (key.return) {
974
- // Execute based on selected index
975
- if (actionSystem.actionState.confirmSelectedIndex === 0) {
976
- if (actionSystem.actionState.onConfirmYes) {
977
- actionSystem.executeCallback(actionSystem.actionState.onConfirmYes);
978
- }
979
- }
980
- else {
981
- if (actionSystem.actionState.onConfirmNo) {
982
- actionSystem.executeCallback(actionSystem.actionState.onConfirmNo);
983
- }
984
- else {
985
- actionSystem.setActionState(prev => ({ ...prev, showConfirmDialog: false }));
986
- }
987
- }
988
- return;
989
- }
990
- else if (input === 'y' || input === 'Y') {
991
- // Shortcut: yes
992
- if (actionSystem.actionState.onConfirmYes) {
993
- actionSystem.executeCallback(actionSystem.actionState.onConfirmYes);
994
- }
995
- return;
996
- }
997
- else if (input === 'n' || input === 'N') {
998
- // Shortcut: no
999
- if (actionSystem.actionState.onConfirmNo) {
1000
- actionSystem.executeCallback(actionSystem.actionState.onConfirmNo);
1001
- }
1002
- else {
1003
- actionSystem.setActionState(prev => ({ ...prev, showConfirmDialog: false }));
1004
- }
1005
- return;
1006
- }
1007
- return;
1008
- }
1009
- // Handle action system input dialog
1010
- if (actionSystem.actionState.showInputDialog) {
1011
- if (key.escape) {
1012
- actionSystem.setActionState(prev => ({ ...prev, showInputDialog: false }));
1013
- return;
1014
- }
1015
- else if (key.return) {
1016
- if (actionSystem.actionState.onInputSubmit) {
1017
- actionSystem.executeCallback(async () => actionSystem.actionState.onInputSubmit(actionSystem.actionState.inputValue));
1018
- }
1019
- return;
1020
- }
1021
- // Let CleanTextInput handle all other key events
1022
- return;
1023
- }
1024
- // Handle action system choice dialog
1025
- if (actionSystem.actionState.showChoiceDialog) {
1026
- if (key.escape) {
1027
- actionSystem.setActionState(prev => ({ ...prev, showChoiceDialog: false }));
1028
- return;
1029
- }
1030
- else if (key.upArrow) {
1031
- actionSystem.setActionState(prev => ({
1032
- ...prev,
1033
- choiceSelectedIndex: Math.max(0, prev.choiceSelectedIndex - 1)
1034
- }));
1035
- return;
1036
- }
1037
- else if (key.downArrow) {
1038
- const maxIndex = actionSystem.actionState.choiceOptions.length - 1;
1039
- actionSystem.setActionState(prev => ({
1040
- ...prev,
1041
- choiceSelectedIndex: Math.min(maxIndex, prev.choiceSelectedIndex + 1)
1042
- }));
1043
- return;
1044
- }
1045
- else if (key.return) {
1046
- const selectedOption = actionSystem.actionState.choiceOptions[actionSystem.actionState.choiceSelectedIndex];
1047
- if (selectedOption && actionSystem.actionState.onChoiceSelect) {
1048
- actionSystem.executeCallback(async () => actionSystem.actionState.onChoiceSelect(selectedOption.id));
1049
- }
1050
- return;
1051
- }
1052
- return;
1053
- }
1054
1695
  if (showFileCopyPrompt) {
1055
- if (input === 'y' || input === 'Y') {
1696
+ if (input === "y" || input === "Y") {
1056
1697
  setShowFileCopyPrompt(false);
1057
1698
  const selectedPane = panes[selectedIndex];
1058
1699
  if (selectedPane && selectedPane.worktreePath && currentCommandType) {
@@ -1060,7 +1701,7 @@ const DmuxApp = ({ panesFile, projectName, sessionName, settingsFile, projectRoo
1060
1701
  // Mark as not first run and continue with command
1061
1702
  const newSettings = {
1062
1703
  ...projectSettings,
1063
- [currentCommandType === 'test' ? 'firstTestRun' : 'firstDevRun']: true
1704
+ [currentCommandType === "test" ? "firstTestRun" : "firstDevRun"]: true,
1064
1705
  };
1065
1706
  await saveSettings(newSettings);
1066
1707
  // Now run the actual command
@@ -1068,14 +1709,14 @@ const DmuxApp = ({ panesFile, projectName, sessionName, settingsFile, projectRoo
1068
1709
  }
1069
1710
  setCurrentCommandType(null);
1070
1711
  }
1071
- else if (input === 'n' || input === 'N' || key.escape) {
1712
+ else if (input === "n" || input === "N" || key.escape) {
1072
1713
  setShowFileCopyPrompt(false);
1073
1714
  const selectedPane = panes[selectedIndex];
1074
1715
  if (selectedPane && currentCommandType) {
1075
1716
  // Mark as not first run and continue without copying
1076
1717
  const newSettings = {
1077
1718
  ...projectSettings,
1078
- [currentCommandType === 'test' ? 'firstTestRun' : 'firstDevRun']: true
1719
+ [currentCommandType === "test" ? "firstTestRun" : "firstDevRun"]: true,
1079
1720
  };
1080
1721
  await saveSettings(newSettings);
1081
1722
  // Now run the actual command
@@ -1085,36 +1726,13 @@ const DmuxApp = ({ panesFile, projectName, sessionName, settingsFile, projectRoo
1085
1726
  }
1086
1727
  return;
1087
1728
  }
1088
- if (showAgentChoiceDialog) {
1089
- if (key.escape) {
1090
- setShowAgentChoiceDialog(false);
1091
- setShowNewPaneDialog(true);
1092
- setNewPanePrompt(pendingPrompt);
1093
- setPendingPrompt('');
1094
- }
1095
- else if (key.leftArrow || input === '1' || (input && input.toLowerCase() === 'c')) {
1096
- setAgentChoice('claude');
1097
- }
1098
- else if (key.rightArrow || input === '2' || (input && input.toLowerCase() === 'o')) {
1099
- setAgentChoice('opencode');
1100
- }
1101
- else if (key.return) {
1102
- const chosen = agentChoice || (availableAgents[0] || 'claude');
1103
- const promptValue = pendingPrompt;
1104
- setShowAgentChoiceDialog(false);
1105
- setPendingPrompt('');
1106
- await createNewPaneHook(promptValue, chosen);
1107
- setNewPanePrompt('');
1108
- }
1109
- return;
1110
- }
1111
1729
  if (showCommandPrompt) {
1112
1730
  if (key.escape) {
1113
1731
  setShowCommandPrompt(null);
1114
- setCommandInput('');
1732
+ setCommandInput("");
1115
1733
  }
1116
1734
  else if (key.return) {
1117
- if (commandInput.trim() === '') {
1735
+ if (commandInput.trim() === "") {
1118
1736
  // If empty, suggest a default command based on package manager
1119
1737
  const suggested = await suggestCommand(showCommandPrompt);
1120
1738
  if (suggested) {
@@ -1125,13 +1743,15 @@ const DmuxApp = ({ panesFile, projectName, sessionName, settingsFile, projectRoo
1125
1743
  // User provided manual command
1126
1744
  const newSettings = {
1127
1745
  ...projectSettings,
1128
- [showCommandPrompt === 'test' ? 'testCommand' : 'devCommand']: commandInput.trim()
1746
+ [showCommandPrompt === "test" ? "testCommand" : "devCommand"]: commandInput.trim(),
1129
1747
  };
1130
1748
  await saveSettings(newSettings);
1131
1749
  const selectedPane = panes[selectedIndex];
1132
1750
  if (selectedPane) {
1133
1751
  // Check if first run
1134
- const isFirstRun = showCommandPrompt === 'test' ? !projectSettings.firstTestRun : !projectSettings.firstDevRun;
1752
+ const isFirstRun = showCommandPrompt === "test"
1753
+ ? !projectSettings.firstTestRun
1754
+ : !projectSettings.firstDevRun;
1135
1755
  if (isFirstRun) {
1136
1756
  setCurrentCommandType(showCommandPrompt);
1137
1757
  setShowCommandPrompt(null);
@@ -1140,171 +1760,210 @@ const DmuxApp = ({ panesFile, projectName, sessionName, settingsFile, projectRoo
1140
1760
  else {
1141
1761
  await runCommandInternal(showCommandPrompt, selectedPane);
1142
1762
  setShowCommandPrompt(null);
1143
- setCommandInput('');
1763
+ setCommandInput("");
1144
1764
  }
1145
1765
  }
1146
1766
  else {
1147
1767
  setShowCommandPrompt(null);
1148
- setCommandInput('');
1768
+ setCommandInput("");
1149
1769
  }
1150
1770
  }
1151
1771
  }
1152
1772
  return;
1153
1773
  }
1154
- if (showNewPaneDialog) {
1155
- if (key.escape) {
1156
- setShowNewPaneDialog(false);
1157
- setNewPanePrompt('');
1158
- }
1159
- // TextInput handles other input events
1160
- return;
1161
- }
1162
1774
  // Handle directional navigation with spatial awareness based on card grid layout
1163
1775
  if (key.upArrow || key.downArrow || key.leftArrow || key.rightArrow) {
1164
1776
  let targetIndex = null;
1165
1777
  if (key.upArrow) {
1166
- targetIndex = findCardInDirection(selectedIndex, 'up');
1778
+ targetIndex = findCardInDirection(selectedIndex, "up");
1167
1779
  }
1168
1780
  else if (key.downArrow) {
1169
- targetIndex = findCardInDirection(selectedIndex, 'down');
1781
+ targetIndex = findCardInDirection(selectedIndex, "down");
1170
1782
  }
1171
1783
  else if (key.leftArrow) {
1172
- targetIndex = findCardInDirection(selectedIndex, 'left');
1784
+ targetIndex = findCardInDirection(selectedIndex, "left");
1173
1785
  }
1174
1786
  else if (key.rightArrow) {
1175
- targetIndex = findCardInDirection(selectedIndex, 'right');
1787
+ targetIndex = findCardInDirection(selectedIndex, "right");
1176
1788
  }
1177
1789
  if (targetIndex !== null) {
1178
1790
  setSelectedIndex(targetIndex);
1179
1791
  }
1180
1792
  return;
1181
1793
  }
1182
- if ((input === 'm' || key.return) && selectedIndex < panes.length) {
1183
- // Open kebab menu for selected pane
1184
- const selectedPane = panes[selectedIndex];
1185
- const actions = getAvailableActions(selectedPane, projectSettings);
1186
- setKebabMenuActions(actions);
1187
- setShowKebabMenu(true);
1188
- setKebabMenuPaneIndex(selectedIndex);
1189
- setKebabMenuOption(0);
1190
- }
1191
- else if (input === 's') {
1192
- // Open settings dialog
1193
- setShowSettingsDialog(true);
1194
- setSettingsMode('list');
1195
- setSettingsSelectedIndex(0);
1196
- }
1197
- 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") {
1198
1817
  cleanExit();
1199
1818
  }
1200
- else if (input === 'r' && server) {
1201
- // Create tunnel if not already created, then show QR code
1202
- if (!tunnelUrl) {
1203
- setIsCreatingTunnel(true);
1204
- setStatusMessage('Creating tunnel...');
1205
- try {
1206
- const url = await server.startTunnel();
1207
- setTunnelUrl(url);
1208
- setStatusMessage('');
1209
- setShowQRCode(true);
1210
- }
1211
- catch (error) {
1212
- setStatusMessage('Failed to create tunnel');
1213
- setTimeout(() => setStatusMessage(''), 3000);
1214
- }
1215
- finally {
1216
- setIsCreatingTunnel(false);
1217
- }
1819
+ else if (input === "r" && server) {
1820
+ // Handle remote tunnel
1821
+ if (tunnelUrl) {
1822
+ // Tunnel exists - open popup with QR code
1823
+ await launchRemotePopup();
1218
1824
  }
1219
- else {
1220
- // Tunnel already exists, just show the QR code
1221
- 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
+ })();
1222
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;
1223
1850
  }
1224
- else if (!isLoading && (input === 'n' || (key.return && selectedIndex === panes.length))) {
1225
- // Clear the prompt and show dialog in next tick to prevent 'n' bleeding through
1226
- setNewPanePrompt('');
1227
- setShowNewPaneDialog(true);
1228
- 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;
1229
1877
  }
1230
- else if (input === 'j' && selectedIndex < panes.length) {
1878
+ else if (input === "j" && selectedIndex < panes.length) {
1231
1879
  // Jump to pane (NEW: using action system)
1232
1880
  StateManager.getInstance().setDebugMessage(`Jumping to pane: ${panes[selectedIndex].slug}`);
1233
- setTimeout(() => StateManager.getInstance().setDebugMessage(''), 2000);
1881
+ setTimeout(() => StateManager.getInstance().setDebugMessage(""), 2000);
1234
1882
  actionSystem.executeAction(PaneAction.VIEW, panes[selectedIndex]);
1235
1883
  }
1236
- else if (input === 'x' && selectedIndex < panes.length) {
1884
+ else if (input === "x" && selectedIndex < panes.length) {
1237
1885
  // Close pane (NEW: using action system)
1238
1886
  StateManager.getInstance().setDebugMessage(`Closing pane: ${panes[selectedIndex].slug}`);
1239
- setTimeout(() => StateManager.getInstance().setDebugMessage(''), 2000);
1887
+ setTimeout(() => StateManager.getInstance().setDebugMessage(""), 2000);
1240
1888
  actionSystem.executeAction(PaneAction.CLOSE, panes[selectedIndex]);
1241
1889
  }
1242
1890
  else if (key.return && selectedIndex < panes.length) {
1243
1891
  // Jump to pane (NEW: using action system)
1244
1892
  StateManager.getInstance().setDebugMessage(`Jumping to pane: ${panes[selectedIndex].slug}`);
1245
- setTimeout(() => StateManager.getInstance().setDebugMessage(''), 2000);
1893
+ setTimeout(() => StateManager.getInstance().setDebugMessage(""), 2000);
1246
1894
  actionSystem.executeAction(PaneAction.VIEW, panes[selectedIndex]);
1247
1895
  }
1248
1896
  });
1249
- // If showing QR code, render only that
1250
- if (showQRCode && tunnelUrl) {
1251
- return (React.createElement(Box, { flexDirection: "column" },
1252
- React.createElement(Box, { marginBottom: 1 },
1253
- React.createElement(Text, { bold: true, color: "cyan" }, "dmux - Remote Access")),
1254
- React.createElement(QRCode, { url: tunnelUrl }),
1255
- React.createElement(Box, { marginTop: 1 },
1256
- 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;
1257
1909
  }
1258
- return (React.createElement(Box, { flexDirection: "column" },
1259
- React.createElement(PanesGrid, { panes: panes, selectedIndex: selectedIndex, isLoading: isLoading, showNewPaneDialog: showNewPaneDialog, agentStatuses: agentStatuses, kebabMenuPaneIndex: kebabMenuPaneIndex ?? undefined }),
1260
- isLoading && (React.createElement(LoadingIndicator, null)),
1261
- showNewPaneDialog && !showAgentChoiceDialog && (React.createElement(NewPaneDialog, { value: newPanePrompt, onChange: setNewPanePrompt, onSubmit: (value) => {
1262
- const promptValue = value;
1263
- const agents = availableAgents;
1264
- if (agents.length === 0) {
1265
- setShowNewPaneDialog(false);
1266
- setNewPanePrompt('');
1267
- createNewPaneHook(promptValue);
1268
- }
1269
- else if (agents.length === 1) {
1270
- setShowNewPaneDialog(false);
1271
- setNewPanePrompt('');
1272
- createNewPaneHook(promptValue, agents[0]);
1273
- }
1274
- else {
1275
- setPendingPrompt(promptValue);
1276
- setShowNewPaneDialog(false);
1277
- setNewPanePrompt('');
1278
- setShowAgentChoiceDialog(true);
1279
- setAgentChoice(agentChoice || 'claude');
1280
- }
1281
- } })),
1282
- showAgentChoiceDialog && (React.createElement(AgentChoiceDialog, { agentChoice: agentChoice })),
1283
- isCreatingPane && (React.createElement(CreatingIndicator, { message: statusMessage })),
1284
- showCommandPrompt && (React.createElement(CommandPromptDialog, { type: showCommandPrompt, value: commandInput, onChange: setCommandInput })),
1285
- showFileCopyPrompt && (React.createElement(FileCopyPrompt, null)),
1286
- showKebabMenu && kebabMenuPaneIndex !== null && panes[kebabMenuPaneIndex] && (React.createElement(KebabMenu, { selectedOption: kebabMenuOption, actions: kebabMenuActions, paneName: panes[kebabMenuPaneIndex].slug })),
1287
- 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 })),
1288
- showHooksDialog && (React.createElement(HooksDialog, { hooks: hooksData, selectedIndex: hooksSelectedIndex })),
1289
- 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 })),
1290
- 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 })),
1291
- 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) => {
1292
- actionSystem.setActionState(prev => ({ ...prev, inputValue: value }));
1293
- } })),
1294
- actionSystem.actionState.showProgressDialog && (React.createElement(ActionProgressDialog, { key: "progress-dialog", message: actionSystem.actionState.progressMessage, percent: actionSystem.actionState.progressPercent })),
1295
- runningCommand && (React.createElement(RunningIndicator, null)),
1296
- isUpdating && (React.createElement(UpdatingIndicator, null)),
1297
- isCreatingTunnel && (React.createElement(Box, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", padding: 1, marginTop: 1 },
1298
- React.createElement(Text, { bold: true, color: "cyan" }, "Creating tunnel..."),
1299
- React.createElement(Box, { marginTop: 1 },
1300
- React.createElement(Text, { dimColor: true }, "This may take a few moments...")))),
1301
- statusMessage && (React.createElement(Box, { marginTop: 1 },
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 },
1935
+ showRepaintSpinner && (React.createElement(Box, { marginTop: -10, marginLeft: -100 },
1936
+ React.createElement(Text, null, "\u27F3"))),
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,
1302
1945
  React.createElement(Text, { color: "green" }, statusMessage))),
1303
- actionSystem.actionState.statusMessage && (React.createElement(Box, { marginTop: 1 },
1304
- React.createElement(Text, { color: actionSystem.actionState.statusType === 'error' ? 'red' :
1305
- actionSystem.actionState.statusType === 'success' ? 'green' :
1306
- 'cyan' }, actionSystem.actionState.statusMessage))),
1307
- 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: (() => {
1308
1967
  if (!process.env.DEBUG_DMUX)
1309
1968
  return undefined;
1310
1969
  const cols = Math.max(1, Math.floor(terminalWidth / 37));
@@ -1312,16 +1971,14 @@ const DmuxApp = ({ panesFile, projectName, sessionName, settingsFile, projectRoo
1312
1971
  const pos = getCardGridPosition(selectedIndex);
1313
1972
  return `Grid: ${cols} cols × ${rows} rows | Selected: row ${pos.row}, col ${pos.col} | Terminal: ${terminalWidth}w`;
1314
1973
  })() }),
1315
- React.createElement(Text, { dimColor: true },
1316
- updateAvailable && updateInfo && (React.createElement(Text, { color: "red", bold: true }, "Update available: npm i -g dmux@latest ")),
1317
- "v",
1318
- packageJson.version,
1319
- serverPort && serverPort > 0 && (React.createElement(Text, { dimColor: true },
1320
- " \u2022 ",
1321
- React.createElement(Text, { color: "cyan" },
1322
- "http://127.0.0.1:",
1323
- serverPort))),
1324
- 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 },
1325
1982
  " \u2022 ",
1326
1983
  debugMessage)))));
1327
1984
  };