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.
- package/dist/CleanTextInput.d.ts +9 -0
- package/dist/CleanTextInput.d.ts.map +1 -1
- package/dist/CleanTextInput.js +127 -47
- package/dist/CleanTextInput.js.map +1 -1
- package/dist/DmuxApp.d.ts +2 -2
- package/dist/DmuxApp.d.ts.map +1 -1
- package/dist/DmuxApp.js +1408 -751
- package/dist/DmuxApp.js.map +1 -1
- package/dist/actions/paneActions.d.ts.map +1 -1
- package/dist/actions/paneActions.js +68 -11
- package/dist/actions/paneActions.js.map +1 -1
- package/dist/actions/types.d.ts +1 -0
- package/dist/actions/types.d.ts.map +1 -1
- package/dist/actions/types.js +1 -0
- package/dist/actions/types.js.map +1 -1
- package/dist/components/ActionInputDialog.d.ts.map +1 -1
- package/dist/components/ActionInputDialog.js +3 -2
- package/dist/components/ActionInputDialog.js.map +1 -1
- package/dist/components/DialogBox.d.ts +16 -0
- package/dist/components/DialogBox.d.ts.map +1 -0
- package/dist/components/DialogBox.js +12 -0
- package/dist/components/DialogBox.js.map +1 -0
- package/dist/components/FooterHelp.d.ts +9 -0
- package/dist/components/FooterHelp.d.ts.map +1 -1
- package/dist/components/FooterHelp.js +41 -5
- package/dist/components/FooterHelp.js.map +1 -1
- package/dist/components/PaneCard.d.ts +3 -1
- package/dist/components/PaneCard.d.ts.map +1 -1
- package/dist/components/PaneCard.js +50 -33
- package/dist/components/PaneCard.js.map +1 -1
- package/dist/components/PanesGrid.d.ts +3 -5
- package/dist/components/PanesGrid.d.ts.map +1 -1
- package/dist/components/PanesGrid.js +40 -10
- package/dist/components/PanesGrid.js.map +1 -1
- package/dist/dashboard.js +1 -1
- package/dist/decorative-pane.d.ts +3 -0
- package/dist/decorative-pane.d.ts.map +1 -0
- package/dist/decorative-pane.js +136 -0
- package/dist/decorative-pane.js.map +1 -0
- package/dist/hooks/useActionSystem.d.ts +14 -1
- package/dist/hooks/useActionSystem.d.ts.map +1 -1
- package/dist/hooks/useActionSystem.js +93 -4
- package/dist/hooks/useActionSystem.js.map +1 -1
- package/dist/hooks/useNavigation.js +1 -1
- package/dist/hooks/useNavigation.js.map +1 -1
- package/dist/hooks/usePaneCreation.d.ts +1 -2
- package/dist/hooks/usePaneCreation.d.ts.map +1 -1
- package/dist/hooks/usePaneCreation.js +30 -29
- package/dist/hooks/usePaneCreation.js.map +1 -1
- package/dist/hooks/usePaneRunner.d.ts.map +1 -1
- package/dist/hooks/usePaneRunner.js +8 -3
- package/dist/hooks/usePaneRunner.js.map +1 -1
- package/dist/hooks/usePanes.d.ts.map +1 -1
- package/dist/hooks/usePanes.js +210 -37
- package/dist/hooks/usePanes.js.map +1 -1
- package/dist/hooks/useWorktreeActions.d.ts.map +1 -1
- package/dist/hooks/useWorktreeActions.js +7 -13
- package/dist/hooks/useWorktreeActions.js.map +1 -1
- package/dist/index.js +217 -29
- package/dist/index.js.map +1 -1
- package/dist/popups/agentChoicePopup.d.ts +7 -0
- package/dist/popups/agentChoicePopup.d.ts.map +1 -0
- package/dist/popups/agentChoicePopup.js +74 -0
- package/dist/popups/agentChoicePopup.js.map +1 -0
- package/dist/popups/choicePopup.d.ts +7 -0
- package/dist/popups/choicePopup.d.ts.map +1 -0
- package/dist/popups/choicePopup.js +64 -0
- package/dist/popups/choicePopup.js.map +1 -0
- package/dist/popups/components/FileList.d.ts +13 -0
- package/dist/popups/components/FileList.d.ts.map +1 -0
- package/dist/popups/components/FileList.js +61 -0
- package/dist/popups/components/FileList.js.map +1 -0
- package/dist/popups/components/PopupContainer.d.ts +14 -0
- package/dist/popups/components/PopupContainer.d.ts.map +1 -0
- package/dist/popups/components/PopupContainer.js +15 -0
- package/dist/popups/components/PopupContainer.js.map +1 -0
- package/dist/popups/components/PopupInputBox.d.ts +11 -0
- package/dist/popups/components/PopupInputBox.d.ts.map +1 -0
- package/dist/popups/components/PopupInputBox.js +10 -0
- package/dist/popups/components/PopupInputBox.js.map +1 -0
- package/dist/popups/components/PopupWrapper.d.ts +37 -0
- package/dist/popups/components/PopupWrapper.d.ts.map +1 -0
- package/dist/popups/components/PopupWrapper.js +88 -0
- package/dist/popups/components/PopupWrapper.js.map +1 -0
- package/dist/popups/components/index.d.ts +8 -0
- package/dist/popups/components/index.d.ts.map +1 -0
- package/dist/popups/components/index.js +8 -0
- package/dist/popups/components/index.js.map +1 -0
- package/dist/popups/config.d.ts +40 -0
- package/dist/popups/config.d.ts.map +1 -0
- package/dist/popups/config.js +40 -0
- package/dist/popups/config.js.map +1 -0
- package/dist/popups/confirmPopup.d.ts +7 -0
- package/dist/popups/confirmPopup.d.ts.map +1 -0
- package/dist/popups/confirmPopup.js +72 -0
- package/dist/popups/confirmPopup.js.map +1 -0
- package/dist/popups/hooksPopup.d.ts +7 -0
- package/dist/popups/hooksPopup.d.ts.map +1 -0
- package/dist/popups/hooksPopup.js +71 -0
- package/dist/popups/hooksPopup.js.map +1 -0
- package/dist/popups/inputPopup.d.ts +7 -0
- package/dist/popups/inputPopup.d.ts.map +1 -0
- package/dist/popups/inputPopup.js +48 -0
- package/dist/popups/inputPopup.js.map +1 -0
- package/dist/popups/kebabMenuPopup.d.ts +7 -0
- package/dist/popups/kebabMenuPopup.d.ts.map +1 -0
- package/dist/popups/kebabMenuPopup.js +52 -0
- package/dist/popups/kebabMenuPopup.js.map +1 -0
- package/dist/popups/logsPopup.d.ts +12 -0
- package/dist/popups/logsPopup.d.ts.map +1 -0
- package/dist/popups/logsPopup.js +364 -0
- package/dist/popups/logsPopup.js.map +1 -0
- package/dist/popups/mergePopup.d.ts +7 -0
- package/dist/popups/mergePopup.d.ts.map +1 -0
- package/dist/popups/mergePopup.js +310 -0
- package/dist/popups/mergePopup.js.map +1 -0
- package/dist/popups/newPanePopup.d.ts +7 -0
- package/dist/popups/newPanePopup.d.ts.map +1 -0
- package/dist/popups/newPanePopup.js +234 -0
- package/dist/popups/newPanePopup.js.map +1 -0
- package/dist/popups/progressPopup.d.ts +8 -0
- package/dist/popups/progressPopup.d.ts.map +1 -0
- package/dist/popups/progressPopup.js +54 -0
- package/dist/popups/progressPopup.js.map +1 -0
- package/dist/popups/remotePopup.d.ts +6 -0
- package/dist/popups/remotePopup.d.ts.map +1 -0
- package/dist/popups/remotePopup.js +166 -0
- package/dist/popups/remotePopup.js.map +1 -0
- package/dist/popups/settingsPopup.d.ts +7 -0
- package/dist/popups/settingsPopup.d.ts.map +1 -0
- package/dist/popups/settingsPopup.js +212 -0
- package/dist/popups/settingsPopup.js.map +1 -0
- package/dist/popups/shortcutsPopup.d.ts +6 -0
- package/dist/popups/shortcutsPopup.d.ts.map +1 -0
- package/dist/popups/shortcutsPopup.js +74 -0
- package/dist/popups/shortcutsPopup.js.map +1 -0
- package/dist/popups/templates/SimpleInputPopup.d.ts +15 -0
- package/dist/popups/templates/SimpleInputPopup.d.ts.map +1 -0
- package/dist/popups/templates/SimpleInputPopup.js +28 -0
- package/dist/popups/templates/SimpleInputPopup.js.map +1 -0
- package/dist/server/embedded-assets.d.ts.map +1 -1
- package/dist/server/embedded-assets.js +2077 -968
- package/dist/server/embedded-assets.js.map +1 -1
- package/dist/server/index.js +1 -1
- package/dist/server/index.js.map +1 -1
- package/dist/server/routes.d.ts +1 -1
- package/dist/server/routes.d.ts.map +1 -1
- package/dist/server/routes.js +73 -6
- package/dist/server/routes.js.map +1 -1
- package/dist/services/ConfigWatcher.d.ts.map +1 -1
- package/dist/services/ConfigWatcher.js +10 -3
- package/dist/services/ConfigWatcher.js.map +1 -1
- package/dist/services/LogService.d.ts +112 -0
- package/dist/services/LogService.d.ts.map +1 -0
- package/dist/services/LogService.js +252 -0
- package/dist/services/LogService.js.map +1 -0
- package/dist/services/PaneWorkerManager.d.ts.map +1 -1
- package/dist/services/PaneWorkerManager.js +35 -9
- package/dist/services/PaneWorkerManager.js.map +1 -1
- package/dist/services/TunnelService.d.ts +1 -0
- package/dist/services/TunnelService.d.ts.map +1 -1
- package/dist/services/TunnelService.js +56 -15
- package/dist/services/TunnelService.js.map +1 -1
- package/dist/shared/StateManager.d.ts +49 -1
- package/dist/shared/StateManager.d.ts.map +1 -1
- package/dist/shared/StateManager.js +97 -2
- package/dist/shared/StateManager.js.map +1 -1
- package/dist/spacer-pane.d.ts +8 -0
- package/dist/spacer-pane.d.ts.map +1 -0
- package/dist/spacer-pane.js +40 -0
- package/dist/spacer-pane.js.map +1 -0
- package/dist/theme/colors.d.ts +25 -0
- package/dist/theme/colors.d.ts.map +1 -0
- package/dist/theme/colors.js +33 -0
- package/dist/theme/colors.js.map +1 -0
- package/dist/types.d.ts +14 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/asciiArt.d.ts +26 -0
- package/dist/utils/asciiArt.d.ts.map +1 -0
- package/dist/utils/asciiArt.js +88 -0
- package/dist/utils/asciiArt.js.map +1 -0
- package/dist/utils/conflictResolutionPane.d.ts.map +1 -1
- package/dist/utils/conflictResolutionPane.js +8 -4
- package/dist/utils/conflictResolutionPane.js.map +1 -1
- package/dist/utils/fileScanner.d.ts +23 -0
- package/dist/utils/fileScanner.d.ts.map +1 -0
- package/dist/utils/fileScanner.js +123 -0
- package/dist/utils/fileScanner.js.map +1 -0
- package/dist/utils/generated-agents-doc.d.ts +1 -1
- package/dist/utils/generated-agents-doc.js +1 -1
- package/dist/utils/hooks.d.ts +3 -3
- package/dist/utils/hooks.d.ts.map +1 -1
- package/dist/utils/hooks.js +70 -41
- package/dist/utils/hooks.js.map +1 -1
- package/dist/utils/hooksDocs.d.ts +1 -1
- package/dist/utils/layoutManager.d.ts +45 -0
- package/dist/utils/layoutManager.d.ts.map +1 -0
- package/dist/utils/layoutManager.js +500 -0
- package/dist/utils/layoutManager.js.map +1 -0
- package/dist/utils/paneCreation.d.ts.map +1 -1
- package/dist/utils/paneCreation.js +125 -11
- package/dist/utils/paneCreation.js.map +1 -1
- package/dist/utils/popup.d.ts +97 -0
- package/dist/utils/popup.d.ts.map +1 -0
- package/dist/utils/popup.js +509 -0
- package/dist/utils/popup.js.map +1 -0
- package/dist/utils/postPaneCleanup.d.ts +12 -0
- package/dist/utils/postPaneCleanup.d.ts.map +1 -0
- package/dist/utils/postPaneCleanup.js +53 -0
- package/dist/utils/postPaneCleanup.js.map +1 -0
- package/dist/utils/shellPaneDetection.d.ts +44 -0
- package/dist/utils/shellPaneDetection.d.ts.map +1 -0
- package/dist/utils/shellPaneDetection.js +175 -0
- package/dist/utils/shellPaneDetection.js.map +1 -0
- package/dist/utils/tmux.d.ts +53 -1
- package/dist/utils/tmux.d.ts.map +1 -1
- package/dist/utils/tmux.js +352 -84
- package/dist/utils/tmux.js.map +1 -1
- package/dist/utils/welcomePane.d.ts +22 -0
- package/dist/utils/welcomePane.d.ts.map +1 -0
- package/dist/utils/welcomePane.js +119 -0
- package/dist/utils/welcomePane.js.map +1 -0
- package/dist/utils/welcomePaneManager.d.ts +36 -0
- package/dist/utils/welcomePaneManager.d.ts.map +1 -0
- package/dist/utils/welcomePaneManager.js +160 -0
- package/dist/utils/welcomePaneManager.js.map +1 -0
- package/dist/workers/PaneWorker.js +2 -0
- package/dist/workers/PaneWorker.js.map +1 -1
- package/package.json +5 -2
- package/dist/components/NewPaneDialog.d.ts +0 -9
- package/dist/components/NewPaneDialog.d.ts.map +0 -1
- package/dist/components/NewPaneDialog.js +0 -11
- package/dist/components/NewPaneDialog.js.map +0 -1
package/dist/DmuxApp.js
CHANGED
|
@@ -1,142 +1,184 @@
|
|
|
1
|
-
import React, { useState, useEffect } from
|
|
2
|
-
import { Box, Text, useInput, useApp } from
|
|
3
|
-
import { execSync } from
|
|
4
|
-
import
|
|
5
|
-
import
|
|
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
|
|
8
|
-
import useProjectSettings from
|
|
9
|
-
import useTerminalWidth from
|
|
10
|
-
import useNavigation from
|
|
11
|
-
import useAutoUpdater from
|
|
12
|
-
import useAgentDetection from
|
|
13
|
-
import useAgentStatus from
|
|
14
|
-
import usePaneRunner from
|
|
15
|
-
import usePaneCreation from
|
|
16
|
-
import useActionSystem from
|
|
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 {
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
22
|
-
import {
|
|
23
|
-
import {
|
|
24
|
-
import {
|
|
25
|
-
import {
|
|
26
|
-
import {
|
|
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(
|
|
29
|
-
import PanesGrid from
|
|
30
|
-
import
|
|
31
|
-
import
|
|
32
|
-
import
|
|
33
|
-
import
|
|
34
|
-
import
|
|
35
|
-
import
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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 [
|
|
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
|
-
|
|
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 = () =>
|
|
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(
|
|
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 !==
|
|
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 !==
|
|
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 ===
|
|
234
|
+
if (event.status === "working") {
|
|
194
235
|
updated.analyzerError = undefined;
|
|
195
236
|
}
|
|
196
|
-
else if (event.status ===
|
|
197
|
-
if (event.analyzerError === undefined &&
|
|
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(
|
|
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(
|
|
254
|
+
statusDetector.on("status-updated", handleStatusUpdate);
|
|
213
255
|
return () => {
|
|
214
|
-
statusDetector.off(
|
|
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(
|
|
240
|
-
//
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
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(
|
|
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] ||
|
|
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:
|
|
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(
|
|
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(
|
|
311
|
-
encoding:
|
|
312
|
-
stdio:
|
|
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,
|
|
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}"', {
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
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(
|
|
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:
|
|
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}'`, {
|
|
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}"`, {
|
|
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
|
-
//
|
|
362
|
-
|
|
363
|
-
|
|
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`, {
|
|
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:
|
|
376
|
-
await new Promise(resolve => setTimeout(resolve, 500));
|
|
377
|
-
setStatusMessage(agent
|
|
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:
|
|
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 ===
|
|
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}'`, {
|
|
405
|
-
|
|
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 ===
|
|
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}'`, {
|
|
412
|
-
|
|
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,
|
|
417
|
-
execSync(`tmux set-buffer -b '${bufName}' -- '${promptEsc}'`, {
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
execSync(`tmux
|
|
421
|
-
|
|
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 ===
|
|
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) {
|
|
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(
|
|
481
|
-
paneContent.includes(
|
|
482
|
-
paneContent.includes(
|
|
483
|
-
paneContent.includes(
|
|
484
|
-
(paneContent.includes(
|
|
485
|
-
if ((hasTrustPrompt || hasClaudePermissionPrompt) &&
|
|
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`, {
|
|
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'`, {
|
|
499
|
-
|
|
500
|
-
|
|
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`, {
|
|
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(
|
|
516
|
-
updatedContent.includes(
|
|
517
|
-
updatedContent.includes(
|
|
518
|
-
(prompt &&
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
execSync(`tmux send-keys -t '${paneInfo}'
|
|
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 &&
|
|
530
|
-
|
|
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:
|
|
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 ||
|
|
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:
|
|
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
|
|
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(
|
|
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:
|
|
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:
|
|
584
|
-
|
|
585
|
-
|
|
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(
|
|
589
|
-
setTimeout(() => setStatusMessage(
|
|
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(
|
|
595
|
-
setTimeout(() => setStatusMessage(
|
|
1538
|
+
setStatusMessage("No worktree path for this pane");
|
|
1539
|
+
setTimeout(() => setStatusMessage(""), 2000);
|
|
596
1540
|
return;
|
|
597
1541
|
}
|
|
598
|
-
const command = type ===
|
|
599
|
-
const isFirstRun = type ===
|
|
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 ===
|
|
1564
|
+
const existingWindowId = type === "test" ? pane.testWindowId : pane.devWindowId;
|
|
619
1565
|
if (existingWindowId) {
|
|
620
1566
|
try {
|
|
621
|
-
execSync(`tmux kill-window -t '${existingWindowId}'`, {
|
|
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:
|
|
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 ===
|
|
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:
|
|
636
|
-
execSync(`tmux send-keys -t '${windowId}' Enter`, { stdio:
|
|
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 ===
|
|
641
|
-
[type ===
|
|
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 ===
|
|
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 ===
|
|
656
|
-
setTimeout(() => setStatusMessage(
|
|
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(
|
|
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(
|
|
1620
|
+
process.stdout.write("\x1b[2J\x1b[H");
|
|
673
1621
|
// 2. Clear tmux history
|
|
674
1622
|
try {
|
|
675
|
-
execSync(
|
|
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(
|
|
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(
|
|
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(
|
|
694
|
-
process.stdout.write(
|
|
695
|
-
process.stdout.write(
|
|
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(
|
|
699
|
-
execSync(
|
|
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(
|
|
1651
|
+
process.stdout.write("\x1b[2J\x1b[H");
|
|
704
1652
|
// Show clean goodbye message
|
|
705
|
-
process.stdout.write(
|
|
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 ===
|
|
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
|
|
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 ===
|
|
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 ===
|
|
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 ===
|
|
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 ===
|
|
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 ===
|
|
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 ===
|
|
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,
|
|
1778
|
+
targetIndex = findCardInDirection(selectedIndex, "up");
|
|
1167
1779
|
}
|
|
1168
1780
|
else if (key.downArrow) {
|
|
1169
|
-
targetIndex = findCardInDirection(selectedIndex,
|
|
1781
|
+
targetIndex = findCardInDirection(selectedIndex, "down");
|
|
1170
1782
|
}
|
|
1171
1783
|
else if (key.leftArrow) {
|
|
1172
|
-
targetIndex = findCardInDirection(selectedIndex,
|
|
1784
|
+
targetIndex = findCardInDirection(selectedIndex, "left");
|
|
1173
1785
|
}
|
|
1174
1786
|
else if (key.rightArrow) {
|
|
1175
|
-
targetIndex = findCardInDirection(selectedIndex,
|
|
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 (
|
|
1183
|
-
// Open kebab menu for selected pane
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
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 ===
|
|
1201
|
-
//
|
|
1202
|
-
if (
|
|
1203
|
-
|
|
1204
|
-
|
|
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
|
-
//
|
|
1221
|
-
|
|
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 &&
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
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 ===
|
|
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(
|
|
1881
|
+
setTimeout(() => StateManager.getInstance().setDebugMessage(""), 2000);
|
|
1234
1882
|
actionSystem.executeAction(PaneAction.VIEW, panes[selectedIndex]);
|
|
1235
1883
|
}
|
|
1236
|
-
else if (input ===
|
|
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(
|
|
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(
|
|
1893
|
+
setTimeout(() => StateManager.getInstance().setDebugMessage(""), 2000);
|
|
1246
1894
|
actionSystem.executeAction(PaneAction.VIEW, panes[selectedIndex]);
|
|
1247
1895
|
}
|
|
1248
1896
|
});
|
|
1249
|
-
//
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
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
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
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,
|
|
1304
|
-
React.createElement(Text, { color: actionSystem.actionState.statusType ===
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
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 },
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
"
|
|
1321
|
-
|
|
1322
|
-
|
|
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
|
};
|