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