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
|
@@ -427,13 +427,29 @@ export default BetterTextInput;
|
|
|
427
427
|
'CleanTextInput.js': {
|
|
428
428
|
content: `import React, { useState, useEffect, useMemo } from 'react';
|
|
429
429
|
import { Box, Text, useInput, useFocus, useStdout } from 'ink';
|
|
430
|
-
|
|
431
|
-
|
|
430
|
+
import fs from 'fs';
|
|
431
|
+
import path from 'path';
|
|
432
|
+
// Debug logging to file
|
|
433
|
+
const DEBUG_LOG = path.join(process.cwd(), '.dmux', 'file-picker-debug.log');
|
|
434
|
+
function debugLog(message, data) {
|
|
435
|
+
const timestamp = new Date().toISOString();
|
|
436
|
+
const logLine = \`[\${timestamp}] \${message} \${data !== undefined ? JSON.stringify(data, null, 2) : ''}\\n\`;
|
|
437
|
+
try {
|
|
438
|
+
fs.appendFileSync(DEBUG_LOG, logLine);
|
|
439
|
+
}
|
|
440
|
+
catch (e) {
|
|
441
|
+
// Ignore write errors
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
const CleanTextInput = ({ value, onChange, onSubmit, placeholder = '', maxWidth: propMaxWidth, maxVisibleLines: propMaxVisibleLines, cursorPosition, disableArrowKeys = false, disableUpDownArrows = false, onCursorChange, disableEscape = false, ignoreFocus = false, onCancel }) => {
|
|
445
|
+
// Use id to maintain focus even when other components mount/unmount
|
|
446
|
+
const { isFocused } = useFocus({ autoFocus: true, id: 'clean-text-input' });
|
|
432
447
|
const [cursor, setCursor] = useState(value.length);
|
|
433
448
|
const { stdout } = useStdout();
|
|
434
449
|
const [pastedItems, setPastedItems] = useState(new Map());
|
|
435
450
|
const [nextPasteId, setNextPasteId] = useState(1);
|
|
436
451
|
const [isProcessingPaste, setIsProcessingPaste] = useState(false);
|
|
452
|
+
const [topVisibleLine, setTopVisibleLine] = useState(0);
|
|
437
453
|
// Only ignore first input in production (not in tests)
|
|
438
454
|
// Check for common test environment indicators
|
|
439
455
|
const isTestEnvironment = process.env.NODE_ENV === 'test' ||
|
|
@@ -449,9 +465,10 @@ const CleanTextInput = ({ value, onChange, onSubmit, placeholder = '' }) => {
|
|
|
449
465
|
// Subtract 2 for borders, 2 for padding, 2 for "> " prompt = 6 total
|
|
450
466
|
// The prompt is always rendered separately, so we need to account for it
|
|
451
467
|
// Use process.stdout.columns as fallback since useStdout might not update
|
|
468
|
+
// Allow override via prop for popup usage
|
|
452
469
|
const terminalWidth = process.stdout.columns || (stdout ? stdout.columns : 80);
|
|
453
470
|
// Reduce by 1 more to prevent edge case where text exactly fills width
|
|
454
|
-
const maxWidth = Math.max(20, terminalWidth - 7);
|
|
471
|
+
const maxWidth = propMaxWidth || Math.max(20, terminalWidth - 7);
|
|
455
472
|
// Keep cursor in bounds
|
|
456
473
|
useEffect(() => {
|
|
457
474
|
if (cursor > value.length) {
|
|
@@ -461,6 +478,17 @@ const CleanTextInput = ({ value, onChange, onSubmit, placeholder = '' }) => {
|
|
|
461
478
|
setCursor(0);
|
|
462
479
|
}
|
|
463
480
|
}, [value.length, cursor]);
|
|
481
|
+
// Update cursor when cursorPosition prop changes
|
|
482
|
+
useEffect(() => {
|
|
483
|
+
if (cursorPosition !== undefined) {
|
|
484
|
+
const boundedPosition = Math.max(0, Math.min(cursorPosition, value.length));
|
|
485
|
+
setCursor(boundedPosition);
|
|
486
|
+
}
|
|
487
|
+
}, [cursorPosition, value.length]);
|
|
488
|
+
// Notify parent when cursor changes
|
|
489
|
+
useEffect(() => {
|
|
490
|
+
onCursorChange?.(cursor);
|
|
491
|
+
}, [cursor, onCursorChange]);
|
|
464
492
|
// Enable bracketed paste mode with small delay to avoid blocking UI
|
|
465
493
|
useEffect(() => {
|
|
466
494
|
let bracketedPasteTimer = null;
|
|
@@ -596,13 +624,35 @@ const CleanTextInput = ({ value, onChange, onSubmit, placeholder = '' }) => {
|
|
|
596
624
|
setInBracketedPaste(false);
|
|
597
625
|
};
|
|
598
626
|
useInput((input, key) => {
|
|
599
|
-
|
|
627
|
+
debugLog('[CleanTextInput] useInput called', {
|
|
628
|
+
isFocused,
|
|
629
|
+
ignoreFocus,
|
|
630
|
+
input: input?.substring(0, 10),
|
|
631
|
+
keyPressed: Object.keys(key).filter(k => key[k]).join(','),
|
|
632
|
+
disableEscape,
|
|
633
|
+
disableArrowKeys
|
|
634
|
+
});
|
|
635
|
+
if (!isFocused && !ignoreFocus) {
|
|
636
|
+
debugLog('[CleanTextInput] Not focused and not ignoring focus, ignoring input');
|
|
600
637
|
return;
|
|
601
|
-
|
|
638
|
+
}
|
|
639
|
+
if (!isFocused && ignoreFocus) {
|
|
640
|
+
debugLog('[CleanTextInput] Not focused but ignoring focus check, processing input');
|
|
641
|
+
}
|
|
642
|
+
// Note: Ctrl+C (SIGINT) is handled at the process level in newPanePopup, not here
|
|
643
|
+
// This is because SIGINT bypasses Ink's input system
|
|
644
|
+
// Escape clears (unless disabled by external control)
|
|
602
645
|
if (key.escape) {
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
646
|
+
debugLog('[CleanTextInput] ESC pressed', { disableEscape, value: value.substring(0, 50) });
|
|
647
|
+
if (!disableEscape) {
|
|
648
|
+
debugLog('[CleanTextInput] Clearing input');
|
|
649
|
+
onChange('');
|
|
650
|
+
setCursor(0);
|
|
651
|
+
return;
|
|
652
|
+
}
|
|
653
|
+
else {
|
|
654
|
+
debugLog('[CleanTextInput] ESC disabled, ignoring');
|
|
655
|
+
}
|
|
606
656
|
}
|
|
607
657
|
// Shift+Enter adds newline
|
|
608
658
|
if (key.return && key.shift) {
|
|
@@ -685,17 +735,17 @@ const CleanTextInput = ({ value, onChange, onSubmit, placeholder = '' }) => {
|
|
|
685
735
|
return;
|
|
686
736
|
}
|
|
687
737
|
// Left arrow
|
|
688
|
-
if (key.leftArrow) {
|
|
738
|
+
if (key.leftArrow && !disableArrowKeys) {
|
|
689
739
|
setCursor(Math.max(0, cursor - 1));
|
|
690
740
|
return;
|
|
691
741
|
}
|
|
692
742
|
// Right arrow
|
|
693
|
-
if (key.rightArrow) {
|
|
743
|
+
if (key.rightArrow && !disableArrowKeys) {
|
|
694
744
|
setCursor(Math.min(value.length, cursor + 1));
|
|
695
745
|
return;
|
|
696
746
|
}
|
|
697
747
|
// Up/Down arrows for navigation (works with both hard and soft wrapped lines)
|
|
698
|
-
if (key.upArrow || key.downArrow) {
|
|
748
|
+
if ((key.upArrow || key.downArrow) && !disableArrowKeys && !disableUpDownArrows) {
|
|
699
749
|
// Get wrapped lines to understand visual layout
|
|
700
750
|
const wrapped = wrapText(value, maxWidth);
|
|
701
751
|
const currentPos = findCursorInWrappedLines(wrapped, cursor);
|
|
@@ -1023,6 +1073,28 @@ const CleanTextInput = ({ value, onChange, onSubmit, placeholder = '' }) => {
|
|
|
1023
1073
|
// Memoize wrapped text to avoid recalculating on every render
|
|
1024
1074
|
const wrappedLines = useMemo(() => wrapText(value, maxWidth), [value, maxWidth]);
|
|
1025
1075
|
const hasMultipleLines = wrappedLines.length > 1;
|
|
1076
|
+
// Default to showing all lines if maxVisibleLines not specified
|
|
1077
|
+
const maxVisibleLines = propMaxVisibleLines || wrappedLines.length;
|
|
1078
|
+
// Find cursor position in wrapped lines
|
|
1079
|
+
const cursorPos = findCursorInWrappedLines(wrappedLines, cursor);
|
|
1080
|
+
// Update visible window when cursor moves outside it
|
|
1081
|
+
useEffect(() => {
|
|
1082
|
+
if (!propMaxVisibleLines)
|
|
1083
|
+
return; // No scrolling if maxVisibleLines not set
|
|
1084
|
+
const cursorLine = cursorPos.line;
|
|
1085
|
+
// If cursor is above the visible window, scroll up
|
|
1086
|
+
if (cursorLine < topVisibleLine) {
|
|
1087
|
+
setTopVisibleLine(cursorLine);
|
|
1088
|
+
}
|
|
1089
|
+
// If cursor is below the visible window, scroll down
|
|
1090
|
+
else if (cursorLine >= topVisibleLine + maxVisibleLines) {
|
|
1091
|
+
setTopVisibleLine(cursorLine - maxVisibleLines + 1);
|
|
1092
|
+
}
|
|
1093
|
+
}, [cursorPos.line, topVisibleLine, maxVisibleLines, propMaxVisibleLines]);
|
|
1094
|
+
// Calculate which lines to render (visible window)
|
|
1095
|
+
const visibleLines = wrappedLines.slice(topVisibleLine, topVisibleLine + maxVisibleLines);
|
|
1096
|
+
const hasMoreAbove = topVisibleLine > 0;
|
|
1097
|
+
const hasMoreBelow = topVisibleLine + maxVisibleLines < wrappedLines.length;
|
|
1026
1098
|
if (value === '') {
|
|
1027
1099
|
// Show cursor for empty input (no placeholder)
|
|
1028
1100
|
return (React.createElement(Box, null,
|
|
@@ -1031,176 +1103,175 @@ const CleanTextInput = ({ value, onChange, onSubmit, placeholder = '' }) => {
|
|
|
1031
1103
|
React.createElement(Box, null,
|
|
1032
1104
|
React.createElement(Text, { inverse: true }, ' '))));
|
|
1033
1105
|
}
|
|
1034
|
-
// Find cursor position in wrapped lines
|
|
1035
|
-
const cursorPos = findCursorInWrappedLines(wrappedLines, cursor);
|
|
1036
1106
|
// Render wrapped lines
|
|
1037
|
-
return (React.createElement(Box, { flexDirection: "column" },
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
const
|
|
1044
|
-
const
|
|
1045
|
-
const
|
|
1046
|
-
const
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1107
|
+
return (React.createElement(Box, { flexDirection: "column" },
|
|
1108
|
+
hasMoreAbove && (React.createElement(Box, null,
|
|
1109
|
+
React.createElement(Box, { width: 2 },
|
|
1110
|
+
React.createElement(Text, { dimColor: true }, "\\u22EE ")),
|
|
1111
|
+
React.createElement(Text, { dimColor: true, italic: true }, "(scroll up for more)"))),
|
|
1112
|
+
visibleLines.map((wrappedLine, visibleIdx) => {
|
|
1113
|
+
const idx = visibleIdx + topVisibleLine; // Actual index in wrappedLines
|
|
1114
|
+
const isFirst = idx === 0;
|
|
1115
|
+
const hasCursor = idx === cursorPos.line;
|
|
1116
|
+
const line = wrappedLine.line;
|
|
1117
|
+
if (hasCursor) {
|
|
1118
|
+
// Ensure cursor position is valid
|
|
1119
|
+
const actualCol = Math.min(cursorPos.col, line.length);
|
|
1120
|
+
const before = line.slice(0, actualCol);
|
|
1121
|
+
const at = line[actualCol] || ' ';
|
|
1122
|
+
const after = line.slice(actualCol + 1);
|
|
1123
|
+
// Check if cursor is within a paste tag
|
|
1124
|
+
const tagPattern = /\\[#\\d+ Pasted, \\d+ lines?\\]/g;
|
|
1125
|
+
let match;
|
|
1126
|
+
let cursorInTag = false;
|
|
1127
|
+
while ((match = tagPattern.exec(line)) !== null) {
|
|
1128
|
+
if (actualCol >= match.index && actualCol < match.index + match[0].length) {
|
|
1129
|
+
cursorInTag = true;
|
|
1130
|
+
break;
|
|
1131
|
+
}
|
|
1055
1132
|
}
|
|
1133
|
+
return (React.createElement(Box, { key: idx },
|
|
1134
|
+
React.createElement(Box, { width: 2 },
|
|
1135
|
+
React.createElement(Text, null, isFirst ? '> ' : ' ')),
|
|
1136
|
+
React.createElement(Box, null, cursorInTag ? (
|
|
1137
|
+
// Cursor is within a paste tag - render specially
|
|
1138
|
+
React.createElement(React.Fragment, null,
|
|
1139
|
+
renderTextWithTags(before),
|
|
1140
|
+
React.createElement(Text, { inverse: true, color: "cyan" }, at),
|
|
1141
|
+
renderTextWithTags(after))) : (
|
|
1142
|
+
// Normal rendering with tag highlighting
|
|
1143
|
+
React.createElement(React.Fragment, null,
|
|
1144
|
+
renderTextWithTags(before),
|
|
1145
|
+
React.createElement(Text, { inverse: true }, at),
|
|
1146
|
+
renderTextWithTags(after))))));
|
|
1056
1147
|
}
|
|
1057
1148
|
return (React.createElement(Box, { key: idx },
|
|
1058
1149
|
React.createElement(Box, { width: 2 },
|
|
1059
1150
|
React.createElement(Text, null, isFirst ? '> ' : ' ')),
|
|
1060
|
-
React.createElement(Box, null,
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
renderTextWithTags(before),
|
|
1064
|
-
React.createElement(Text, { inverse: true, color: "cyan" }, at),
|
|
1065
|
-
renderTextWithTags(after))) : (
|
|
1066
|
-
// Normal rendering with tag highlighting
|
|
1067
|
-
React.createElement(React.Fragment, null,
|
|
1068
|
-
renderTextWithTags(before),
|
|
1069
|
-
React.createElement(Text, { inverse: true }, at),
|
|
1070
|
-
renderTextWithTags(after))))));
|
|
1071
|
-
}
|
|
1072
|
-
return (React.createElement(Box, { key: idx },
|
|
1151
|
+
React.createElement(Box, null, line ? renderTextWithTags(line) : React.createElement(Text, null, ' '))));
|
|
1152
|
+
}),
|
|
1153
|
+
hasMoreBelow && (React.createElement(Box, null,
|
|
1073
1154
|
React.createElement(Box, { width: 2 },
|
|
1074
|
-
React.createElement(Text,
|
|
1075
|
-
React.createElement(
|
|
1076
|
-
})));
|
|
1155
|
+
React.createElement(Text, { dimColor: true }, "\\u22EE ")),
|
|
1156
|
+
React.createElement(Text, { dimColor: true, italic: true }, "(scroll down for more)")))));
|
|
1077
1157
|
};
|
|
1078
1158
|
export default CleanTextInput;
|
|
1079
1159
|
//# sourceMappingURL=CleanTextInput.js.map`,
|
|
1080
1160
|
mimeType: 'application/javascript',
|
|
1081
|
-
size:
|
|
1161
|
+
size: 34293
|
|
1082
1162
|
},
|
|
1083
1163
|
'DmuxApp.js': {
|
|
1084
|
-
content: `import React, { useState, useEffect } from
|
|
1085
|
-
import { Box, Text, useInput, useApp } from
|
|
1086
|
-
import { execSync } from
|
|
1087
|
-
import
|
|
1088
|
-
import
|
|
1164
|
+
content: `import React, { useState, useEffect } from "react";
|
|
1165
|
+
import { Box, Text, useInput, useApp, useStdout } from "ink";
|
|
1166
|
+
import { execSync } from "child_process";
|
|
1167
|
+
import fs from "fs/promises";
|
|
1168
|
+
import path from "path";
|
|
1169
|
+
import { createRequire } from "module";
|
|
1089
1170
|
// Hooks
|
|
1090
|
-
import usePanes from
|
|
1091
|
-
import useProjectSettings from
|
|
1092
|
-
import useTerminalWidth from
|
|
1093
|
-
import useNavigation from
|
|
1094
|
-
import useAutoUpdater from
|
|
1095
|
-
import useAgentDetection from
|
|
1096
|
-
import useAgentStatus from
|
|
1097
|
-
import usePaneRunner from
|
|
1098
|
-
import usePaneCreation from
|
|
1099
|
-
import useActionSystem from
|
|
1171
|
+
import usePanes from "./hooks/usePanes.js";
|
|
1172
|
+
import useProjectSettings from "./hooks/useProjectSettings.js";
|
|
1173
|
+
import useTerminalWidth from "./hooks/useTerminalWidth.js";
|
|
1174
|
+
import useNavigation from "./hooks/useNavigation.js";
|
|
1175
|
+
import useAutoUpdater from "./hooks/useAutoUpdater.js";
|
|
1176
|
+
import useAgentDetection from "./hooks/useAgentDetection.js";
|
|
1177
|
+
import useAgentStatus from "./hooks/useAgentStatus.js";
|
|
1178
|
+
import usePaneRunner from "./hooks/usePaneRunner.js";
|
|
1179
|
+
import usePaneCreation from "./hooks/usePaneCreation.js";
|
|
1180
|
+
import useActionSystem from "./hooks/useActionSystem.js";
|
|
1100
1181
|
// Utils
|
|
1101
|
-
import {
|
|
1102
|
-
import {
|
|
1103
|
-
import {
|
|
1104
|
-
import {
|
|
1105
|
-
import {
|
|
1106
|
-
import {
|
|
1107
|
-
import {
|
|
1108
|
-
import {
|
|
1109
|
-
import {
|
|
1182
|
+
import { enforceControlPaneSize } from "./utils/tmux.js";
|
|
1183
|
+
import { SIDEBAR_WIDTH } from "./utils/layoutManager.js";
|
|
1184
|
+
import { suggestCommand } from "./utils/commands.js";
|
|
1185
|
+
import { generateSlug } from "./utils/slug.js";
|
|
1186
|
+
import { getMainBranch } from "./utils/git.js";
|
|
1187
|
+
import { capturePaneContent } from "./utils/paneCapture.js";
|
|
1188
|
+
import { supportsPopups, launchNodePopupNonBlocking, POPUP_POSITIONING, } from "./utils/popup.js";
|
|
1189
|
+
import { StateManager } from "./shared/StateManager.js";
|
|
1190
|
+
import { LogService } from "./services/LogService.js";
|
|
1191
|
+
import { getStatusDetector, } from "./services/StatusDetector.js";
|
|
1192
|
+
import { PaneAction, getAvailableActions, } from "./actions/index.js";
|
|
1193
|
+
import { SettingsManager, SETTING_DEFINITIONS, } from "./utils/settingsManager.js";
|
|
1194
|
+
import { fileURLToPath } from "url";
|
|
1195
|
+
import { dirname } from "path";
|
|
1196
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
1197
|
+
const __dirname = dirname(__filename);
|
|
1110
1198
|
const require = createRequire(import.meta.url);
|
|
1111
|
-
const packageJson = require(
|
|
1112
|
-
import PanesGrid from
|
|
1113
|
-
import
|
|
1114
|
-
import
|
|
1115
|
-
import
|
|
1116
|
-
import
|
|
1117
|
-
import
|
|
1118
|
-
import
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
import QRCode from './components/QRCode.js';
|
|
1123
|
-
import KebabMenu from './components/KebabMenu.js';
|
|
1124
|
-
import ActionChoiceDialog from './components/ActionChoiceDialog.js';
|
|
1125
|
-
import ActionConfirmDialog from './components/ActionConfirmDialog.js';
|
|
1126
|
-
import ActionInputDialog from './components/ActionInputDialog.js';
|
|
1127
|
-
import ActionProgressDialog from './components/ActionProgressDialog.js';
|
|
1128
|
-
import SettingsDialog from './components/SettingsDialog.js';
|
|
1129
|
-
import HooksDialog from './components/HooksDialog.js';
|
|
1130
|
-
const DmuxApp = ({ panesFile, projectName, sessionName, settingsFile, projectRoot, autoUpdater, serverPort, server }) => {
|
|
1199
|
+
const packageJson = require("../package.json");
|
|
1200
|
+
import PanesGrid from "./components/PanesGrid.js";
|
|
1201
|
+
import CommandPromptDialog from "./components/CommandPromptDialog.js";
|
|
1202
|
+
import FileCopyPrompt from "./components/FileCopyPrompt.js";
|
|
1203
|
+
import LoadingIndicator from "./components/LoadingIndicator.js";
|
|
1204
|
+
import RunningIndicator from "./components/RunningIndicator.js";
|
|
1205
|
+
import UpdatingIndicator from "./components/UpdatingIndicator.js";
|
|
1206
|
+
import FooterHelp from "./components/FooterHelp.js";
|
|
1207
|
+
const DmuxApp = ({ panesFile, projectName, sessionName, settingsFile, projectRoot, autoUpdater, serverPort, server, controlPaneId, }) => {
|
|
1208
|
+
const { stdout } = useStdout();
|
|
1209
|
+
const terminalHeight = stdout?.rows || 40;
|
|
1131
1210
|
/* panes state moved to usePanes */
|
|
1132
1211
|
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
1133
|
-
const [
|
|
1134
|
-
const [newPanePrompt, setNewPanePrompt] = useState('');
|
|
1135
|
-
const [statusMessage, setStatusMessage] = useState('');
|
|
1212
|
+
const [statusMessage, setStatusMessage] = useState("");
|
|
1136
1213
|
const [isCreatingPane, setIsCreatingPane] = useState(false);
|
|
1137
|
-
const [showQRCode, setShowQRCode] = useState(false);
|
|
1138
|
-
const [tunnelUrl, setTunnelUrl] = useState(null);
|
|
1139
|
-
const [isCreatingTunnel, setIsCreatingTunnel] = useState(false);
|
|
1140
1214
|
// Settings state
|
|
1141
1215
|
const [settingsManager] = useState(() => new SettingsManager(projectRoot));
|
|
1142
|
-
const [showSettingsDialog, setShowSettingsDialog] = useState(false);
|
|
1143
|
-
const [settingsMode, setSettingsMode] = useState('list');
|
|
1144
|
-
const [settingsSelectedIndex, setSettingsSelectedIndex] = useState(0);
|
|
1145
|
-
const [settingsEditingKey, setSettingsEditingKey] = useState();
|
|
1146
|
-
const [settingsEditingValueIndex, setSettingsEditingValueIndex] = useState(0);
|
|
1147
|
-
const [settingsScopeIndex, setSettingsScopeIndex] = useState(0);
|
|
1148
1216
|
// Force repaint trigger - incrementing this causes Ink to re-render
|
|
1149
1217
|
const [forceRepaintTrigger, setForceRepaintTrigger] = useState(0);
|
|
1150
1218
|
// Spinner state - shows for a few frames to force render
|
|
1151
1219
|
const [showRepaintSpinner, setShowRepaintSpinner] = useState(false);
|
|
1152
1220
|
const { projectSettings, saveSettings } = useProjectSettings(settingsFile);
|
|
1153
|
-
// Hooks management state
|
|
1154
|
-
const [showHooksDialog, setShowHooksDialog] = useState(false);
|
|
1155
|
-
const [hooksSelectedIndex, setHooksSelectedIndex] = useState(0);
|
|
1156
|
-
const [hooksData, setHooksData] = useState([]);
|
|
1157
1221
|
const [showCommandPrompt, setShowCommandPrompt] = useState(null);
|
|
1158
|
-
const [commandInput, setCommandInput] = useState(
|
|
1222
|
+
const [commandInput, setCommandInput] = useState("");
|
|
1159
1223
|
const [showFileCopyPrompt, setShowFileCopyPrompt] = useState(false);
|
|
1160
1224
|
const [currentCommandType, setCurrentCommandType] = useState(null);
|
|
1161
1225
|
const [runningCommand, setRunningCommand] = useState(false);
|
|
1162
1226
|
const [quitConfirmMode, setQuitConfirmMode] = useState(false);
|
|
1163
|
-
const [showKebabMenu, setShowKebabMenu] = useState(false);
|
|
1164
|
-
const [kebabMenuPaneIndex, setKebabMenuPaneIndex] = useState(null);
|
|
1165
|
-
const [kebabMenuOption, setKebabMenuOption] = useState(0);
|
|
1166
|
-
const [kebabMenuActions, setKebabMenuActions] = useState([]);
|
|
1167
1227
|
// Debug message state - for temporary logging messages
|
|
1168
|
-
const [debugMessage, setDebugMessage] = useState(
|
|
1228
|
+
const [debugMessage, setDebugMessage] = useState("");
|
|
1229
|
+
// Current git branch state (for dev builds)
|
|
1230
|
+
const [currentBranch, setCurrentBranch] = useState(null);
|
|
1169
1231
|
// Update state handled by hook
|
|
1170
|
-
const { updateInfo, showUpdateDialog, isUpdating, performUpdate, skipUpdate, dismissUpdate, updateAvailable } = useAutoUpdater(autoUpdater, setStatusMessage);
|
|
1232
|
+
const { updateInfo, showUpdateDialog, isUpdating, performUpdate, skipUpdate, dismissUpdate, updateAvailable, } = useAutoUpdater(autoUpdater, setStatusMessage);
|
|
1171
1233
|
const { exit } = useApp();
|
|
1234
|
+
// Flag to ignore input temporarily after popup closes (prevents buffered keys)
|
|
1235
|
+
const [ignoreInput, setIgnoreInput] = useState(false);
|
|
1172
1236
|
// Agent selection state
|
|
1173
1237
|
const { availableAgents } = useAgentDetection();
|
|
1174
|
-
const [showAgentChoiceDialog, setShowAgentChoiceDialog] = useState(false);
|
|
1175
1238
|
const [agentChoice, setAgentChoice] = useState(null);
|
|
1176
|
-
|
|
1239
|
+
// Popup support detection
|
|
1240
|
+
const [popupsSupported, setPopupsSupported] = useState(false);
|
|
1177
1241
|
// Track terminal dimensions for responsive layout
|
|
1178
1242
|
const terminalWidth = useTerminalWidth();
|
|
1243
|
+
// Track unread error and warning counts for logs badge
|
|
1244
|
+
const [unreadErrorCount, setUnreadErrorCount] = useState(0);
|
|
1245
|
+
const [unreadWarningCount, setUnreadWarningCount] = useState(0);
|
|
1246
|
+
// Tunnel state
|
|
1247
|
+
const [tunnelUrl, setTunnelUrl] = useState(null);
|
|
1248
|
+
const [tunnelCreating, setTunnelCreating] = useState(false);
|
|
1249
|
+
const [tunnelSpinnerFrame, setTunnelSpinnerFrame] = useState(0);
|
|
1250
|
+
const [localIp, setLocalIp] = useState("127.0.0.1");
|
|
1251
|
+
const [tunnelCopied, setTunnelCopied] = useState(false);
|
|
1252
|
+
// Subscribe to StateManager for unread error/warning count updates
|
|
1253
|
+
useEffect(() => {
|
|
1254
|
+
const stateManager = StateManager.getInstance();
|
|
1255
|
+
const updateCounts = () => {
|
|
1256
|
+
setUnreadErrorCount(stateManager.getUnreadErrorCount());
|
|
1257
|
+
setUnreadWarningCount(stateManager.getUnreadWarningCount());
|
|
1258
|
+
};
|
|
1259
|
+
// Initial count
|
|
1260
|
+
updateCounts();
|
|
1261
|
+
// Subscribe to changes
|
|
1262
|
+
const unsubscribe = stateManager.subscribe(updateCounts);
|
|
1263
|
+
return () => {
|
|
1264
|
+
unsubscribe();
|
|
1265
|
+
};
|
|
1266
|
+
}, []);
|
|
1179
1267
|
// Panes state and persistence (skipLoading will be updated after actionSystem is initialized)
|
|
1180
1268
|
const { panes, setPanes, isLoading, loadPanes, savePanes } = usePanes(panesFile, false);
|
|
1181
1269
|
// Track intentionally closed panes to prevent race condition
|
|
1182
1270
|
// When a user closes a pane, we add it to this set. If the worker detects
|
|
1183
1271
|
// the pane is gone (which it will), we check this set first before re-saving.
|
|
1184
1272
|
const intentionallyClosedPanes = React.useRef(new Set());
|
|
1185
|
-
// Action system
|
|
1186
|
-
const actionSystem = useActionSystem({
|
|
1187
|
-
panes,
|
|
1188
|
-
savePanes,
|
|
1189
|
-
sessionName,
|
|
1190
|
-
projectName,
|
|
1191
|
-
onPaneRemove: (paneId) => {
|
|
1192
|
-
// Mark this pane as intentionally closed
|
|
1193
|
-
intentionallyClosedPanes.current.add(paneId);
|
|
1194
|
-
const updated = panes.filter(p => p.id !== paneId);
|
|
1195
|
-
setPanes(updated);
|
|
1196
|
-
// Clean up the tracking after a delay (in case of race conditions)
|
|
1197
|
-
setTimeout(() => {
|
|
1198
|
-
intentionallyClosedPanes.current.delete(paneId);
|
|
1199
|
-
}, 5000);
|
|
1200
|
-
},
|
|
1201
|
-
});
|
|
1202
1273
|
// Pane runner
|
|
1203
|
-
const { copyNonGitFiles, runCommandInternal, monitorTestOutput, monitorDevOutput, attachBackgroundWindow } = usePaneRunner({
|
|
1274
|
+
const { copyNonGitFiles, runCommandInternal, monitorTestOutput, monitorDevOutput, attachBackgroundWindow, } = usePaneRunner({
|
|
1204
1275
|
panes,
|
|
1205
1276
|
savePanes,
|
|
1206
1277
|
projectSettings,
|
|
@@ -1209,7 +1280,7 @@ const DmuxApp = ({ panesFile, projectName, sessionName, settingsFile, projectRoo
|
|
|
1209
1280
|
});
|
|
1210
1281
|
// Force repaint helper - shows spinner for a few frames to force full re-render
|
|
1211
1282
|
const forceRepaint = () => {
|
|
1212
|
-
setForceRepaintTrigger(prev => prev + 1);
|
|
1283
|
+
setForceRepaintTrigger((prev) => prev + 1);
|
|
1213
1284
|
setShowRepaintSpinner(true);
|
|
1214
1285
|
// Hide spinner after a few frames (enough to trigger multiple renders)
|
|
1215
1286
|
setTimeout(() => setShowRepaintSpinner(false), 100);
|
|
@@ -1220,13 +1291,57 @@ const DmuxApp = ({ panesFile, projectName, sessionName, settingsFile, projectRoo
|
|
|
1220
1291
|
// Small delay to ensure terminal is ready
|
|
1221
1292
|
const timer = setTimeout(() => {
|
|
1222
1293
|
try {
|
|
1223
|
-
execSync(
|
|
1294
|
+
execSync("tmux refresh-client", { stdio: "pipe" });
|
|
1224
1295
|
}
|
|
1225
1296
|
catch { }
|
|
1226
1297
|
}, 50);
|
|
1227
1298
|
return () => clearTimeout(timer);
|
|
1228
1299
|
}
|
|
1229
1300
|
}, [forceRepaintTrigger]);
|
|
1301
|
+
// Get local network IP on mount
|
|
1302
|
+
useEffect(() => {
|
|
1303
|
+
try {
|
|
1304
|
+
// Get local IP address (not 127.0.0.1)
|
|
1305
|
+
const result = execSync(\`hostname -I 2>/dev/null || ifconfig | grep "inet " | grep -v 127.0.0.1 | awk '{print $2}' | head -1\`, {
|
|
1306
|
+
encoding: "utf-8",
|
|
1307
|
+
stdio: "pipe",
|
|
1308
|
+
}).trim();
|
|
1309
|
+
if (result) {
|
|
1310
|
+
setLocalIp(result.split(" ")[0]); // Take first IP if multiple
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
catch {
|
|
1314
|
+
// Fallback to 127.0.0.1
|
|
1315
|
+
setLocalIp("127.0.0.1");
|
|
1316
|
+
}
|
|
1317
|
+
}, []);
|
|
1318
|
+
// Spinner animation for tunnel creation
|
|
1319
|
+
useEffect(() => {
|
|
1320
|
+
if (!tunnelCreating)
|
|
1321
|
+
return;
|
|
1322
|
+
const spinnerInterval = setInterval(() => {
|
|
1323
|
+
setTunnelSpinnerFrame((prev) => (prev + 1) % 10);
|
|
1324
|
+
}, 80); // Update every 80ms
|
|
1325
|
+
return () => clearInterval(spinnerInterval);
|
|
1326
|
+
}, [tunnelCreating]);
|
|
1327
|
+
// Get current git branch on mount (only for dev builds)
|
|
1328
|
+
useEffect(() => {
|
|
1329
|
+
const isDev = process.env.DMUX_DEV === "true" || __dirname.includes("dist") === false;
|
|
1330
|
+
if (isDev) {
|
|
1331
|
+
try {
|
|
1332
|
+
const branch = execSync("git rev-parse --abbrev-ref HEAD", {
|
|
1333
|
+
encoding: "utf-8",
|
|
1334
|
+
stdio: "pipe",
|
|
1335
|
+
cwd: projectRoot,
|
|
1336
|
+
}).trim();
|
|
1337
|
+
setCurrentBranch(branch);
|
|
1338
|
+
}
|
|
1339
|
+
catch {
|
|
1340
|
+
// Not in a git repo or git not available
|
|
1341
|
+
setCurrentBranch(null);
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1344
|
+
}, [projectRoot]);
|
|
1230
1345
|
// Pane creation
|
|
1231
1346
|
const { createNewPane: createNewPaneHook } = usePaneCreation({
|
|
1232
1347
|
panes,
|
|
@@ -1234,7 +1349,6 @@ const DmuxApp = ({ panesFile, projectName, sessionName, settingsFile, projectRoo
|
|
|
1234
1349
|
projectName,
|
|
1235
1350
|
setIsCreatingPane,
|
|
1236
1351
|
setStatusMessage,
|
|
1237
|
-
setNewPanePrompt,
|
|
1238
1352
|
loadPanes,
|
|
1239
1353
|
panesFile,
|
|
1240
1354
|
availableAgents,
|
|
@@ -1244,8 +1358,8 @@ const DmuxApp = ({ panesFile, projectName, sessionName, settingsFile, projectRoo
|
|
|
1244
1358
|
useEffect(() => {
|
|
1245
1359
|
const statusDetector = getStatusDetector();
|
|
1246
1360
|
const handleStatusUpdate = (event) => {
|
|
1247
|
-
setPanes(prevPanes => {
|
|
1248
|
-
const updatedPanes = prevPanes.map(pane => {
|
|
1361
|
+
setPanes((prevPanes) => {
|
|
1362
|
+
const updatedPanes = prevPanes.map((pane) => {
|
|
1249
1363
|
if (pane.id === event.paneId) {
|
|
1250
1364
|
const updated = {
|
|
1251
1365
|
...pane,
|
|
@@ -1269,22 +1383,23 @@ const DmuxApp = ({ panesFile, projectName, sessionName, settingsFile, projectRoo
|
|
|
1269
1383
|
updated.analyzerError = event.analyzerError;
|
|
1270
1384
|
}
|
|
1271
1385
|
// Clear option dialog data when transitioning away from 'waiting' state
|
|
1272
|
-
if (event.status !==
|
|
1386
|
+
if (event.status !== "waiting" && pane.agentStatus === "waiting") {
|
|
1273
1387
|
updated.optionsQuestion = undefined;
|
|
1274
1388
|
updated.options = undefined;
|
|
1275
1389
|
updated.potentialHarm = undefined;
|
|
1276
1390
|
}
|
|
1277
1391
|
// Clear summary when transitioning away from 'idle' state
|
|
1278
|
-
if (event.status !==
|
|
1392
|
+
if (event.status !== "idle" && pane.agentStatus === "idle") {
|
|
1279
1393
|
updated.agentSummary = undefined;
|
|
1280
1394
|
}
|
|
1281
1395
|
// Clear analyzer error when successfully getting a new analysis
|
|
1282
1396
|
// or when transitioning to 'working' status
|
|
1283
|
-
if (event.status ===
|
|
1397
|
+
if (event.status === "working") {
|
|
1284
1398
|
updated.analyzerError = undefined;
|
|
1285
1399
|
}
|
|
1286
|
-
else if (event.status ===
|
|
1287
|
-
if (event.analyzerError === undefined &&
|
|
1400
|
+
else if (event.status === "waiting" || event.status === "idle") {
|
|
1401
|
+
if (event.analyzerError === undefined &&
|
|
1402
|
+
(event.optionsQuestion || event.summary)) {
|
|
1288
1403
|
updated.analyzerError = undefined;
|
|
1289
1404
|
}
|
|
1290
1405
|
}
|
|
@@ -1293,15 +1408,15 @@ const DmuxApp = ({ panesFile, projectName, sessionName, settingsFile, projectRoo
|
|
|
1293
1408
|
return pane;
|
|
1294
1409
|
});
|
|
1295
1410
|
// Persist to disk - ConfigWatcher will handle syncing to StateManager
|
|
1296
|
-
savePanes(updatedPanes).catch(err => {
|
|
1297
|
-
console.error(
|
|
1411
|
+
savePanes(updatedPanes).catch((err) => {
|
|
1412
|
+
console.error("Failed to save panes after status update:", err);
|
|
1298
1413
|
});
|
|
1299
1414
|
return updatedPanes;
|
|
1300
1415
|
});
|
|
1301
1416
|
};
|
|
1302
|
-
statusDetector.on(
|
|
1417
|
+
statusDetector.on("status-updated", handleStatusUpdate);
|
|
1303
1418
|
return () => {
|
|
1304
|
-
statusDetector.off(
|
|
1419
|
+
statusDetector.off("status-updated", handleStatusUpdate);
|
|
1305
1420
|
};
|
|
1306
1421
|
}, [setPanes, savePanes]);
|
|
1307
1422
|
// Note: No need to sync panes with StateManager here.
|
|
@@ -1326,62 +1441,29 @@ const DmuxApp = ({ panesFile, projectName, sessionName, settingsFile, projectRoo
|
|
|
1326
1441
|
const handleTermination = () => {
|
|
1327
1442
|
cleanExit();
|
|
1328
1443
|
};
|
|
1329
|
-
process.on(
|
|
1330
|
-
//
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1444
|
+
process.on("SIGTERM", handleTermination);
|
|
1445
|
+
// Check if tmux supports popups (3.2+) and enable mouse mode for click-outside-to-close
|
|
1446
|
+
const popupSupport = supportsPopups();
|
|
1447
|
+
setPopupsSupported(popupSupport);
|
|
1448
|
+
if (popupSupport) {
|
|
1449
|
+
// Enable mouse mode only for this dmux session (not global)
|
|
1450
|
+
}
|
|
1335
1451
|
return () => {
|
|
1336
|
-
process.removeListener(
|
|
1452
|
+
process.removeListener("SIGTERM", handleTermination);
|
|
1337
1453
|
};
|
|
1338
1454
|
}, []);
|
|
1339
|
-
// Auto-show new pane dialog when starting with no panes
|
|
1340
|
-
useEffect(() => {
|
|
1341
|
-
// Only show the dialog if:
|
|
1342
|
-
// 1. Initial load is complete (!isLoading)
|
|
1343
|
-
// 2. We have no panes
|
|
1344
|
-
// 3. We're not already showing the dialog
|
|
1345
|
-
// 4. We're not showing any other dialogs or prompts
|
|
1346
|
-
if (!isLoading &&
|
|
1347
|
-
panes.length === 0 &&
|
|
1348
|
-
!showNewPaneDialog &&
|
|
1349
|
-
!actionSystem.actionState.showConfirmDialog &&
|
|
1350
|
-
!actionSystem.actionState.showChoiceDialog &&
|
|
1351
|
-
!actionSystem.actionState.showInputDialog &&
|
|
1352
|
-
!actionSystem.actionState.showProgressDialog &&
|
|
1353
|
-
!showCommandPrompt &&
|
|
1354
|
-
!showFileCopyPrompt &&
|
|
1355
|
-
!showAgentChoiceDialog &&
|
|
1356
|
-
!isCreatingPane &&
|
|
1357
|
-
!runningCommand &&
|
|
1358
|
-
!isUpdating) {
|
|
1359
|
-
setShowNewPaneDialog(true);
|
|
1360
|
-
}
|
|
1361
|
-
}, [isLoading, panes.length, showNewPaneDialog, actionSystem.actionState.showConfirmDialog, actionSystem.actionState.showChoiceDialog, actionSystem.actionState.showInputDialog, actionSystem.actionState.showProgressDialog, showCommandPrompt, showFileCopyPrompt, showAgentChoiceDialog, isCreatingPane, runningCommand, isUpdating]);
|
|
1362
1455
|
// Update checking moved to useAutoUpdater
|
|
1363
1456
|
// Set default agent choice when detection completes
|
|
1364
1457
|
useEffect(() => {
|
|
1365
1458
|
if (agentChoice == null && availableAgents.length > 0) {
|
|
1366
|
-
setAgentChoice(availableAgents[0] ||
|
|
1459
|
+
setAgentChoice(availableAgents[0] || "claude");
|
|
1367
1460
|
}
|
|
1368
1461
|
}, [availableAgents]);
|
|
1369
|
-
//
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
// Check if this pane was intentionally closed
|
|
1375
|
-
// If so, don't re-save - the close action already handled it
|
|
1376
|
-
if (intentionallyClosedPanes.current.has(paneId)) {
|
|
1377
|
-
return;
|
|
1378
|
-
}
|
|
1379
|
-
// Pane was removed unexpectedly (e.g., user killed tmux pane manually)
|
|
1380
|
-
// Remove it from our tracking
|
|
1381
|
-
const updatedPanes = panes.filter(p => p.id !== paneId);
|
|
1382
|
-
savePanes(updatedPanes);
|
|
1383
|
-
},
|
|
1384
|
-
});
|
|
1462
|
+
// Welcome pane is now fully event-based:
|
|
1463
|
+
// - Created at startup (in src/index.ts)
|
|
1464
|
+
// - Destroyed when first pane is created (in paneCreation.ts)
|
|
1465
|
+
// - Recreated when last pane is closed (in paneActions.ts)
|
|
1466
|
+
// No polling needed!
|
|
1385
1467
|
// loadPanes moved to usePanes
|
|
1386
1468
|
// getPanePositions moved to utils/tmux
|
|
1387
1469
|
// Navigation logic moved to hook
|
|
@@ -1389,137 +1471,1057 @@ const DmuxApp = ({ panesFile, projectName, sessionName, settingsFile, projectRoo
|
|
|
1389
1471
|
// findCardInDirection provided by useNavigation
|
|
1390
1472
|
// savePanes moved to usePanes
|
|
1391
1473
|
// applySmartLayout moved to utils/tmux
|
|
1392
|
-
const
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1474
|
+
const launchNewPanePopup = async () => {
|
|
1475
|
+
// Only launch popup if tmux supports it
|
|
1476
|
+
if (!popupsSupported) {
|
|
1477
|
+
setStatusMessage("Popups require tmux 3.2+");
|
|
1478
|
+
setTimeout(() => setStatusMessage(""), 3000);
|
|
1479
|
+
return;
|
|
1480
|
+
}
|
|
1399
1481
|
try {
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1482
|
+
// Resolve the popup script path from the project root
|
|
1483
|
+
// This handles both dev (tsx running from src) and prod (compiled to dist)
|
|
1484
|
+
const projectRootForPopup = __dirname.includes("/dist")
|
|
1485
|
+
? path.resolve(__dirname, "..") // If in dist/, go up one level
|
|
1486
|
+
: path.resolve(__dirname, ".."); // If in src/, go up one level
|
|
1487
|
+
const popupScriptPath = path.join(projectRootForPopup, "dist", "popups", "newPanePopup.js");
|
|
1488
|
+
// Calculate popup height as 80% of terminal height to allow room for file list
|
|
1489
|
+
const popupHeight = Math.floor(terminalHeight * 0.8);
|
|
1490
|
+
// Launch the popup non-blocking and track it
|
|
1491
|
+
const popupHandle = launchNodePopupNonBlocking(popupScriptPath, [], {
|
|
1492
|
+
...POPUP_POSITIONING.centeredWithSidebar(SIDEBAR_WIDTH),
|
|
1493
|
+
width: 90,
|
|
1494
|
+
height: popupHeight,
|
|
1495
|
+
title: " ✨ dmux - Create New Pane ",
|
|
1496
|
+
});
|
|
1497
|
+
LogService.getInstance().debug(\`Popup created - PID: \${popupHandle.pid}, bounds: \${JSON.stringify(popupHandle.bounds)}\`, "PopupTracking");
|
|
1498
|
+
// Wait for the popup to close
|
|
1499
|
+
const result = await popupHandle.resultPromise;
|
|
1500
|
+
// Clear active popup tracking
|
|
1501
|
+
LogService.getInstance().debug("Popup closed, clearing tracking", "PopupTracking");
|
|
1502
|
+
// Ignore input briefly after popup closes to prevent buffered keys
|
|
1503
|
+
setIgnoreInput(true);
|
|
1504
|
+
setTimeout(() => setIgnoreInput(false), 100);
|
|
1505
|
+
if (result.success && result.data) {
|
|
1506
|
+
// User entered a prompt - now decide which agent to use
|
|
1507
|
+
const promptValue = result.data;
|
|
1508
|
+
const agents = availableAgents;
|
|
1509
|
+
if (agents.length === 0) {
|
|
1510
|
+
await createNewPaneHook(promptValue);
|
|
1511
|
+
}
|
|
1512
|
+
else if (agents.length === 1) {
|
|
1513
|
+
await createNewPaneHook(promptValue, agents[0]);
|
|
1514
|
+
}
|
|
1515
|
+
else {
|
|
1516
|
+
// Multiple agents available - check for default agent setting first
|
|
1517
|
+
const settings = settingsManager.getSettings();
|
|
1518
|
+
if (settings.defaultAgent && agents.includes(settings.defaultAgent)) {
|
|
1519
|
+
// Use the default agent from settings
|
|
1520
|
+
await createNewPaneHook(promptValue, settings.defaultAgent);
|
|
1521
|
+
}
|
|
1522
|
+
else {
|
|
1523
|
+
// No default agent configured or default not available - show agent choice popup
|
|
1524
|
+
const selectedAgent = await launchAgentChoicePopup(promptValue);
|
|
1525
|
+
if (selectedAgent) {
|
|
1526
|
+
await createNewPaneHook(promptValue, selectedAgent);
|
|
1527
|
+
}
|
|
1528
|
+
}
|
|
1529
|
+
}
|
|
1530
|
+
}
|
|
1531
|
+
else if (result.cancelled) {
|
|
1532
|
+
// User pressed ESC - do nothing
|
|
1533
|
+
return;
|
|
1534
|
+
}
|
|
1535
|
+
else if (result.error) {
|
|
1536
|
+
setStatusMessage(\`Popup error: \${result.error}\`);
|
|
1537
|
+
setTimeout(() => setStatusMessage(""), 3000);
|
|
1538
|
+
}
|
|
1404
1539
|
}
|
|
1405
|
-
catch {
|
|
1406
|
-
|
|
1407
|
-
|
|
1540
|
+
catch (error) {
|
|
1541
|
+
setStatusMessage(\`Failed to launch popup: \${error.message}\`);
|
|
1542
|
+
setTimeout(() => setStatusMessage(""), 3000);
|
|
1408
1543
|
}
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
//
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
// 2. Fill with blank lines to push content off screen
|
|
1417
|
-
process.stdout.write('\\n'.repeat(100));
|
|
1418
|
-
// 3. Clear tmux history and send clear command
|
|
1419
|
-
try {
|
|
1420
|
-
execSync('tmux clear-history', { stdio: 'pipe' });
|
|
1421
|
-
execSync('tmux send-keys C-l', { stdio: 'pipe' });
|
|
1544
|
+
};
|
|
1545
|
+
const launchKebabMenuPopup = async (paneIndex) => {
|
|
1546
|
+
// Only launch popup if tmux supports it
|
|
1547
|
+
if (!popupsSupported) {
|
|
1548
|
+
setStatusMessage("Popups require tmux 3.2+");
|
|
1549
|
+
setTimeout(() => setStatusMessage(""), 3000);
|
|
1550
|
+
return;
|
|
1422
1551
|
}
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
// 4. Force tmux to refresh the display
|
|
1427
|
-
try {
|
|
1428
|
-
execSync('tmux refresh-client', { stdio: 'pipe' });
|
|
1552
|
+
const selectedPane = panes[paneIndex];
|
|
1553
|
+
if (!selectedPane) {
|
|
1554
|
+
return;
|
|
1429
1555
|
}
|
|
1430
|
-
catch { }
|
|
1431
|
-
// Get current pane count to determine layout
|
|
1432
|
-
const paneCount = parseInt(execSync('tmux list-panes | wc -l', { encoding: 'utf-8' }).trim());
|
|
1433
|
-
// Enable pane borders to show titles
|
|
1434
1556
|
try {
|
|
1435
|
-
|
|
1557
|
+
// Resolve the popup script path
|
|
1558
|
+
const projectRootForPopup = __dirname.includes("/dist")
|
|
1559
|
+
? path.resolve(__dirname, "..") // If in dist/, go up one level
|
|
1560
|
+
: path.resolve(__dirname, ".."); // If in src/, go up one level
|
|
1561
|
+
const popupScriptPath = path.join(projectRootForPopup, "dist", "popups", "kebabMenuPopup.js");
|
|
1562
|
+
// Get available actions for this pane
|
|
1563
|
+
const actions = getAvailableActions(selectedPane, projectSettings);
|
|
1564
|
+
const actionsJson = JSON.stringify(actions);
|
|
1565
|
+
// Launch the popup non-blocking and track it
|
|
1566
|
+
const popupHandle = launchNodePopupNonBlocking(popupScriptPath, [selectedPane.slug, actionsJson], {
|
|
1567
|
+
...POPUP_POSITIONING.standard(SIDEBAR_WIDTH),
|
|
1568
|
+
width: 60,
|
|
1569
|
+
height: Math.min(20, actions.length + 5),
|
|
1570
|
+
title: \`Menu: \${selectedPane.slug}\`,
|
|
1571
|
+
});
|
|
1572
|
+
// Wait for the popup to close
|
|
1573
|
+
const result = await popupHandle.resultPromise;
|
|
1574
|
+
// Clear active popup tracking
|
|
1575
|
+
// Log the entire result for debugging
|
|
1576
|
+
LogService.getInstance().debug(\`Kebab menu result: \${JSON.stringify(result)}\`, "KebabMenu");
|
|
1577
|
+
if (result.success && result.data) {
|
|
1578
|
+
// User selected an action
|
|
1579
|
+
const actionId = result.data;
|
|
1580
|
+
LogService.getInstance().debug(\`Action selected: \${actionId}\`, "KebabMenu");
|
|
1581
|
+
// Handle merge action with dedicated popup
|
|
1582
|
+
if (actionId === PaneAction.MERGE) {
|
|
1583
|
+
LogService.getInstance().debug(\`Merge action selected for pane: \${selectedPane.slug}\`, "MergeAction");
|
|
1584
|
+
try {
|
|
1585
|
+
await launchMergePopup(selectedPane);
|
|
1586
|
+
LogService.getInstance().debug("Merge popup completed", "MergeAction");
|
|
1587
|
+
}
|
|
1588
|
+
catch (error) {
|
|
1589
|
+
LogService.getInstance().error("Merge popup error", "MergeAction", selectedPane.id, error instanceof Error ? error : undefined);
|
|
1590
|
+
setStatusMessage(\`Merge popup failed: \${error}\`);
|
|
1591
|
+
setTimeout(() => setStatusMessage(""), 3000);
|
|
1592
|
+
}
|
|
1593
|
+
}
|
|
1594
|
+
else {
|
|
1595
|
+
// Execute other actions through action system
|
|
1596
|
+
await actionSystem.executeAction(actionId, selectedPane, {
|
|
1597
|
+
mainBranch: getMainBranch(),
|
|
1598
|
+
});
|
|
1599
|
+
}
|
|
1600
|
+
}
|
|
1601
|
+
else if (result.cancelled) {
|
|
1602
|
+
// User pressed ESC - do nothing
|
|
1603
|
+
LogService.getInstance().debug("Kebab menu cancelled", "KebabMenu");
|
|
1604
|
+
return;
|
|
1605
|
+
}
|
|
1606
|
+
else if (result.error) {
|
|
1607
|
+
LogService.getInstance().error(\`Kebab menu error: \${result.error}\`, "KebabMenu");
|
|
1608
|
+
setStatusMessage(\`Popup error: \${result.error}\`);
|
|
1609
|
+
setTimeout(() => setStatusMessage(""), 3000);
|
|
1610
|
+
}
|
|
1611
|
+
else {
|
|
1612
|
+
LogService.getInstance().warn(\`Unexpected kebab menu result: \${JSON.stringify(result)}\`, "KebabMenu");
|
|
1613
|
+
}
|
|
1436
1614
|
}
|
|
1437
|
-
catch {
|
|
1438
|
-
|
|
1615
|
+
catch (error) {
|
|
1616
|
+
setStatusMessage(\`Failed to launch popup: \${error.message}\`);
|
|
1617
|
+
setTimeout(() => setStatusMessage(""), 3000);
|
|
1618
|
+
}
|
|
1619
|
+
};
|
|
1620
|
+
const launchConfirmPopup = async (title, message, yesLabel, noLabel) => {
|
|
1621
|
+
// Only launch popup if tmux supports it
|
|
1622
|
+
if (!popupsSupported) {
|
|
1623
|
+
setStatusMessage("Popups require tmux 3.2+");
|
|
1624
|
+
setTimeout(() => setStatusMessage(""), 3000);
|
|
1625
|
+
return false;
|
|
1439
1626
|
}
|
|
1440
|
-
// Create new pane
|
|
1441
|
-
const paneInfo = execSync(\`tmux split-window -h -P -F '#{pane_id}'\`, { encoding: 'utf-8' }).trim();
|
|
1442
|
-
// Wait for pane creation to settle
|
|
1443
|
-
await new Promise(resolve => setTimeout(resolve, 500));
|
|
1444
|
-
// Set pane title to match the slug
|
|
1445
1627
|
try {
|
|
1446
|
-
|
|
1628
|
+
// Resolve the popup script path
|
|
1629
|
+
const projectRootForPopup = __dirname.includes("/dist")
|
|
1630
|
+
? path.resolve(__dirname, "..") // If in dist/, go up one level
|
|
1631
|
+
: path.resolve(__dirname, ".."); // If in src/, go up one level
|
|
1632
|
+
const popupScriptPath = path.join(projectRootForPopup, "dist", "popups", "confirmPopup.js");
|
|
1633
|
+
// Write data to temp file to avoid shell escaping issues
|
|
1634
|
+
const dataFile = \`/tmp/dmux-confirm-\${Date.now()}.json\`;
|
|
1635
|
+
const dataJson = JSON.stringify({ title, message, yesLabel, noLabel });
|
|
1636
|
+
await fs.writeFile(dataFile, dataJson);
|
|
1637
|
+
// Launch the popup non-blocking and track it
|
|
1638
|
+
const popupHandle = launchNodePopupNonBlocking(popupScriptPath, [dataFile], {
|
|
1639
|
+
...POPUP_POSITIONING.standard(SIDEBAR_WIDTH),
|
|
1640
|
+
width: 60,
|
|
1641
|
+
height: 12,
|
|
1642
|
+
title: title || "Confirm",
|
|
1643
|
+
});
|
|
1644
|
+
// Wait for the popup to close
|
|
1645
|
+
const result = await popupHandle.resultPromise;
|
|
1646
|
+
// Clear active popup tracking
|
|
1647
|
+
// Clean up temp file
|
|
1648
|
+
try {
|
|
1649
|
+
await fs.unlink(dataFile);
|
|
1650
|
+
}
|
|
1651
|
+
catch { }
|
|
1652
|
+
if (result.success && result.data !== undefined) {
|
|
1653
|
+
return result.data;
|
|
1654
|
+
}
|
|
1655
|
+
else if (result.cancelled) {
|
|
1656
|
+
return false;
|
|
1657
|
+
}
|
|
1658
|
+
else if (result.error) {
|
|
1659
|
+
setStatusMessage(\`Popup error: \${result.error}\`);
|
|
1660
|
+
setTimeout(() => setStatusMessage(""), 3000);
|
|
1661
|
+
return false;
|
|
1662
|
+
}
|
|
1447
1663
|
}
|
|
1448
|
-
catch {
|
|
1449
|
-
|
|
1664
|
+
catch (error) {
|
|
1665
|
+
setStatusMessage(\`Failed to launch popup: \${error.message}\`);
|
|
1666
|
+
setTimeout(() => setStatusMessage(""), 3000);
|
|
1667
|
+
}
|
|
1668
|
+
return false;
|
|
1669
|
+
};
|
|
1670
|
+
const launchAgentChoicePopup = async (prompt) => {
|
|
1671
|
+
// Only launch popup if tmux supports it
|
|
1672
|
+
if (!popupsSupported) {
|
|
1673
|
+
setStatusMessage("Popups require tmux 3.2+");
|
|
1674
|
+
setTimeout(() => setStatusMessage(""), 3000);
|
|
1675
|
+
return null;
|
|
1450
1676
|
}
|
|
1451
|
-
// Apply smart layout based on pane count
|
|
1452
|
-
const newPaneCount = paneCount + 1;
|
|
1453
|
-
applySmartLayout(newPaneCount);
|
|
1454
|
-
// Create git worktree and cd into it
|
|
1455
|
-
// This MUST happen before launching Claude to ensure we're in the right directory
|
|
1456
1677
|
try {
|
|
1457
|
-
//
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
//
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1678
|
+
// Resolve the popup script path
|
|
1679
|
+
const projectRootForPopup = __dirname.includes("/dist")
|
|
1680
|
+
? path.resolve(__dirname, "..") // If in dist/, go up one level
|
|
1681
|
+
: path.resolve(__dirname, ".."); // If in src/, go up one level
|
|
1682
|
+
const popupScriptPath = path.join(projectRootForPopup, "dist", "popups", "agentChoicePopup.js");
|
|
1683
|
+
const agentsJson = JSON.stringify(availableAgents);
|
|
1684
|
+
const defaultAgentArg = agentChoice || availableAgents[0] || "claude";
|
|
1685
|
+
// Launch the popup non-blocking and track it
|
|
1686
|
+
const popupHandle = launchNodePopupNonBlocking(popupScriptPath, [agentsJson, defaultAgentArg], {
|
|
1687
|
+
...POPUP_POSITIONING.standard(SIDEBAR_WIDTH),
|
|
1688
|
+
width: 50,
|
|
1689
|
+
height: 10,
|
|
1690
|
+
title: "Select Agent",
|
|
1691
|
+
});
|
|
1692
|
+
// Wait for the popup to close
|
|
1693
|
+
const result = await popupHandle.resultPromise;
|
|
1694
|
+
// Clear active popup tracking
|
|
1695
|
+
if (result.success && result.data) {
|
|
1696
|
+
return result.data;
|
|
1697
|
+
}
|
|
1698
|
+
else if (result.cancelled) {
|
|
1699
|
+
return null;
|
|
1700
|
+
}
|
|
1701
|
+
else if (result.error) {
|
|
1702
|
+
setStatusMessage(\`Popup error: \${result.error}\`);
|
|
1703
|
+
setTimeout(() => setStatusMessage(""), 3000);
|
|
1704
|
+
return null;
|
|
1705
|
+
}
|
|
1468
1706
|
}
|
|
1469
1707
|
catch (error) {
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
// Even if worktree creation failed, try to cd to the directory in case it exists
|
|
1473
|
-
execSync(\`tmux send-keys -t '\${paneInfo}' 'cd "\${worktreePath}" 2>/dev/null || (echo "ERROR: Failed to create/enter worktree \${slug}" && pwd)' Enter\`, { stdio: 'pipe' });
|
|
1474
|
-
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
1708
|
+
setStatusMessage(\`Failed to launch popup: \${error.message}\`);
|
|
1709
|
+
setTimeout(() => setStatusMessage(""), 3000);
|
|
1475
1710
|
}
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1711
|
+
return null;
|
|
1712
|
+
};
|
|
1713
|
+
const launchHooksPopup = async () => {
|
|
1714
|
+
// Only launch popup if tmux supports it
|
|
1715
|
+
if (!popupsSupported) {
|
|
1716
|
+
setStatusMessage("Popups require tmux 3.2+");
|
|
1717
|
+
setTimeout(() => setStatusMessage(""), 3000);
|
|
1718
|
+
return;
|
|
1719
|
+
}
|
|
1720
|
+
try {
|
|
1721
|
+
// Resolve the popup script path
|
|
1722
|
+
const projectRootForPopup = __dirname.includes("/dist")
|
|
1723
|
+
? path.resolve(__dirname, "..") // If in dist/, go up one level
|
|
1724
|
+
: path.resolve(__dirname, ".."); // If in src/, go up one level
|
|
1725
|
+
const popupScriptPath = path.join(projectRootForPopup, "dist", "popups", "hooksPopup.js");
|
|
1726
|
+
// Get hooks data
|
|
1727
|
+
const { hasHook } = await import("./utils/hooks.js");
|
|
1728
|
+
const allHookTypes = [
|
|
1729
|
+
"before_pane_create",
|
|
1730
|
+
"pane_created",
|
|
1731
|
+
"worktree_created",
|
|
1732
|
+
"before_pane_close",
|
|
1733
|
+
"pane_closed",
|
|
1734
|
+
"before_worktree_remove",
|
|
1735
|
+
"worktree_removed",
|
|
1736
|
+
"pre_merge",
|
|
1737
|
+
"post_merge",
|
|
1738
|
+
"run_test",
|
|
1739
|
+
"run_dev",
|
|
1740
|
+
];
|
|
1741
|
+
const hooks = allHookTypes.map((hookName) => ({
|
|
1742
|
+
name: hookName,
|
|
1743
|
+
active: hasHook(projectRoot || process.cwd(), hookName),
|
|
1744
|
+
}));
|
|
1745
|
+
const hooksJson = JSON.stringify(hooks);
|
|
1746
|
+
// Launch the popup non-blocking and track it
|
|
1747
|
+
const popupHandle = launchNodePopupNonBlocking(popupScriptPath, [hooksJson], {
|
|
1748
|
+
...POPUP_POSITIONING.standard(SIDEBAR_WIDTH),
|
|
1749
|
+
width: 70,
|
|
1750
|
+
height: 24,
|
|
1751
|
+
title: "🪝 Manage Hooks",
|
|
1752
|
+
});
|
|
1753
|
+
// Wait for the popup to close
|
|
1754
|
+
const result = await popupHandle.resultPromise;
|
|
1755
|
+
// Clear active popup tracking
|
|
1756
|
+
if (result.success && result.data?.action === "edit") {
|
|
1757
|
+
// Edit hooks using an agent
|
|
1758
|
+
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";
|
|
1759
|
+
// Choose agent
|
|
1760
|
+
const agents = availableAgents;
|
|
1761
|
+
if (agents.length === 0) {
|
|
1762
|
+
await createNewPaneHook(prompt);
|
|
1763
|
+
}
|
|
1764
|
+
else if (agents.length === 1) {
|
|
1765
|
+
await createNewPaneHook(prompt, agents[0]);
|
|
1766
|
+
}
|
|
1767
|
+
else {
|
|
1768
|
+
// Multiple agents available - check for default agent setting first
|
|
1769
|
+
const settings = settingsManager.getSettings();
|
|
1770
|
+
if (settings.defaultAgent && agents.includes(settings.defaultAgent)) {
|
|
1771
|
+
// Use the default agent from settings
|
|
1772
|
+
await createNewPaneHook(prompt, settings.defaultAgent);
|
|
1773
|
+
}
|
|
1774
|
+
else {
|
|
1775
|
+
// No default agent configured or default not available - show agent choice popup
|
|
1776
|
+
const selectedAgent = await launchAgentChoicePopup(prompt);
|
|
1777
|
+
if (selectedAgent) {
|
|
1778
|
+
await createNewPaneHook(prompt, selectedAgent);
|
|
1779
|
+
}
|
|
1780
|
+
}
|
|
1781
|
+
}
|
|
1488
1782
|
}
|
|
1489
|
-
else {
|
|
1490
|
-
|
|
1783
|
+
else if (result.success && result.data?.action === "view") {
|
|
1784
|
+
// View hooks file in editor - could implement this later
|
|
1785
|
+
setStatusMessage("View in editor not yet implemented");
|
|
1786
|
+
setTimeout(() => setStatusMessage(""), 2000);
|
|
1787
|
+
}
|
|
1788
|
+
else if (result.cancelled) {
|
|
1789
|
+
// User pressed ESC - do nothing
|
|
1790
|
+
return;
|
|
1791
|
+
}
|
|
1792
|
+
else if (result.error) {
|
|
1793
|
+
setStatusMessage(\`Popup error: \${result.error}\`);
|
|
1794
|
+
setTimeout(() => setStatusMessage(""), 3000);
|
|
1491
1795
|
}
|
|
1492
|
-
// Send Claude command to new pane
|
|
1493
|
-
escapedCmd = claudeCmd.replace(/'/g, "'\\\\''");
|
|
1494
|
-
execSync(\`tmux send-keys -t '\${paneInfo}' '\${escapedCmd}'\`, { stdio: 'pipe' });
|
|
1495
|
-
execSync(\`tmux send-keys -t '\${paneInfo}' Enter\`, { stdio: 'pipe' });
|
|
1496
1796
|
}
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1797
|
+
catch (error) {
|
|
1798
|
+
setStatusMessage(\`Failed to launch popup: \${error.message}\`);
|
|
1799
|
+
setTimeout(() => setStatusMessage(""), 3000);
|
|
1800
|
+
}
|
|
1801
|
+
};
|
|
1802
|
+
const launchLogsPopup = async () => {
|
|
1803
|
+
// Only launch popup if tmux supports it
|
|
1804
|
+
if (!popupsSupported) {
|
|
1805
|
+
setStatusMessage("Popups require tmux 3.2+");
|
|
1806
|
+
setTimeout(() => setStatusMessage(""), 3000);
|
|
1807
|
+
return;
|
|
1808
|
+
}
|
|
1809
|
+
try {
|
|
1810
|
+
// Resolve the popup script path
|
|
1811
|
+
const projectRootForPopup = __dirname.includes("/dist")
|
|
1812
|
+
? path.resolve(__dirname, "..") // If in dist/, go up one level
|
|
1813
|
+
: path.resolve(__dirname, ".."); // If in src/, go up one level
|
|
1814
|
+
const popupScriptPath = path.join(projectRootForPopup, "dist", "popups", "logsPopup.js");
|
|
1815
|
+
// Get logs from StateManager and write to temp file
|
|
1816
|
+
const stateManager = StateManager.getInstance();
|
|
1817
|
+
const allLogs = stateManager.getLogs();
|
|
1818
|
+
const stats = stateManager.getLogStats();
|
|
1819
|
+
const logsData = { logs: allLogs, stats };
|
|
1820
|
+
// Write data to temp file to avoid shell escaping issues with complex JSON
|
|
1821
|
+
const dataFile = \`/tmp/dmux-logs-\${Date.now()}.json\`;
|
|
1822
|
+
const dataJson = JSON.stringify(logsData);
|
|
1823
|
+
await fs.writeFile(dataFile, dataJson);
|
|
1824
|
+
// Launch the popup with large positioning
|
|
1825
|
+
// Get tmux client dimensions (not process.stdout which is just the sidebar)
|
|
1826
|
+
const tmuxDims = execSync('tmux display-message -p "#{client_width},#{client_height}"', { encoding: "utf-8" }).trim();
|
|
1827
|
+
const [termWidth, termHeight] = tmuxDims.split(",").map(Number);
|
|
1828
|
+
// Launch the popup non-blocking and track it
|
|
1829
|
+
const popupHandle = launchNodePopupNonBlocking(popupScriptPath, [dataFile], {
|
|
1830
|
+
...POPUP_POSITIONING.large(SIDEBAR_WIDTH, termWidth, termHeight),
|
|
1831
|
+
title: "🪵 dmux Logs",
|
|
1832
|
+
});
|
|
1833
|
+
// Wait for the popup to close
|
|
1834
|
+
const result = await popupHandle.resultPromise;
|
|
1835
|
+
// Clear active popup tracking
|
|
1836
|
+
// Clean up temp file
|
|
1837
|
+
try {
|
|
1838
|
+
await fs.unlink(dataFile);
|
|
1839
|
+
}
|
|
1840
|
+
catch (err) {
|
|
1841
|
+
// Ignore cleanup errors
|
|
1842
|
+
}
|
|
1843
|
+
// Popup closed - mark all logs as read
|
|
1844
|
+
if (result.success) {
|
|
1845
|
+
stateManager.markAllLogsAsRead();
|
|
1512
1846
|
}
|
|
1513
1847
|
}
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1848
|
+
catch (error) {
|
|
1849
|
+
setStatusMessage(\`Failed to launch popup: \${error.message}\`);
|
|
1850
|
+
setTimeout(() => setStatusMessage(""), 3000);
|
|
1851
|
+
}
|
|
1852
|
+
};
|
|
1853
|
+
const launchShortcutsPopup = async () => {
|
|
1854
|
+
// Only launch popup if tmux supports it
|
|
1855
|
+
if (!popupsSupported) {
|
|
1856
|
+
setStatusMessage("Popups require tmux 3.2+");
|
|
1857
|
+
setTimeout(() => setStatusMessage(""), 3000);
|
|
1858
|
+
return;
|
|
1859
|
+
}
|
|
1860
|
+
try {
|
|
1861
|
+
// Resolve the popup script path
|
|
1862
|
+
const projectRootForPopup = __dirname.includes("/dist")
|
|
1863
|
+
? path.resolve(__dirname, "..") // If in dist/, go up one level
|
|
1864
|
+
: path.resolve(__dirname, ".."); // If in src/, go up one level
|
|
1865
|
+
const popupScriptPath = path.join(projectRootForPopup, "dist", "popups", "shortcutsPopup.js");
|
|
1866
|
+
// Prepare data for shortcuts popup
|
|
1867
|
+
const shortcutsData = {
|
|
1868
|
+
hasSidebarLayout: !!controlPaneId,
|
|
1869
|
+
showRemoteKey: !!server,
|
|
1870
|
+
};
|
|
1871
|
+
// Write data to temp file
|
|
1872
|
+
const dataFile = \`/tmp/dmux-shortcuts-\${Date.now()}.json\`;
|
|
1873
|
+
const dataJson = JSON.stringify(shortcutsData);
|
|
1874
|
+
await fs.writeFile(dataFile, dataJson);
|
|
1875
|
+
// Launch the popup non-blocking and track it
|
|
1876
|
+
const popupHandle = launchNodePopupNonBlocking(popupScriptPath, [dataFile], {
|
|
1877
|
+
...POPUP_POSITIONING.standard(SIDEBAR_WIDTH),
|
|
1878
|
+
width: 50,
|
|
1879
|
+
height: 20,
|
|
1880
|
+
title: "⌨️ Keyboard Shortcuts",
|
|
1881
|
+
});
|
|
1882
|
+
// Wait for the popup to close
|
|
1883
|
+
const result = await popupHandle.resultPromise;
|
|
1884
|
+
// Clear active popup tracking
|
|
1885
|
+
// Clean up temp file
|
|
1886
|
+
try {
|
|
1887
|
+
await fs.unlink(dataFile);
|
|
1888
|
+
}
|
|
1889
|
+
catch (err) {
|
|
1890
|
+
// Ignore cleanup errors
|
|
1891
|
+
}
|
|
1892
|
+
}
|
|
1893
|
+
catch (error) {
|
|
1894
|
+
setStatusMessage(\`Failed to launch popup: \${error.message}\`);
|
|
1895
|
+
setTimeout(() => setStatusMessage(""), 3000);
|
|
1896
|
+
}
|
|
1897
|
+
};
|
|
1898
|
+
const launchRemotePopup = async () => {
|
|
1899
|
+
// Only launch popup if tmux supports it
|
|
1900
|
+
if (!popupsSupported) {
|
|
1901
|
+
setStatusMessage("Popups require tmux 3.2+");
|
|
1902
|
+
setTimeout(() => setStatusMessage(""), 3000);
|
|
1903
|
+
return;
|
|
1904
|
+
}
|
|
1905
|
+
// Check if server is available and tunnel URL exists
|
|
1906
|
+
if (!server || !serverPort || !tunnelUrl) {
|
|
1907
|
+
setStatusMessage("Tunnel not ready");
|
|
1908
|
+
setTimeout(() => setStatusMessage(""), 3000);
|
|
1909
|
+
return;
|
|
1910
|
+
}
|
|
1911
|
+
try {
|
|
1912
|
+
// Resolve the popup script path
|
|
1913
|
+
const projectRootForPopup = __dirname.includes("/dist")
|
|
1914
|
+
? path.resolve(__dirname, "..") // If in dist/, go up one level
|
|
1915
|
+
: path.resolve(__dirname, ".."); // If in src/, go up one level
|
|
1916
|
+
const popupScriptPath = path.join(projectRootForPopup, "dist", "popups", "remotePopup.js");
|
|
1917
|
+
// Prepare status file with existing tunnel URL
|
|
1918
|
+
const tunnelStatusFile = \`/tmp/dmux-tunnel-status-\${Date.now()}.json\`;
|
|
1919
|
+
await fs.writeFile(tunnelStatusFile, JSON.stringify({ url: tunnelUrl }));
|
|
1920
|
+
// Prepare data for remote popup
|
|
1921
|
+
const remoteData = {
|
|
1922
|
+
loading: false,
|
|
1923
|
+
serverPort: serverPort,
|
|
1924
|
+
statusFile: tunnelStatusFile,
|
|
1925
|
+
};
|
|
1926
|
+
// Write data to temp file
|
|
1927
|
+
const dataFile = \`/tmp/dmux-remote-\${Date.now()}.json\`;
|
|
1928
|
+
await fs.writeFile(dataFile, JSON.stringify(remoteData));
|
|
1929
|
+
// Launch the popup non-blocking and track it
|
|
1930
|
+
const popupHandle = launchNodePopupNonBlocking(popupScriptPath, [dataFile], {
|
|
1931
|
+
...POPUP_POSITIONING.centeredWithSidebar(SIDEBAR_WIDTH),
|
|
1932
|
+
width: 60,
|
|
1933
|
+
height: 30,
|
|
1934
|
+
title: "🌐 Remote Access",
|
|
1935
|
+
});
|
|
1936
|
+
// Wait for the popup to close
|
|
1937
|
+
const result = await popupHandle.resultPromise;
|
|
1938
|
+
// Clear active popup tracking
|
|
1939
|
+
// Show "Copied!" message if URL was copied
|
|
1940
|
+
// Note: result is the parsed JSON directly, not wrapped in PopupResult.data
|
|
1941
|
+
if (result && result.copied) {
|
|
1942
|
+
setTunnelCopied(true);
|
|
1943
|
+
setTimeout(() => setTunnelCopied(false), 2000);
|
|
1944
|
+
}
|
|
1945
|
+
// Clean up temp files
|
|
1946
|
+
try {
|
|
1947
|
+
await fs.unlink(dataFile);
|
|
1948
|
+
}
|
|
1949
|
+
catch (err) {
|
|
1950
|
+
// Ignore cleanup errors
|
|
1951
|
+
}
|
|
1952
|
+
try {
|
|
1953
|
+
await fs.unlink(tunnelStatusFile);
|
|
1954
|
+
}
|
|
1955
|
+
catch (err) {
|
|
1956
|
+
// Ignore cleanup errors (file might not exist yet)
|
|
1957
|
+
}
|
|
1958
|
+
}
|
|
1959
|
+
catch (error) {
|
|
1960
|
+
setStatusMessage(\`Failed to launch popup: \${error.message}\`);
|
|
1961
|
+
setTimeout(() => setStatusMessage(""), 3000);
|
|
1962
|
+
}
|
|
1963
|
+
};
|
|
1964
|
+
const launchSettingsPopup = async () => {
|
|
1965
|
+
// Only launch popup if tmux supports it
|
|
1966
|
+
if (!popupsSupported) {
|
|
1967
|
+
setStatusMessage("Popups require tmux 3.2+");
|
|
1968
|
+
setTimeout(() => setStatusMessage(""), 3000);
|
|
1969
|
+
return;
|
|
1970
|
+
}
|
|
1971
|
+
try {
|
|
1972
|
+
// Resolve the popup script path
|
|
1973
|
+
const projectRootForPopup = __dirname.includes("/dist")
|
|
1974
|
+
? path.resolve(__dirname, "..") // If in dist/, go up one level
|
|
1975
|
+
: path.resolve(__dirname, ".."); // If in src/, go up one level
|
|
1976
|
+
const popupScriptPath = path.join(projectRootForPopup, "dist", "popups", "settingsPopup.js");
|
|
1977
|
+
// Prepare settings data for popup
|
|
1978
|
+
const settingsData = {
|
|
1979
|
+
settingDefinitions: SETTING_DEFINITIONS,
|
|
1980
|
+
settings: settingsManager.getSettings(),
|
|
1981
|
+
globalSettings: settingsManager.getGlobalSettings(),
|
|
1982
|
+
projectSettings: settingsManager.getProjectSettings(),
|
|
1983
|
+
};
|
|
1984
|
+
const settingsJson = JSON.stringify(settingsData);
|
|
1985
|
+
// Launch the popup non-blocking and track it
|
|
1986
|
+
const popupHandle = launchNodePopupNonBlocking(popupScriptPath, [settingsJson], {
|
|
1987
|
+
...POPUP_POSITIONING.standard(SIDEBAR_WIDTH),
|
|
1988
|
+
width: 70,
|
|
1989
|
+
height: Math.min(25, SETTING_DEFINITIONS.length + 8),
|
|
1990
|
+
title: "⚙️ Settings",
|
|
1991
|
+
});
|
|
1992
|
+
// Wait for the popup to close
|
|
1993
|
+
const result = await popupHandle.resultPromise;
|
|
1994
|
+
// Clear active popup tracking
|
|
1995
|
+
if (result.success) {
|
|
1996
|
+
// Check if this is an action result (action field at top level)
|
|
1997
|
+
if (result.action) {
|
|
1998
|
+
// Action type setting (like 'hooks')
|
|
1999
|
+
if (result.action === "hooks") {
|
|
2000
|
+
// Launch hooks popup
|
|
2001
|
+
await launchHooksPopup();
|
|
2002
|
+
}
|
|
2003
|
+
}
|
|
2004
|
+
else if (result.data) {
|
|
2005
|
+
// Regular setting change (result.data contains the setting)
|
|
2006
|
+
const { key, value, scope } = result.data;
|
|
2007
|
+
settingsManager.updateSetting(key, value, scope);
|
|
2008
|
+
setStatusMessage(\`Setting saved (\${scope})\`);
|
|
2009
|
+
setTimeout(() => setStatusMessage(""), 2000);
|
|
2010
|
+
}
|
|
2011
|
+
}
|
|
2012
|
+
else if (result.cancelled) {
|
|
2013
|
+
// User pressed ESC - do nothing
|
|
2014
|
+
return;
|
|
2015
|
+
}
|
|
2016
|
+
else if (result.error) {
|
|
2017
|
+
setStatusMessage(\`Popup error: \${result.error}\`);
|
|
2018
|
+
setTimeout(() => setStatusMessage(""), 3000);
|
|
2019
|
+
}
|
|
2020
|
+
}
|
|
2021
|
+
catch (error) {
|
|
2022
|
+
setStatusMessage(\`Failed to launch popup: \${error.message}\`);
|
|
2023
|
+
setTimeout(() => setStatusMessage(""), 3000);
|
|
2024
|
+
}
|
|
2025
|
+
};
|
|
2026
|
+
const launchMergePopup = async (pane) => {
|
|
2027
|
+
LogService.getInstance().debug(\`launchMergePopup called for pane: \${pane.slug}\`, "MergePopup");
|
|
2028
|
+
// Only launch popup if tmux supports it
|
|
2029
|
+
if (!popupsSupported) {
|
|
2030
|
+
LogService.getInstance().warn("Popups not supported", "MergePopup");
|
|
2031
|
+
setStatusMessage("Popups require tmux 3.2+");
|
|
2032
|
+
setTimeout(() => setStatusMessage(""), 3000);
|
|
2033
|
+
return;
|
|
2034
|
+
}
|
|
2035
|
+
if (!pane.worktreePath) {
|
|
2036
|
+
LogService.getInstance().warn("No worktree path for pane", "MergePopup", pane.id);
|
|
2037
|
+
setStatusMessage("This pane has no worktree to merge");
|
|
2038
|
+
setTimeout(() => setStatusMessage(""), 3000);
|
|
2039
|
+
return;
|
|
2040
|
+
}
|
|
2041
|
+
try {
|
|
2042
|
+
// Resolve the popup script path
|
|
2043
|
+
const projectRootForPopup = __dirname.includes("/dist")
|
|
2044
|
+
? path.resolve(__dirname, "..") // If in dist/, go up one level
|
|
2045
|
+
: path.resolve(__dirname, ".."); // If in src/, go up one level
|
|
2046
|
+
const popupScriptPath = path.join(projectRootForPopup, "dist", "popups", "mergePopup.js");
|
|
2047
|
+
LogService.getInstance().debug(\`Popup script path: \${popupScriptPath}\`, "MergePopup");
|
|
2048
|
+
// Check if popup script exists
|
|
2049
|
+
try {
|
|
2050
|
+
await fs.access(popupScriptPath);
|
|
2051
|
+
LogService.getInstance().debug("Popup script exists", "MergePopup");
|
|
2052
|
+
}
|
|
2053
|
+
catch {
|
|
2054
|
+
LogService.getInstance().error(\`Popup script NOT found at: \${popupScriptPath}\`, "MergePopup");
|
|
2055
|
+
setStatusMessage(\`Merge popup script not found: \${popupScriptPath}\`);
|
|
2056
|
+
setTimeout(() => setStatusMessage(""), 5000);
|
|
2057
|
+
return;
|
|
2058
|
+
}
|
|
2059
|
+
// Prepare merge data
|
|
2060
|
+
const mainRepoPath = pane.worktreePath.replace(/\\/\\.dmux\\/worktrees\\/[^/]+$/, "");
|
|
2061
|
+
const mergeData = {
|
|
2062
|
+
paneSlug: pane.slug,
|
|
2063
|
+
worktreePath: pane.worktreePath,
|
|
2064
|
+
mainRepoPath,
|
|
2065
|
+
mainBranch: getMainBranch(),
|
|
2066
|
+
};
|
|
2067
|
+
// Write data to temp file
|
|
2068
|
+
const dataFile = \`/tmp/dmux-merge-\${Date.now()}.json\`;
|
|
2069
|
+
await fs.writeFile(dataFile, JSON.stringify(mergeData));
|
|
2070
|
+
LogService.getInstance().debug(\`Merge data written to: \${dataFile}\`, "MergePopup");
|
|
2071
|
+
LogService.getInstance().debug(\`Merge data: \${JSON.stringify(mergeData)}\`, "MergePopup");
|
|
2072
|
+
// Launch the popup non-blocking and track it
|
|
2073
|
+
LogService.getInstance().debug("Launching merge popup...", "MergePopup");
|
|
2074
|
+
const popupHandle = launchNodePopupNonBlocking(popupScriptPath, [dataFile], {
|
|
2075
|
+
...POPUP_POSITIONING.large(SIDEBAR_WIDTH, terminalWidth, terminalHeight),
|
|
2076
|
+
width: 80,
|
|
2077
|
+
height: 30,
|
|
2078
|
+
title: \`🔀 Merge: \${pane.slug}\`,
|
|
2079
|
+
});
|
|
2080
|
+
LogService.getInstance().debug(\`Popup launched, PID: \${popupHandle.pid}\`, "MergePopup");
|
|
2081
|
+
// Wait for the popup to close
|
|
2082
|
+
const result = await popupHandle.resultPromise;
|
|
2083
|
+
// Clear active popup tracking
|
|
2084
|
+
// Clean up temp file
|
|
2085
|
+
try {
|
|
2086
|
+
await fs.unlink(dataFile);
|
|
2087
|
+
}
|
|
2088
|
+
catch { }
|
|
2089
|
+
if (result.success && result.data?.merged) {
|
|
2090
|
+
if (result.data.closedPane) {
|
|
2091
|
+
// Pane was closed during merge, refresh pane list
|
|
2092
|
+
await loadPanes();
|
|
2093
|
+
setStatusMessage("Merge complete, pane closed");
|
|
2094
|
+
}
|
|
2095
|
+
else {
|
|
2096
|
+
setStatusMessage("Merge complete");
|
|
2097
|
+
}
|
|
2098
|
+
setTimeout(() => setStatusMessage(""), 3000);
|
|
2099
|
+
}
|
|
2100
|
+
else if (result.cancelled) {
|
|
2101
|
+
// User cancelled
|
|
2102
|
+
return;
|
|
2103
|
+
}
|
|
2104
|
+
else if (result.error) {
|
|
2105
|
+
setStatusMessage(\`Merge error: \${result.error}\`);
|
|
2106
|
+
setTimeout(() => setStatusMessage(""), 3000);
|
|
2107
|
+
}
|
|
2108
|
+
}
|
|
2109
|
+
catch (error) {
|
|
2110
|
+
setStatusMessage(\`Failed to launch merge popup: \${error.message}\`);
|
|
2111
|
+
setTimeout(() => setStatusMessage(""), 3000);
|
|
2112
|
+
}
|
|
2113
|
+
};
|
|
2114
|
+
const launchChoicePopup = async (title, message, options) => {
|
|
2115
|
+
// Only launch popup if tmux supports it
|
|
2116
|
+
if (!popupsSupported) {
|
|
2117
|
+
setStatusMessage("Popups require tmux 3.2+");
|
|
2118
|
+
setTimeout(() => setStatusMessage(""), 3000);
|
|
2119
|
+
return null;
|
|
2120
|
+
}
|
|
2121
|
+
try {
|
|
2122
|
+
// Resolve the popup script path
|
|
2123
|
+
const projectRootForPopup = __dirname.includes("/dist")
|
|
2124
|
+
? path.resolve(__dirname, "..") // If in dist/, go up one level
|
|
2125
|
+
: path.resolve(__dirname, ".."); // If in src/, go up one level
|
|
2126
|
+
const popupScriptPath = path.join(projectRootForPopup, "dist", "popups", "choicePopup.js");
|
|
2127
|
+
// Write data to temp file to avoid shell escaping issues
|
|
2128
|
+
const dataFile = \`/tmp/dmux-choice-\${Date.now()}.json\`;
|
|
2129
|
+
const dataJson = JSON.stringify({ title, message, options });
|
|
2130
|
+
await fs.writeFile(dataFile, dataJson);
|
|
2131
|
+
// Launch the popup non-blocking and track it
|
|
2132
|
+
const popupHandle = launchNodePopupNonBlocking(popupScriptPath, [dataFile], {
|
|
2133
|
+
...POPUP_POSITIONING.standard(SIDEBAR_WIDTH),
|
|
2134
|
+
width: 70,
|
|
2135
|
+
height: Math.min(25, options.length * 3 + 8),
|
|
2136
|
+
title: title || "Choose Option",
|
|
2137
|
+
});
|
|
2138
|
+
// Wait for the popup to close
|
|
2139
|
+
const result = await popupHandle.resultPromise;
|
|
2140
|
+
// Clear active popup tracking
|
|
2141
|
+
// Clean up temp file
|
|
2142
|
+
try {
|
|
2143
|
+
await fs.unlink(dataFile);
|
|
2144
|
+
}
|
|
2145
|
+
catch { }
|
|
2146
|
+
if (result.success && result.data) {
|
|
2147
|
+
return result.data;
|
|
2148
|
+
}
|
|
2149
|
+
else if (result.cancelled) {
|
|
2150
|
+
return null;
|
|
2151
|
+
}
|
|
2152
|
+
else if (result.error) {
|
|
2153
|
+
setStatusMessage(\`Popup error: \${result.error}\`);
|
|
2154
|
+
setTimeout(() => setStatusMessage(""), 3000);
|
|
2155
|
+
return null;
|
|
2156
|
+
}
|
|
2157
|
+
}
|
|
2158
|
+
catch (error) {
|
|
2159
|
+
setStatusMessage(\`Failed to launch popup: \${error.message}\`);
|
|
2160
|
+
setTimeout(() => setStatusMessage(""), 3000);
|
|
2161
|
+
}
|
|
2162
|
+
return null;
|
|
2163
|
+
};
|
|
2164
|
+
const launchInputPopup = async (title, message, placeholder, defaultValue) => {
|
|
2165
|
+
// Only launch popup if tmux supports it
|
|
2166
|
+
if (!popupsSupported) {
|
|
2167
|
+
setStatusMessage("Popups require tmux 3.2+");
|
|
2168
|
+
setTimeout(() => setStatusMessage(""), 3000);
|
|
2169
|
+
return null;
|
|
2170
|
+
}
|
|
2171
|
+
try {
|
|
2172
|
+
// Resolve the popup script path
|
|
2173
|
+
const projectRootForPopup = __dirname.includes("/dist")
|
|
2174
|
+
? path.resolve(__dirname, "..") // If in dist/, go up one level
|
|
2175
|
+
: path.resolve(__dirname, ".."); // If in src/, go up one level
|
|
2176
|
+
const popupScriptPath = path.join(projectRootForPopup, "dist", "popups", "inputPopup.js");
|
|
2177
|
+
// Write data to temp file to avoid shell escaping issues
|
|
2178
|
+
const dataFile = \`/tmp/dmux-input-\${Date.now()}.json\`;
|
|
2179
|
+
const dataJson = JSON.stringify({
|
|
2180
|
+
title,
|
|
2181
|
+
message,
|
|
2182
|
+
placeholder,
|
|
2183
|
+
defaultValue,
|
|
2184
|
+
});
|
|
2185
|
+
await fs.writeFile(dataFile, dataJson);
|
|
2186
|
+
// Launch the popup non-blocking and track it
|
|
2187
|
+
const popupHandle = launchNodePopupNonBlocking(popupScriptPath, [dataFile], {
|
|
2188
|
+
...POPUP_POSITIONING.standard(SIDEBAR_WIDTH),
|
|
2189
|
+
width: 70,
|
|
2190
|
+
height: 15,
|
|
2191
|
+
title: title || "Input",
|
|
2192
|
+
});
|
|
2193
|
+
// Wait for the popup to close
|
|
2194
|
+
const result = await popupHandle.resultPromise;
|
|
2195
|
+
// Clear active popup tracking
|
|
2196
|
+
// Clean up temp file
|
|
2197
|
+
try {
|
|
2198
|
+
await fs.unlink(dataFile);
|
|
2199
|
+
}
|
|
2200
|
+
catch { }
|
|
2201
|
+
if (result.success && result.data !== undefined) {
|
|
2202
|
+
return result.data;
|
|
2203
|
+
}
|
|
2204
|
+
else if (result.cancelled) {
|
|
2205
|
+
return null;
|
|
2206
|
+
}
|
|
2207
|
+
else if (result.error) {
|
|
2208
|
+
setStatusMessage(\`Popup error: \${result.error}\`);
|
|
2209
|
+
setTimeout(() => setStatusMessage(""), 3000);
|
|
2210
|
+
return null;
|
|
2211
|
+
}
|
|
2212
|
+
}
|
|
2213
|
+
catch (error) {
|
|
2214
|
+
setStatusMessage(\`Failed to launch popup: \${error.message}\`);
|
|
2215
|
+
setTimeout(() => setStatusMessage(""), 3000);
|
|
2216
|
+
}
|
|
2217
|
+
return null;
|
|
2218
|
+
};
|
|
2219
|
+
const launchProgressPopup = async (message, type = "info", timeout = 2000) => {
|
|
2220
|
+
// Only launch popup if tmux supports it
|
|
2221
|
+
if (!popupsSupported) {
|
|
2222
|
+
setStatusMessage(message);
|
|
2223
|
+
setTimeout(() => setStatusMessage(""), timeout);
|
|
2224
|
+
return;
|
|
2225
|
+
}
|
|
2226
|
+
try {
|
|
2227
|
+
// Resolve the popup script path
|
|
2228
|
+
const projectRootForPopup = __dirname.includes("/dist")
|
|
2229
|
+
? path.resolve(__dirname, "..") // If in dist/, go up one level
|
|
2230
|
+
: path.resolve(__dirname, ".."); // If in src/, go up one level
|
|
2231
|
+
const popupScriptPath = path.join(projectRootForPopup, "dist", "popups", "progressPopup.js");
|
|
2232
|
+
// Write data to temp file to avoid shell escaping issues
|
|
2233
|
+
const dataFile = \`/tmp/dmux-progress-\${Date.now()}.json\`;
|
|
2234
|
+
const dataJson = JSON.stringify({ message, type, timeout });
|
|
2235
|
+
await fs.writeFile(dataFile, dataJson);
|
|
2236
|
+
// Launch the popup - position at top, 1 char right of sidebar
|
|
2237
|
+
// Height depends on message length
|
|
2238
|
+
const lines = Math.ceil(message.length / 60) + 3; // Estimate lines needed
|
|
2239
|
+
const titleText = type === "success"
|
|
2240
|
+
? "✓ Success"
|
|
2241
|
+
: type === "error"
|
|
2242
|
+
? "✗ Error"
|
|
2243
|
+
: type === "info"
|
|
2244
|
+
? "ℹ Info"
|
|
2245
|
+
: "Progress";
|
|
2246
|
+
// Launch the popup non-blocking and track it
|
|
2247
|
+
const popupHandle = launchNodePopupNonBlocking(popupScriptPath, [dataFile], {
|
|
2248
|
+
...POPUP_POSITIONING.standard(SIDEBAR_WIDTH),
|
|
2249
|
+
width: 70,
|
|
2250
|
+
height: Math.min(15, lines + 4),
|
|
2251
|
+
title: titleText,
|
|
2252
|
+
});
|
|
2253
|
+
// Wait for the popup to close
|
|
2254
|
+
await popupHandle.resultPromise;
|
|
2255
|
+
// Clear active popup tracking
|
|
2256
|
+
// Clean up temp file
|
|
2257
|
+
try {
|
|
2258
|
+
await fs.unlink(dataFile);
|
|
2259
|
+
}
|
|
2260
|
+
catch { }
|
|
2261
|
+
}
|
|
2262
|
+
catch (error) {
|
|
2263
|
+
// Fallback to inline message
|
|
2264
|
+
setStatusMessage(message);
|
|
2265
|
+
setTimeout(() => setStatusMessage(""), timeout);
|
|
2266
|
+
}
|
|
2267
|
+
};
|
|
2268
|
+
// Action system - initialized after popup launchers are defined
|
|
2269
|
+
const actionSystem = useActionSystem({
|
|
2270
|
+
panes,
|
|
2271
|
+
savePanes,
|
|
2272
|
+
sessionName,
|
|
2273
|
+
projectName,
|
|
2274
|
+
onPaneRemove: (paneId) => {
|
|
2275
|
+
// Mark the pane as intentionally closed to prevent race condition with worker
|
|
2276
|
+
intentionallyClosedPanes.current.add(paneId);
|
|
2277
|
+
// Remove from panes list
|
|
2278
|
+
const updatedPanes = panes.filter((p) => p.paneId !== paneId);
|
|
2279
|
+
savePanes(updatedPanes);
|
|
2280
|
+
// Clean up after a delay
|
|
2281
|
+
setTimeout(() => {
|
|
2282
|
+
intentionallyClosedPanes.current.delete(paneId);
|
|
2283
|
+
}, 5000);
|
|
2284
|
+
},
|
|
2285
|
+
forceRepaint,
|
|
2286
|
+
popupLaunchers: popupsSupported
|
|
2287
|
+
? {
|
|
2288
|
+
launchConfirmPopup,
|
|
2289
|
+
launchChoicePopup,
|
|
2290
|
+
launchInputPopup,
|
|
2291
|
+
launchProgressPopup,
|
|
2292
|
+
}
|
|
2293
|
+
: undefined,
|
|
2294
|
+
});
|
|
2295
|
+
// Auto-show new pane dialog removed - users can press 'n' to create panes via popup
|
|
2296
|
+
// Periodic enforcement of control pane size and content pane rebalancing (left sidebar at 40 chars)
|
|
2297
|
+
useEffect(() => {
|
|
2298
|
+
if (!controlPaneId) {
|
|
2299
|
+
return; // No sidebar layout configured
|
|
2300
|
+
}
|
|
2301
|
+
// Enforce sidebar width immediately on mount
|
|
2302
|
+
enforceControlPaneSize(controlPaneId, SIDEBAR_WIDTH);
|
|
2303
|
+
// Debounce resize handler to prevent infinite loops
|
|
2304
|
+
let resizeTimeout = null;
|
|
2305
|
+
let isApplyingLayout = false;
|
|
2306
|
+
const handleResize = () => {
|
|
2307
|
+
// Skip if we're already applying a layout (prevents loops)
|
|
2308
|
+
if (isApplyingLayout) {
|
|
2309
|
+
return;
|
|
2310
|
+
}
|
|
2311
|
+
// Clear any pending resize
|
|
2312
|
+
if (resizeTimeout) {
|
|
2313
|
+
clearTimeout(resizeTimeout);
|
|
2314
|
+
}
|
|
2315
|
+
// Debounce: wait 500ms after last resize event (prevents excessive recalculations)
|
|
2316
|
+
resizeTimeout = setTimeout(() => {
|
|
2317
|
+
// Only enforce if not showing dialogs (to avoid interference)
|
|
2318
|
+
const hasActiveDialog = actionSystem.actionState.showConfirmDialog ||
|
|
2319
|
+
actionSystem.actionState.showChoiceDialog ||
|
|
2320
|
+
actionSystem.actionState.showInputDialog ||
|
|
2321
|
+
actionSystem.actionState.showProgressDialog ||
|
|
2322
|
+
!!showCommandPrompt ||
|
|
2323
|
+
showFileCopyPrompt ||
|
|
2324
|
+
isCreatingPane ||
|
|
2325
|
+
runningCommand ||
|
|
2326
|
+
isUpdating;
|
|
2327
|
+
if (!hasActiveDialog) {
|
|
2328
|
+
isApplyingLayout = true;
|
|
2329
|
+
// Only enforce sidebar width when terminal resizes
|
|
2330
|
+
enforceControlPaneSize(controlPaneId, SIDEBAR_WIDTH);
|
|
2331
|
+
// Force Ink to repaint after resize to prevent blank dmux pane
|
|
2332
|
+
forceRepaint();
|
|
2333
|
+
// Reset flag after a brief delay
|
|
2334
|
+
setTimeout(() => {
|
|
2335
|
+
isApplyingLayout = false;
|
|
2336
|
+
}, 100);
|
|
2337
|
+
}
|
|
2338
|
+
}, 500);
|
|
2339
|
+
};
|
|
2340
|
+
// Listen to stdout resize events
|
|
2341
|
+
process.stdout.on("resize", handleResize);
|
|
2342
|
+
// Also listen for SIGWINCH and SIGUSR1 (tmux hook sends USR1)
|
|
2343
|
+
process.on("SIGWINCH", handleResize);
|
|
2344
|
+
process.on("SIGUSR1", handleResize);
|
|
2345
|
+
return () => {
|
|
2346
|
+
process.stdout.off("resize", handleResize);
|
|
2347
|
+
process.off("SIGWINCH", handleResize);
|
|
2348
|
+
process.off("SIGUSR1", handleResize);
|
|
2349
|
+
if (resizeTimeout) {
|
|
2350
|
+
clearTimeout(resizeTimeout);
|
|
2351
|
+
}
|
|
2352
|
+
};
|
|
2353
|
+
}, [
|
|
2354
|
+
controlPaneId,
|
|
2355
|
+
actionSystem.actionState.showConfirmDialog,
|
|
2356
|
+
actionSystem.actionState.showChoiceDialog,
|
|
2357
|
+
actionSystem.actionState.showInputDialog,
|
|
2358
|
+
actionSystem.actionState.showProgressDialog,
|
|
2359
|
+
showCommandPrompt,
|
|
2360
|
+
showFileCopyPrompt,
|
|
2361
|
+
isCreatingPane,
|
|
2362
|
+
runningCommand,
|
|
2363
|
+
isUpdating,
|
|
2364
|
+
]);
|
|
2365
|
+
// Monitor agent status across panes (returns a map of pane ID to status)
|
|
2366
|
+
const agentStatuses = useAgentStatus({
|
|
2367
|
+
panes,
|
|
2368
|
+
suspend: actionSystem.actionState.showConfirmDialog ||
|
|
2369
|
+
actionSystem.actionState.showChoiceDialog ||
|
|
2370
|
+
actionSystem.actionState.showInputDialog ||
|
|
2371
|
+
actionSystem.actionState.showProgressDialog ||
|
|
2372
|
+
!!showCommandPrompt ||
|
|
2373
|
+
showFileCopyPrompt,
|
|
2374
|
+
onPaneRemoved: (paneId) => {
|
|
2375
|
+
// Check if this pane was intentionally closed
|
|
2376
|
+
// If so, don't re-save - the close action already handled it
|
|
2377
|
+
if (intentionallyClosedPanes.current.has(paneId)) {
|
|
2378
|
+
return;
|
|
2379
|
+
}
|
|
2380
|
+
// Pane was removed unexpectedly (e.g., user killed tmux pane manually)
|
|
2381
|
+
// Remove it from our tracking
|
|
2382
|
+
const updatedPanes = panes.filter((p) => p.id !== paneId);
|
|
2383
|
+
savePanes(updatedPanes);
|
|
2384
|
+
},
|
|
2385
|
+
});
|
|
2386
|
+
const createNewPane = async (prompt, agent) => {
|
|
2387
|
+
setIsCreatingPane(true);
|
|
2388
|
+
setStatusMessage("Generating slug...");
|
|
2389
|
+
const slug = await generateSlug(prompt);
|
|
2390
|
+
setStatusMessage(\`Creating worktree: \${slug}...\`);
|
|
2391
|
+
// Get git root directory for consistent worktree placement
|
|
2392
|
+
let projectRoot;
|
|
2393
|
+
try {
|
|
2394
|
+
projectRoot = execSync("git rev-parse --show-toplevel", {
|
|
2395
|
+
encoding: "utf-8",
|
|
2396
|
+
stdio: "pipe",
|
|
2397
|
+
}).trim();
|
|
2398
|
+
}
|
|
2399
|
+
catch {
|
|
2400
|
+
// Fallback to current directory if not in a git repo
|
|
2401
|
+
projectRoot = process.cwd();
|
|
2402
|
+
}
|
|
2403
|
+
// Create worktree path inside .dmux/worktrees directory
|
|
2404
|
+
const worktreePath = path.join(projectRoot, ".dmux", "worktrees", slug);
|
|
2405
|
+
// Get the original pane ID (where dmux is running) before clearing
|
|
2406
|
+
const originalPaneId = execSync('tmux display-message -p "#{pane_id}"', {
|
|
2407
|
+
encoding: "utf-8",
|
|
2408
|
+
}).trim();
|
|
2409
|
+
// Minimal clearing to avoid layout shifts
|
|
2410
|
+
process.stdout.write("\\x1b[2J\\x1b[H");
|
|
2411
|
+
// Get current pane count to determine layout
|
|
2412
|
+
const paneCount = parseInt(execSync("tmux list-panes | wc -l", { encoding: "utf-8" }).trim());
|
|
2413
|
+
// Enable pane borders to show titles
|
|
2414
|
+
try {
|
|
2415
|
+
execSync(\`tmux set-option -g pane-border-status top\`, { stdio: "pipe" });
|
|
2416
|
+
}
|
|
2417
|
+
catch {
|
|
2418
|
+
// Ignore if already set or fails
|
|
2419
|
+
}
|
|
2420
|
+
// Create new pane
|
|
2421
|
+
const paneInfo = execSync(\`tmux split-window -h -P -F '#{pane_id}'\`, {
|
|
2422
|
+
encoding: "utf-8",
|
|
2423
|
+
}).trim();
|
|
2424
|
+
// Wait for pane creation to settle
|
|
2425
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
2426
|
+
// Set pane title to match the slug
|
|
2427
|
+
try {
|
|
2428
|
+
execSync(\`tmux select-pane -t '\${paneInfo}' -T "\${slug}"\`, {
|
|
2429
|
+
stdio: "pipe",
|
|
2430
|
+
});
|
|
2431
|
+
}
|
|
2432
|
+
catch {
|
|
2433
|
+
// Ignore if setting title fails
|
|
2434
|
+
}
|
|
2435
|
+
// Don't apply global layouts - let content panes arrange naturally
|
|
2436
|
+
// Only enforce sidebar width
|
|
2437
|
+
try {
|
|
2438
|
+
const controlPaneId = execSync('tmux display-message -p "#{pane_id}"', {
|
|
2439
|
+
encoding: "utf-8",
|
|
2440
|
+
}).trim();
|
|
2441
|
+
enforceControlPaneSize(controlPaneId, SIDEBAR_WIDTH);
|
|
2442
|
+
}
|
|
2443
|
+
catch { }
|
|
2444
|
+
// Create git worktree and cd into it
|
|
2445
|
+
// This MUST happen before launching Claude to ensure we're in the right directory
|
|
2446
|
+
try {
|
|
2447
|
+
// First, create the worktree and cd into it as a single command
|
|
2448
|
+
// Use ; instead of && to ensure cd runs even if worktree already exists
|
|
2449
|
+
const worktreeCmd = \`git worktree add "\${worktreePath}" -b \${slug} 2>/dev/null ; cd "\${worktreePath}"\`;
|
|
2450
|
+
execSync(\`tmux send-keys -t '\${paneInfo}' '\${worktreeCmd}' Enter\`, {
|
|
2451
|
+
stdio: "pipe",
|
|
2452
|
+
});
|
|
2453
|
+
// Wait longer for worktree creation and cd to complete
|
|
2454
|
+
// This is critical - if we don't wait long enough, Claude will start in the wrong directory
|
|
2455
|
+
await new Promise((resolve) => setTimeout(resolve, 2500));
|
|
2456
|
+
// Verify we're in the worktree directory by sending pwd command
|
|
2457
|
+
execSync(\`tmux send-keys -t '\${paneInfo}' 'echo "Worktree created at:" && pwd' Enter\`, { stdio: "pipe" });
|
|
2458
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
2459
|
+
setStatusMessage(agent
|
|
2460
|
+
? \`Worktree created, launching \${agent === "opencode" ? "opencode" : "Claude"}...\`
|
|
2461
|
+
: "Worktree created.");
|
|
2462
|
+
}
|
|
2463
|
+
catch (error) {
|
|
2464
|
+
// Log error but continue - worktree creation is essential
|
|
2465
|
+
setStatusMessage(\`Warning: Worktree issue: \${error}\`);
|
|
2466
|
+
// Even if worktree creation failed, try to cd to the directory in case it exists
|
|
2467
|
+
execSync(\`tmux send-keys -t '\${paneInfo}' 'cd "\${worktreePath}" 2>/dev/null || (echo "ERROR: Failed to create/enter worktree \${slug}" && pwd)' Enter\`, { stdio: "pipe" });
|
|
2468
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
2469
|
+
}
|
|
2470
|
+
// Prepare and send the agent command
|
|
2471
|
+
let escapedCmd = "";
|
|
2472
|
+
if (agent === "claude") {
|
|
2473
|
+
// Claude should always be launched AFTER we're in the worktree directory
|
|
2474
|
+
let claudeCmd;
|
|
2475
|
+
if (prompt && prompt.trim()) {
|
|
2476
|
+
const escapedPrompt = prompt
|
|
2477
|
+
.replace(/\\\\/g, "\\\\\\\\")
|
|
2478
|
+
.replace(/"/g, '\\\\"')
|
|
2479
|
+
.replace(/\`/g, "\\\\\`")
|
|
2480
|
+
.replace(/\\$/g, "\\\\$");
|
|
2481
|
+
claudeCmd = \`claude "\${escapedPrompt}" --permission-mode=acceptEdits\`;
|
|
2482
|
+
}
|
|
2483
|
+
else {
|
|
2484
|
+
claudeCmd = \`claude --permission-mode=acceptEdits\`;
|
|
2485
|
+
}
|
|
2486
|
+
// Send Claude command to new pane
|
|
2487
|
+
escapedCmd = claudeCmd.replace(/'/g, "'\\\\''");
|
|
2488
|
+
execSync(\`tmux send-keys -t '\${paneInfo}' '\${escapedCmd}'\`, {
|
|
2489
|
+
stdio: "pipe",
|
|
2490
|
+
});
|
|
2491
|
+
execSync(\`tmux send-keys -t '\${paneInfo}' Enter\`, { stdio: "pipe" });
|
|
2492
|
+
}
|
|
2493
|
+
else if (agent === "opencode") {
|
|
2494
|
+
// opencode: start the TUI, then paste the prompt and submit
|
|
2495
|
+
const openCoderCmd = \`opencode\`;
|
|
2496
|
+
const escapedOpenCmd = openCoderCmd.replace(/'/g, "'\\\\''");
|
|
2497
|
+
execSync(\`tmux send-keys -t '\${paneInfo}' '\${escapedOpenCmd}'\`, {
|
|
2498
|
+
stdio: "pipe",
|
|
2499
|
+
});
|
|
2500
|
+
execSync(\`tmux send-keys -t '\${paneInfo}' Enter\`, { stdio: "pipe" });
|
|
2501
|
+
if (prompt && prompt.trim()) {
|
|
2502
|
+
await new Promise((resolve) => setTimeout(resolve, 1500));
|
|
2503
|
+
const bufName = \`dmux_prompt_\${Date.now()}\`;
|
|
2504
|
+
const promptEsc = prompt.replace(/\\\\/g, "\\\\\\\\").replace(/'/g, "'\\\\''");
|
|
2505
|
+
execSync(\`tmux set-buffer -b '\${bufName}' -- '\${promptEsc}'\`, {
|
|
2506
|
+
stdio: "pipe",
|
|
2507
|
+
});
|
|
2508
|
+
execSync(\`tmux paste-buffer -b '\${bufName}' -t '\${paneInfo}'\`, {
|
|
2509
|
+
stdio: "pipe",
|
|
2510
|
+
});
|
|
2511
|
+
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
2512
|
+
execSync(\`tmux delete-buffer -b '\${bufName}'\`, { stdio: "pipe" });
|
|
2513
|
+
execSync(\`tmux send-keys -t '\${paneInfo}' Enter\`, { stdio: "pipe" });
|
|
2514
|
+
}
|
|
2515
|
+
}
|
|
2516
|
+
if (agent === "claude") {
|
|
2517
|
+
// Monitor for Claude Code trust prompt and auto-respond
|
|
2518
|
+
const autoApproveTrust = async () => {
|
|
2519
|
+
// Wait for Claude to start up before checking for prompts
|
|
2520
|
+
await new Promise((resolve) => setTimeout(resolve, 800));
|
|
2521
|
+
const maxChecks = 100; // 100 checks * 100ms = 10 seconds total
|
|
2522
|
+
const checkInterval = 100; // Check every 100ms
|
|
2523
|
+
let lastContent = "";
|
|
2524
|
+
let stableContentCount = 0;
|
|
1523
2525
|
let promptHandled = false;
|
|
1524
2526
|
// More comprehensive trust prompt patterns
|
|
1525
2527
|
const trustPromptPatterns = [
|
|
@@ -1547,14 +2549,15 @@ const DmuxApp = ({ panesFile, projectName, sessionName, settingsFile, projectRoo
|
|
|
1547
2549
|
/❯\\s*1\\.\\s*Yes,\\s*proceed/i, // New Claude numbered menu format
|
|
1548
2550
|
/Enter to confirm.*Esc to exit/i, // New Claude confirmation format
|
|
1549
2551
|
/1\\.\\s*Yes,\\s*proceed/i, // Yes proceed option
|
|
1550
|
-
/2\\.\\s*No,\\s*exit/i // No exit option
|
|
2552
|
+
/2\\.\\s*No,\\s*exit/i, // No exit option
|
|
1551
2553
|
];
|
|
1552
2554
|
for (let i = 0; i < maxChecks; i++) {
|
|
1553
|
-
await new Promise(resolve => setTimeout(resolve, checkInterval));
|
|
2555
|
+
await new Promise((resolve) => setTimeout(resolve, checkInterval));
|
|
1554
2556
|
try {
|
|
1555
2557
|
// Capture the pane content
|
|
1556
2558
|
const paneContent = capturePaneContent(paneInfo, 30);
|
|
1557
|
-
if (i % 10 === 0) {
|
|
2559
|
+
if (i % 10 === 0) {
|
|
2560
|
+
// Log every 10 checks (every second)
|
|
1558
2561
|
}
|
|
1559
2562
|
// Check if content has stabilized (same for 3 checks = prompt is waiting)
|
|
1560
2563
|
if (paneContent === lastContent) {
|
|
@@ -1565,14 +2568,15 @@ const DmuxApp = ({ panesFile, projectName, sessionName, settingsFile, projectRoo
|
|
|
1565
2568
|
lastContent = paneContent;
|
|
1566
2569
|
}
|
|
1567
2570
|
// Look for trust prompt in the current content
|
|
1568
|
-
const hasTrustPrompt = trustPromptPatterns.some(pattern => pattern.test(paneContent));
|
|
2571
|
+
const hasTrustPrompt = trustPromptPatterns.some((pattern) => pattern.test(paneContent));
|
|
1569
2572
|
// Also check if we see specific Claude permission text
|
|
1570
|
-
const hasClaudePermissionPrompt = paneContent.includes(
|
|
1571
|
-
paneContent.includes(
|
|
1572
|
-
paneContent.includes(
|
|
1573
|
-
paneContent.includes(
|
|
1574
|
-
(paneContent.includes(
|
|
1575
|
-
if ((hasTrustPrompt || hasClaudePermissionPrompt) &&
|
|
2573
|
+
const hasClaudePermissionPrompt = paneContent.includes("Do you trust") ||
|
|
2574
|
+
paneContent.includes("trust the files") ||
|
|
2575
|
+
paneContent.includes("permission") ||
|
|
2576
|
+
paneContent.includes("allow") ||
|
|
2577
|
+
(paneContent.includes("folder") && paneContent.includes("?"));
|
|
2578
|
+
if ((hasTrustPrompt || hasClaudePermissionPrompt) &&
|
|
2579
|
+
!promptHandled) {
|
|
1576
2580
|
// Content is stable and we found a prompt
|
|
1577
2581
|
if (stableContentCount >= 2) {
|
|
1578
2582
|
// Check if this is the new Claude numbered menu format
|
|
@@ -1580,44 +2584,57 @@ const DmuxApp = ({ panesFile, projectName, sessionName, settingsFile, projectRoo
|
|
|
1580
2584
|
/Enter to confirm.*Esc to exit/i.test(paneContent);
|
|
1581
2585
|
if (isNewClaudeFormat) {
|
|
1582
2586
|
// For new Claude format, just press Enter to confirm default "Yes, proceed"
|
|
1583
|
-
execSync(\`tmux send-keys -t '\${paneInfo}' Enter\`, {
|
|
2587
|
+
execSync(\`tmux send-keys -t '\${paneInfo}' Enter\`, {
|
|
2588
|
+
stdio: "pipe",
|
|
2589
|
+
});
|
|
1584
2590
|
}
|
|
1585
2591
|
else {
|
|
1586
2592
|
// Try multiple response methods for older formats
|
|
1587
2593
|
// Method 1: Send 'y' followed by Enter (most explicit)
|
|
1588
|
-
execSync(\`tmux send-keys -t '\${paneInfo}' 'y'\`, {
|
|
1589
|
-
|
|
1590
|
-
|
|
2594
|
+
execSync(\`tmux send-keys -t '\${paneInfo}' 'y'\`, {
|
|
2595
|
+
stdio: "pipe",
|
|
2596
|
+
});
|
|
2597
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
2598
|
+
execSync(\`tmux send-keys -t '\${paneInfo}' Enter\`, {
|
|
2599
|
+
stdio: "pipe",
|
|
2600
|
+
});
|
|
1591
2601
|
// Method 2: Just Enter (if it's a yes/no with default yes)
|
|
1592
|
-
await new Promise(resolve => setTimeout(resolve, 100));
|
|
1593
|
-
execSync(\`tmux send-keys -t '\${paneInfo}' Enter\`, {
|
|
2602
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
2603
|
+
execSync(\`tmux send-keys -t '\${paneInfo}' Enter\`, {
|
|
2604
|
+
stdio: "pipe",
|
|
2605
|
+
});
|
|
1594
2606
|
}
|
|
1595
2607
|
// Mark as handled to avoid duplicate responses
|
|
1596
2608
|
promptHandled = true;
|
|
1597
2609
|
// Wait and check if prompt is gone
|
|
1598
|
-
await new Promise(resolve => setTimeout(resolve, 500));
|
|
2610
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
1599
2611
|
// Verify the prompt is gone
|
|
1600
2612
|
const updatedContent = capturePaneContent(paneInfo, 10);
|
|
1601
2613
|
// If trust prompt is gone, check if we need to resend the Claude command
|
|
1602
|
-
const promptGone = !trustPromptPatterns.some(p => p.test(updatedContent));
|
|
2614
|
+
const promptGone = !trustPromptPatterns.some((p) => p.test(updatedContent));
|
|
1603
2615
|
if (promptGone) {
|
|
1604
2616
|
// Check if Claude is running or if we need to restart it
|
|
1605
|
-
const claudeRunning = updatedContent.includes(
|
|
1606
|
-
updatedContent.includes(
|
|
1607
|
-
updatedContent.includes(
|
|
1608
|
-
(prompt &&
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
execSync(\`tmux send-keys -t '\${paneInfo}'
|
|
2617
|
+
const claudeRunning = updatedContent.includes("Claude") ||
|
|
2618
|
+
updatedContent.includes("claude") ||
|
|
2619
|
+
updatedContent.includes("Assistant") ||
|
|
2620
|
+
(prompt &&
|
|
2621
|
+
updatedContent.includes(prompt.substring(0, Math.min(20, prompt.length))));
|
|
2622
|
+
if (!claudeRunning && !updatedContent.includes("$")) {
|
|
2623
|
+
await new Promise((resolve) => setTimeout(resolve, 300));
|
|
2624
|
+
execSync(\`tmux send-keys -t '\${paneInfo}' '\${escapedCmd}'\`, { stdio: "pipe" });
|
|
2625
|
+
execSync(\`tmux send-keys -t '\${paneInfo}' Enter\`, {
|
|
2626
|
+
stdio: "pipe",
|
|
2627
|
+
});
|
|
1613
2628
|
}
|
|
1614
2629
|
break;
|
|
1615
2630
|
}
|
|
1616
2631
|
}
|
|
1617
2632
|
}
|
|
1618
2633
|
// If we see Claude is already running without prompts, we're done
|
|
1619
|
-
if (!hasTrustPrompt &&
|
|
1620
|
-
|
|
2634
|
+
if (!hasTrustPrompt &&
|
|
2635
|
+
!hasClaudePermissionPrompt &&
|
|
2636
|
+
(paneContent.includes("Claude") ||
|
|
2637
|
+
paneContent.includes("Assistant"))) {
|
|
1621
2638
|
break;
|
|
1622
2639
|
}
|
|
1623
2640
|
}
|
|
@@ -1627,37 +2644,35 @@ const DmuxApp = ({ panesFile, projectName, sessionName, settingsFile, projectRoo
|
|
|
1627
2644
|
}
|
|
1628
2645
|
};
|
|
1629
2646
|
// Start monitoring for trust prompt in background
|
|
1630
|
-
autoApproveTrust().catch(err => {
|
|
1631
|
-
});
|
|
2647
|
+
autoApproveTrust().catch((err) => { });
|
|
1632
2648
|
}
|
|
1633
2649
|
// Keep focus on the new pane
|
|
1634
|
-
execSync(\`tmux select-pane -t '\${paneInfo}'\`, { stdio:
|
|
2650
|
+
execSync(\`tmux select-pane -t '\${paneInfo}'\`, { stdio: "pipe" });
|
|
1635
2651
|
// Save pane info
|
|
1636
2652
|
const newPane = {
|
|
1637
2653
|
id: \`dmux-\${Date.now()}\`,
|
|
1638
2654
|
slug,
|
|
1639
|
-
prompt: prompt ||
|
|
2655
|
+
prompt: prompt || "No initial prompt",
|
|
1640
2656
|
paneId: paneInfo,
|
|
1641
2657
|
worktreePath,
|
|
1642
|
-
agent
|
|
2658
|
+
agent,
|
|
1643
2659
|
};
|
|
1644
2660
|
const updatedPanes = [...panes, newPane];
|
|
1645
2661
|
await savePanes(updatedPanes);
|
|
1646
2662
|
// Switch back to the original pane (where dmux is running)
|
|
1647
|
-
execSync(\`tmux select-pane -t '\${originalPaneId}'\`, { stdio:
|
|
2663
|
+
execSync(\`tmux select-pane -t '\${originalPaneId}'\`, { stdio: "pipe" });
|
|
1648
2664
|
// Re-set the title for the dmux pane
|
|
1649
2665
|
try {
|
|
1650
|
-
execSync(\`tmux select-pane -t '\${originalPaneId}' -T "dmux
|
|
2666
|
+
execSync(\`tmux select-pane -t '\${originalPaneId}' -T "dmux v\${packageJson.version} - \${projectName}"\`, { stdio: "pipe" });
|
|
1651
2667
|
}
|
|
1652
2668
|
catch {
|
|
1653
2669
|
// Ignore if setting title fails
|
|
1654
2670
|
}
|
|
1655
2671
|
// Clear the screen and redraw the UI
|
|
1656
|
-
process.stdout.write(
|
|
2672
|
+
process.stdout.write("\\x1b[2J\\x1b[H");
|
|
1657
2673
|
// Reset the creating pane flag and refresh
|
|
1658
2674
|
setIsCreatingPane(false);
|
|
1659
|
-
setStatusMessage(
|
|
1660
|
-
setNewPanePrompt('');
|
|
2675
|
+
setStatusMessage("");
|
|
1661
2676
|
// Force a reload of panes to ensure UI is up to date
|
|
1662
2677
|
await loadPanes();
|
|
1663
2678
|
};
|
|
@@ -1665,30 +2680,32 @@ const DmuxApp = ({ panesFile, projectName, sessionName, settingsFile, projectRoo
|
|
|
1665
2680
|
try {
|
|
1666
2681
|
// Enable pane borders to show titles (if not already enabled)
|
|
1667
2682
|
try {
|
|
1668
|
-
execSync(\`tmux set-option -g pane-border-status top\`, { stdio:
|
|
2683
|
+
execSync(\`tmux set-option -g pane-border-status top\`, { stdio: "pipe" });
|
|
1669
2684
|
}
|
|
1670
2685
|
catch {
|
|
1671
2686
|
// Ignore if already set or fails
|
|
1672
2687
|
}
|
|
1673
|
-
execSync(\`tmux select-pane -t '\${paneId}'\`, { stdio:
|
|
2688
|
+
execSync(\`tmux select-pane -t '\${paneId}'\`, { stdio: "pipe" });
|
|
1674
2689
|
// Clear screen after jump to remove artifacts
|
|
1675
2690
|
clearScreen();
|
|
1676
|
-
setStatusMessage(
|
|
1677
|
-
setTimeout(() => setStatusMessage(
|
|
2691
|
+
setStatusMessage("Jumped to pane");
|
|
2692
|
+
setTimeout(() => setStatusMessage(""), 2000);
|
|
1678
2693
|
}
|
|
1679
2694
|
catch {
|
|
1680
|
-
setStatusMessage(
|
|
1681
|
-
setTimeout(() => setStatusMessage(
|
|
2695
|
+
setStatusMessage("Failed to jump - pane may be closed");
|
|
2696
|
+
setTimeout(() => setStatusMessage(""), 2000);
|
|
1682
2697
|
}
|
|
1683
2698
|
};
|
|
1684
2699
|
const runCommand = async (type, pane) => {
|
|
1685
2700
|
if (!pane.worktreePath) {
|
|
1686
|
-
setStatusMessage(
|
|
1687
|
-
setTimeout(() => setStatusMessage(
|
|
2701
|
+
setStatusMessage("No worktree path for this pane");
|
|
2702
|
+
setTimeout(() => setStatusMessage(""), 2000);
|
|
1688
2703
|
return;
|
|
1689
2704
|
}
|
|
1690
|
-
const command = type ===
|
|
1691
|
-
const isFirstRun = type ===
|
|
2705
|
+
const command = type === "test" ? projectSettings.testCommand : projectSettings.devCommand;
|
|
2706
|
+
const isFirstRun = type === "test"
|
|
2707
|
+
? !projectSettings.firstTestRun
|
|
2708
|
+
: !projectSettings.firstDevRun;
|
|
1692
2709
|
if (!command) {
|
|
1693
2710
|
// No command configured, prompt user
|
|
1694
2711
|
setShowCommandPrompt(type);
|
|
@@ -1707,444 +2724,139 @@ const DmuxApp = ({ panesFile, projectName, sessionName, settingsFile, projectRoo
|
|
|
1707
2724
|
setRunningCommand(true);
|
|
1708
2725
|
setStatusMessage(\`Starting \${type} in background window...\`);
|
|
1709
2726
|
// Kill existing window if present
|
|
1710
|
-
const existingWindowId = type ===
|
|
2727
|
+
const existingWindowId = type === "test" ? pane.testWindowId : pane.devWindowId;
|
|
1711
2728
|
if (existingWindowId) {
|
|
1712
2729
|
try {
|
|
1713
|
-
execSync(\`tmux kill-window -t '\${existingWindowId}'\`, {
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
}
|
|
1717
|
-
// Create a new background window for the command
|
|
1718
|
-
const windowName = \`\${pane.slug}-\${type}\`;
|
|
1719
|
-
const windowId = execSync(\`tmux new-window -d -n '\${windowName}' -P -F '#{window_id}'\`, { encoding: 'utf-8', stdio: 'pipe' }).trim();
|
|
1720
|
-
// Create a log file to capture output
|
|
1721
|
-
const logFile = \`/tmp/dmux-\${pane.id}-\${type}.log\`;
|
|
1722
|
-
// Build the command with output capture
|
|
1723
|
-
const fullCommand = type === 'test'
|
|
1724
|
-
? \`cd "\${pane.worktreePath}" && \${command} 2>&1 | tee \${logFile}\`
|
|
1725
|
-
: \`cd "\${pane.worktreePath}" && \${command} 2>&1 | tee \${logFile}\`;
|
|
1726
|
-
// Send the command to the new window
|
|
1727
|
-
execSync(\`tmux send-keys -t '\${windowId}' '\${fullCommand.replace(/'/g, "'\\\\''")}'\`, { stdio: 'pipe' });
|
|
1728
|
-
execSync(\`tmux send-keys -t '\${windowId}' Enter\`, { stdio: 'pipe' });
|
|
1729
|
-
// Update pane with window info
|
|
1730
|
-
const updatedPane = {
|
|
1731
|
-
...pane,
|
|
1732
|
-
[type === 'test' ? 'testWindowId' : 'devWindowId']: windowId,
|
|
1733
|
-
[type === 'test' ? 'testStatus' : 'devStatus']: 'running'
|
|
1734
|
-
};
|
|
1735
|
-
const updatedPanes = panes.map(p => p.id === pane.id ? updatedPane : p);
|
|
1736
|
-
await savePanes(updatedPanes);
|
|
1737
|
-
// Start monitoring the output
|
|
1738
|
-
if (type === 'test') {
|
|
1739
|
-
// For tests, monitor for completion
|
|
1740
|
-
setTimeout(() => monitorTestOutput(pane.id, logFile), 2000);
|
|
1741
|
-
}
|
|
1742
|
-
else {
|
|
1743
|
-
// For dev, monitor for server URL
|
|
1744
|
-
setTimeout(() => monitorDevOutput(pane.id, logFile), 2000);
|
|
1745
|
-
}
|
|
1746
|
-
setRunningCommand(false);
|
|
1747
|
-
setStatusMessage(\`\${type === 'test' ? 'Test' : 'Dev server'} started in background\`);
|
|
1748
|
-
setTimeout(() => setStatusMessage(''), 3000);
|
|
1749
|
-
}
|
|
1750
|
-
catch (error) {
|
|
1751
|
-
setRunningCommand(false);
|
|
1752
|
-
setStatusMessage(\`Failed to run \${type} command\`);
|
|
1753
|
-
setTimeout(() => setStatusMessage(''), 3000);
|
|
1754
|
-
}
|
|
1755
|
-
};
|
|
1756
|
-
// Update handling moved to useAutoUpdater
|
|
1757
|
-
// Helper function to clear screen artifacts
|
|
1758
|
-
const clearScreen = () => {
|
|
1759
|
-
// CRITICAL: Force Ink to re-render FIRST, before clearing
|
|
1760
|
-
// This prevents blank screen by ensuring React starts rendering immediately
|
|
1761
|
-
setForceRepaintTrigger(prev => prev + 1);
|
|
1762
|
-
// Multiple clearing strategies to prevent artifacts
|
|
1763
|
-
// 1. Clear screen with ANSI codes
|
|
1764
|
-
process.stdout.write('\\x1b[2J\\x1b[H');
|
|
1765
|
-
// 2. Clear tmux history
|
|
1766
|
-
try {
|
|
1767
|
-
execSync('tmux clear-history', { stdio: 'pipe' });
|
|
1768
|
-
}
|
|
1769
|
-
catch { }
|
|
1770
|
-
// 3. Force tmux to refresh the display
|
|
1771
|
-
try {
|
|
1772
|
-
execSync('tmux refresh-client', { stdio: 'pipe' });
|
|
1773
|
-
}
|
|
1774
|
-
catch { }
|
|
1775
|
-
};
|
|
1776
|
-
// Cleanup function for exit
|
|
1777
|
-
const cleanExit = () => {
|
|
1778
|
-
// Clear screen before exiting Ink
|
|
1779
|
-
process.stdout.write('\\x1b[2J\\x1b[3J\\x1b[H');
|
|
1780
|
-
// Exit the Ink app (this cleans up the React tree)
|
|
1781
|
-
exit();
|
|
1782
|
-
// Give Ink a moment to clean up its rendering, then do final cleanup
|
|
1783
|
-
setTimeout(() => {
|
|
1784
|
-
// Multiple aggressive clearing strategies
|
|
1785
|
-
process.stdout.write('\\x1b[2J\\x1b[H'); // Clear screen and move cursor to home
|
|
1786
|
-
process.stdout.write('\\x1b[3J'); // Clear scrollback buffer
|
|
1787
|
-
process.stdout.write('\\x1b[0m'); // Reset all attributes
|
|
1788
|
-
// Clear tmux history and pane
|
|
1789
|
-
try {
|
|
1790
|
-
execSync('tmux clear-history', { stdio: 'pipe' });
|
|
1791
|
-
execSync('tmux send-keys C-l', { stdio: 'pipe' });
|
|
1792
|
-
}
|
|
1793
|
-
catch { }
|
|
1794
|
-
// One more final clear
|
|
1795
|
-
process.stdout.write('\\x1b[2J\\x1b[H');
|
|
1796
|
-
// Show clean goodbye message
|
|
1797
|
-
process.stdout.write('\\n Run dmux again to resume. Goodbye 👋\\n\\n');
|
|
1798
|
-
// Exit process
|
|
1799
|
-
process.exit(0);
|
|
1800
|
-
}, 100);
|
|
1801
|
-
};
|
|
1802
|
-
useInput(async (input, key) => {
|
|
1803
|
-
// Handle Ctrl+C for quit confirmation (must be first, before any other checks)
|
|
1804
|
-
if (key.ctrl && input === 'c') {
|
|
1805
|
-
if (quitConfirmMode) {
|
|
1806
|
-
// Second Ctrl+C - actually quit
|
|
1807
|
-
cleanExit();
|
|
1808
|
-
}
|
|
1809
|
-
else {
|
|
1810
|
-
// First Ctrl+C - show confirmation
|
|
1811
|
-
setQuitConfirmMode(true);
|
|
1812
|
-
// Reset after 3 seconds if user doesn't press Ctrl+C again
|
|
1813
|
-
setTimeout(() => {
|
|
1814
|
-
setQuitConfirmMode(false);
|
|
1815
|
-
}, 3000);
|
|
1816
|
-
}
|
|
1817
|
-
return;
|
|
1818
|
-
}
|
|
1819
|
-
if (isCreatingPane || runningCommand || isUpdating || isLoading || isCreatingTunnel) {
|
|
1820
|
-
// Disable input while performing operations or loading
|
|
1821
|
-
return;
|
|
1822
|
-
}
|
|
1823
|
-
// Handle kebab menu navigation
|
|
1824
|
-
if (showKebabMenu && kebabMenuPaneIndex !== null) {
|
|
1825
|
-
const currentPane = panes[kebabMenuPaneIndex];
|
|
1826
|
-
const availableActions = kebabMenuActions;
|
|
1827
|
-
if (key.escape) {
|
|
1828
|
-
setShowKebabMenu(false);
|
|
1829
|
-
setKebabMenuPaneIndex(null);
|
|
1830
|
-
setKebabMenuOption(0);
|
|
1831
|
-
setKebabMenuActions([]);
|
|
1832
|
-
return;
|
|
1833
|
-
}
|
|
1834
|
-
else if (key.upArrow) {
|
|
1835
|
-
setKebabMenuOption(Math.max(0, kebabMenuOption - 1));
|
|
1836
|
-
return;
|
|
1837
|
-
}
|
|
1838
|
-
else if (key.downArrow) {
|
|
1839
|
-
setKebabMenuOption(Math.min(availableActions.length - 1, kebabMenuOption + 1));
|
|
1840
|
-
return;
|
|
1841
|
-
}
|
|
1842
|
-
else if (key.return) {
|
|
1843
|
-
// Execute the selected menu action
|
|
1844
|
-
setShowKebabMenu(false);
|
|
1845
|
-
const selectedAction = availableActions[kebabMenuOption];
|
|
1846
|
-
if (selectedAction) {
|
|
1847
|
-
// Use the action system to execute
|
|
1848
|
-
actionSystem.executeAction(selectedAction.id, currentPane, { mainBranch: getMainBranch() });
|
|
1849
|
-
}
|
|
1850
|
-
setKebabMenuPaneIndex(null);
|
|
1851
|
-
setKebabMenuOption(0);
|
|
1852
|
-
setKebabMenuActions([]);
|
|
1853
|
-
return;
|
|
1854
|
-
}
|
|
1855
|
-
// Don't process other inputs while menu is open
|
|
1856
|
-
return;
|
|
1857
|
-
}
|
|
1858
|
-
// Handle quit confirm mode - ESC cancels it
|
|
1859
|
-
if (quitConfirmMode) {
|
|
1860
|
-
if (key.escape) {
|
|
1861
|
-
setQuitConfirmMode(false);
|
|
1862
|
-
return;
|
|
1863
|
-
}
|
|
1864
|
-
// Allow other inputs to continue (don't return early)
|
|
1865
|
-
}
|
|
1866
|
-
// Handle QR code view
|
|
1867
|
-
if (showQRCode) {
|
|
1868
|
-
if (key.escape) {
|
|
1869
|
-
setShowQRCode(false);
|
|
1870
|
-
}
|
|
1871
|
-
return;
|
|
1872
|
-
}
|
|
1873
|
-
// Handle hooks dialog
|
|
1874
|
-
if (showHooksDialog) {
|
|
1875
|
-
if (key.escape) {
|
|
1876
|
-
setShowHooksDialog(false);
|
|
1877
|
-
setHooksSelectedIndex(0);
|
|
1878
|
-
// Go back to settings dialog
|
|
1879
|
-
setShowSettingsDialog(true);
|
|
1880
|
-
setSettingsMode('list');
|
|
1881
|
-
return;
|
|
1882
|
-
}
|
|
1883
|
-
else if (key.upArrow) {
|
|
1884
|
-
setHooksSelectedIndex(Math.max(0, hooksSelectedIndex - 1));
|
|
1885
|
-
return;
|
|
1886
|
-
}
|
|
1887
|
-
else if (key.downArrow) {
|
|
1888
|
-
setHooksSelectedIndex(Math.min(hooksData.length - 1, hooksSelectedIndex + 1));
|
|
1889
|
-
return;
|
|
1890
|
-
}
|
|
1891
|
-
else if (input === 'e') {
|
|
1892
|
-
// Edit hooks using an agent
|
|
1893
|
-
setShowHooksDialog(false);
|
|
1894
|
-
setHooksSelectedIndex(0);
|
|
1895
|
-
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";
|
|
1896
|
-
setPendingPrompt(prompt);
|
|
1897
|
-
setNewPanePrompt(prompt);
|
|
1898
|
-
// Choose agent
|
|
1899
|
-
const agents = availableAgents;
|
|
1900
|
-
if (agents.length === 0) {
|
|
1901
|
-
createNewPaneHook(prompt);
|
|
1902
|
-
}
|
|
1903
|
-
else if (agents.length === 1) {
|
|
1904
|
-
createNewPaneHook(prompt, agents[0]);
|
|
1905
|
-
}
|
|
1906
|
-
else {
|
|
1907
|
-
setShowAgentChoiceDialog(true);
|
|
1908
|
-
setAgentChoice(agentChoice || 'claude');
|
|
1909
|
-
}
|
|
1910
|
-
return;
|
|
1911
|
-
}
|
|
1912
|
-
return;
|
|
1913
|
-
}
|
|
1914
|
-
// Handle settings dialog
|
|
1915
|
-
if (showSettingsDialog) {
|
|
1916
|
-
if (key.escape) {
|
|
1917
|
-
if (settingsMode === 'list') {
|
|
1918
|
-
// Close settings dialog
|
|
1919
|
-
setShowSettingsDialog(false);
|
|
1920
|
-
setSettingsMode('list');
|
|
1921
|
-
setSettingsSelectedIndex(0);
|
|
1922
|
-
setSettingsEditingKey(undefined);
|
|
1923
|
-
}
|
|
1924
|
-
else {
|
|
1925
|
-
// Go back to list
|
|
1926
|
-
setSettingsMode('list');
|
|
1927
|
-
setSettingsEditingKey(undefined);
|
|
1928
|
-
setSettingsEditingValueIndex(0);
|
|
1929
|
-
setSettingsScopeIndex(0);
|
|
1930
|
-
}
|
|
1931
|
-
return;
|
|
1932
|
-
}
|
|
1933
|
-
else if (key.upArrow) {
|
|
1934
|
-
if (settingsMode === 'list') {
|
|
1935
|
-
setSettingsSelectedIndex(Math.max(0, settingsSelectedIndex - 1));
|
|
1936
|
-
}
|
|
1937
|
-
else if (settingsMode === 'edit') {
|
|
1938
|
-
const currentDef = SETTING_DEFINITIONS[settingsSelectedIndex];
|
|
1939
|
-
const maxIndex = currentDef.type === 'boolean' ? 1 : (currentDef.options?.length || 1) - 1;
|
|
1940
|
-
setSettingsEditingValueIndex(Math.max(0, settingsEditingValueIndex - 1));
|
|
1941
|
-
}
|
|
1942
|
-
else if (settingsMode === 'scope') {
|
|
1943
|
-
setSettingsScopeIndex(Math.max(0, settingsScopeIndex - 1));
|
|
1944
|
-
}
|
|
1945
|
-
return;
|
|
1946
|
-
}
|
|
1947
|
-
else if (key.downArrow) {
|
|
1948
|
-
if (settingsMode === 'list') {
|
|
1949
|
-
setSettingsSelectedIndex(Math.min(SETTING_DEFINITIONS.length - 1, settingsSelectedIndex + 1));
|
|
1950
|
-
}
|
|
1951
|
-
else if (settingsMode === 'edit') {
|
|
1952
|
-
const currentDef = SETTING_DEFINITIONS[settingsSelectedIndex];
|
|
1953
|
-
const maxIndex = currentDef.type === 'boolean' ? 1 : (currentDef.options?.length || 1) - 1;
|
|
1954
|
-
setSettingsEditingValueIndex(Math.min(maxIndex, settingsEditingValueIndex + 1));
|
|
1955
|
-
}
|
|
1956
|
-
else if (settingsMode === 'scope') {
|
|
1957
|
-
setSettingsScopeIndex(Math.min(1, settingsScopeIndex + 1));
|
|
1958
|
-
}
|
|
1959
|
-
return;
|
|
1960
|
-
}
|
|
1961
|
-
else if (key.return) {
|
|
1962
|
-
if (settingsMode === 'list') {
|
|
1963
|
-
const currentDef = SETTING_DEFINITIONS[settingsSelectedIndex];
|
|
1964
|
-
// Handle action type - trigger the action instead of editing
|
|
1965
|
-
if (currentDef.type === 'action') {
|
|
1966
|
-
if (currentDef.key === 'hooks') {
|
|
1967
|
-
// Show hooks dialog
|
|
1968
|
-
setShowSettingsDialog(false);
|
|
1969
|
-
const { hasHook } = await import('./utils/hooks.js');
|
|
1970
|
-
const allHookTypes = [
|
|
1971
|
-
'before_pane_create',
|
|
1972
|
-
'pane_created',
|
|
1973
|
-
'worktree_created',
|
|
1974
|
-
'before_pane_close',
|
|
1975
|
-
'pane_closed',
|
|
1976
|
-
'before_worktree_remove',
|
|
1977
|
-
'worktree_removed',
|
|
1978
|
-
'pre_merge',
|
|
1979
|
-
'post_merge',
|
|
1980
|
-
'run_test',
|
|
1981
|
-
'run_dev',
|
|
1982
|
-
];
|
|
1983
|
-
const hooks = allHookTypes.map(hookName => ({
|
|
1984
|
-
name: hookName,
|
|
1985
|
-
active: hasHook(projectRoot || process.cwd(), hookName)
|
|
1986
|
-
}));
|
|
1987
|
-
setHooksData(hooks);
|
|
1988
|
-
setShowHooksDialog(true);
|
|
1989
|
-
setHooksSelectedIndex(0);
|
|
1990
|
-
}
|
|
1991
|
-
return;
|
|
1992
|
-
}
|
|
1993
|
-
// Enter edit mode for regular settings
|
|
1994
|
-
setSettingsEditingKey(currentDef.key);
|
|
1995
|
-
setSettingsMode('edit');
|
|
1996
|
-
// Set initial value index based on current setting
|
|
1997
|
-
const currentValue = settingsManager.getSetting(currentDef.key);
|
|
1998
|
-
if (currentDef.type === 'boolean') {
|
|
1999
|
-
setSettingsEditingValueIndex(currentValue ? 0 : 1);
|
|
2000
|
-
}
|
|
2001
|
-
else if (currentDef.type === 'select' && currentDef.options) {
|
|
2002
|
-
const optIndex = currentDef.options.findIndex(o => o.value === currentValue);
|
|
2003
|
-
setSettingsEditingValueIndex(Math.max(0, optIndex));
|
|
2004
|
-
}
|
|
2005
|
-
}
|
|
2006
|
-
else if (settingsMode === 'edit') {
|
|
2007
|
-
// Go to scope selection
|
|
2008
|
-
setSettingsMode('scope');
|
|
2009
|
-
setSettingsScopeIndex(0);
|
|
2010
|
-
}
|
|
2011
|
-
else if (settingsMode === 'scope') {
|
|
2012
|
-
// Save the setting
|
|
2013
|
-
const currentDef = SETTING_DEFINITIONS[settingsSelectedIndex];
|
|
2014
|
-
const scope = settingsScopeIndex === 0 ? 'global' : 'project';
|
|
2015
|
-
// Only save if this is not an action type (actions don't have values)
|
|
2016
|
-
if (currentDef.type !== 'action') {
|
|
2017
|
-
// Calculate the new value
|
|
2018
|
-
let newValue;
|
|
2019
|
-
if (currentDef.type === 'boolean') {
|
|
2020
|
-
newValue = settingsEditingValueIndex === 0;
|
|
2021
|
-
}
|
|
2022
|
-
else if (currentDef.type === 'select' && currentDef.options) {
|
|
2023
|
-
newValue = currentDef.options[settingsEditingValueIndex]?.value || '';
|
|
2024
|
-
}
|
|
2025
|
-
// Update the setting - cast key to proper type since we know it's not an action
|
|
2026
|
-
settingsManager.updateSetting(currentDef.key, newValue, scope);
|
|
2027
|
-
}
|
|
2028
|
-
// Reset to list view
|
|
2029
|
-
setSettingsMode('list');
|
|
2030
|
-
setSettingsEditingKey(undefined);
|
|
2031
|
-
setSettingsEditingValueIndex(0);
|
|
2032
|
-
setSettingsScopeIndex(0);
|
|
2033
|
-
setStatusMessage(\`Setting saved (\${scope})\`);
|
|
2034
|
-
setTimeout(() => setStatusMessage(''), 2000);
|
|
2035
|
-
}
|
|
2036
|
-
return;
|
|
2037
|
-
}
|
|
2038
|
-
return;
|
|
2039
|
-
}
|
|
2040
|
-
// Handle action system confirm dialog
|
|
2041
|
-
if (actionSystem.actionState.showConfirmDialog) {
|
|
2042
|
-
if (key.escape) {
|
|
2043
|
-
if (actionSystem.actionState.onConfirmNo) {
|
|
2044
|
-
actionSystem.executeCallback(actionSystem.actionState.onConfirmNo);
|
|
2045
|
-
}
|
|
2046
|
-
else {
|
|
2047
|
-
actionSystem.setActionState(prev => ({ ...prev, showConfirmDialog: false }));
|
|
2048
|
-
}
|
|
2049
|
-
return;
|
|
2050
|
-
}
|
|
2051
|
-
else if (key.upArrow) {
|
|
2052
|
-
actionSystem.setActionState(prev => ({
|
|
2053
|
-
...prev,
|
|
2054
|
-
confirmSelectedIndex: Math.max(0, prev.confirmSelectedIndex - 1)
|
|
2055
|
-
}));
|
|
2056
|
-
return;
|
|
2057
|
-
}
|
|
2058
|
-
else if (key.downArrow) {
|
|
2059
|
-
actionSystem.setActionState(prev => ({
|
|
2060
|
-
...prev,
|
|
2061
|
-
confirmSelectedIndex: Math.min(1, prev.confirmSelectedIndex + 1)
|
|
2062
|
-
}));
|
|
2063
|
-
return;
|
|
2064
|
-
}
|
|
2065
|
-
else if (key.return) {
|
|
2066
|
-
// Execute based on selected index
|
|
2067
|
-
if (actionSystem.actionState.confirmSelectedIndex === 0) {
|
|
2068
|
-
if (actionSystem.actionState.onConfirmYes) {
|
|
2069
|
-
actionSystem.executeCallback(actionSystem.actionState.onConfirmYes);
|
|
2070
|
-
}
|
|
2071
|
-
}
|
|
2072
|
-
else {
|
|
2073
|
-
if (actionSystem.actionState.onConfirmNo) {
|
|
2074
|
-
actionSystem.executeCallback(actionSystem.actionState.onConfirmNo);
|
|
2075
|
-
}
|
|
2076
|
-
else {
|
|
2077
|
-
actionSystem.setActionState(prev => ({ ...prev, showConfirmDialog: false }));
|
|
2078
|
-
}
|
|
2079
|
-
}
|
|
2080
|
-
return;
|
|
2081
|
-
}
|
|
2082
|
-
else if (input === 'y' || input === 'Y') {
|
|
2083
|
-
// Shortcut: yes
|
|
2084
|
-
if (actionSystem.actionState.onConfirmYes) {
|
|
2085
|
-
actionSystem.executeCallback(actionSystem.actionState.onConfirmYes);
|
|
2730
|
+
execSync(\`tmux kill-window -t '\${existingWindowId}'\`, {
|
|
2731
|
+
stdio: "pipe",
|
|
2732
|
+
});
|
|
2086
2733
|
}
|
|
2087
|
-
|
|
2734
|
+
catch { }
|
|
2088
2735
|
}
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
}
|
|
2097
|
-
|
|
2736
|
+
// Create a new background window for the command
|
|
2737
|
+
const windowName = \`\${pane.slug}-\${type}\`;
|
|
2738
|
+
const windowId = execSync(\`tmux new-window -d -n '\${windowName}' -P -F '#{window_id}'\`, { encoding: "utf-8", stdio: "pipe" }).trim();
|
|
2739
|
+
// Create a log file to capture output
|
|
2740
|
+
const logFile = \`/tmp/dmux-\${pane.id}-\${type}.log\`;
|
|
2741
|
+
// Build the command with output capture
|
|
2742
|
+
const fullCommand = type === "test"
|
|
2743
|
+
? \`cd "\${pane.worktreePath}" && \${command} 2>&1 | tee \${logFile}\`
|
|
2744
|
+
: \`cd "\${pane.worktreePath}" && \${command} 2>&1 | tee \${logFile}\`;
|
|
2745
|
+
// Send the command to the new window
|
|
2746
|
+
execSync(\`tmux send-keys -t '\${windowId}' '\${fullCommand.replace(/'/g, "'\\\\''")}'\`, { stdio: "pipe" });
|
|
2747
|
+
execSync(\`tmux send-keys -t '\${windowId}' Enter\`, { stdio: "pipe" });
|
|
2748
|
+
// Update pane with window info
|
|
2749
|
+
const updatedPane = {
|
|
2750
|
+
...pane,
|
|
2751
|
+
[type === "test" ? "testWindowId" : "devWindowId"]: windowId,
|
|
2752
|
+
[type === "test" ? "testStatus" : "devStatus"]: "running",
|
|
2753
|
+
};
|
|
2754
|
+
const updatedPanes = panes.map((p) => p.id === pane.id ? updatedPane : p);
|
|
2755
|
+
await savePanes(updatedPanes);
|
|
2756
|
+
// Start monitoring the output
|
|
2757
|
+
if (type === "test") {
|
|
2758
|
+
// For tests, monitor for completion
|
|
2759
|
+
setTimeout(() => monitorTestOutput(pane.id, logFile), 2000);
|
|
2760
|
+
}
|
|
2761
|
+
else {
|
|
2762
|
+
// For dev, monitor for server URL
|
|
2763
|
+
setTimeout(() => monitorDevOutput(pane.id, logFile), 2000);
|
|
2764
|
+
}
|
|
2765
|
+
setRunningCommand(false);
|
|
2766
|
+
setStatusMessage(\`\${type === "test" ? "Test" : "Dev server"} started in background\`);
|
|
2767
|
+
setTimeout(() => setStatusMessage(""), 3000);
|
|
2768
|
+
}
|
|
2769
|
+
catch (error) {
|
|
2770
|
+
setRunningCommand(false);
|
|
2771
|
+
setStatusMessage(\`Failed to run \${type} command\`);
|
|
2772
|
+
setTimeout(() => setStatusMessage(""), 3000);
|
|
2773
|
+
}
|
|
2774
|
+
};
|
|
2775
|
+
// Update handling moved to useAutoUpdater
|
|
2776
|
+
// Helper function to clear screen artifacts
|
|
2777
|
+
const clearScreen = () => {
|
|
2778
|
+
// CRITICAL: Force Ink to re-render FIRST, before clearing
|
|
2779
|
+
// This prevents blank screen by ensuring React starts rendering immediately
|
|
2780
|
+
setForceRepaintTrigger((prev) => prev + 1);
|
|
2781
|
+
// Multiple clearing strategies to prevent artifacts
|
|
2782
|
+
// 1. Clear screen with ANSI codes
|
|
2783
|
+
process.stdout.write("\\x1b[2J\\x1b[H");
|
|
2784
|
+
// 2. Clear tmux history
|
|
2785
|
+
try {
|
|
2786
|
+
execSync("tmux clear-history", { stdio: "pipe" });
|
|
2787
|
+
}
|
|
2788
|
+
catch { }
|
|
2789
|
+
// 3. Force tmux to refresh the display
|
|
2790
|
+
try {
|
|
2791
|
+
execSync("tmux refresh-client", { stdio: "pipe" });
|
|
2792
|
+
}
|
|
2793
|
+
catch { }
|
|
2794
|
+
};
|
|
2795
|
+
// Cleanup function for exit
|
|
2796
|
+
const cleanExit = () => {
|
|
2797
|
+
// Clear screen before exiting Ink
|
|
2798
|
+
process.stdout.write("\\x1b[2J\\x1b[3J\\x1b[H");
|
|
2799
|
+
// Exit the Ink app (this cleans up the React tree)
|
|
2800
|
+
exit();
|
|
2801
|
+
// Give Ink a moment to clean up its rendering, then do final cleanup
|
|
2802
|
+
setTimeout(() => {
|
|
2803
|
+
// Multiple aggressive clearing strategies
|
|
2804
|
+
process.stdout.write("\\x1b[2J\\x1b[H"); // Clear screen and move cursor to home
|
|
2805
|
+
process.stdout.write("\\x1b[3J"); // Clear scrollback buffer
|
|
2806
|
+
process.stdout.write("\\x1b[0m"); // Reset all attributes
|
|
2807
|
+
// Clear tmux history and pane
|
|
2808
|
+
try {
|
|
2809
|
+
execSync("tmux clear-history", { stdio: "pipe" });
|
|
2810
|
+
execSync("tmux send-keys C-l", { stdio: "pipe" });
|
|
2098
2811
|
}
|
|
2812
|
+
catch { }
|
|
2813
|
+
// One more final clear
|
|
2814
|
+
process.stdout.write("\\x1b[2J\\x1b[H");
|
|
2815
|
+
// Show clean goodbye message
|
|
2816
|
+
process.stdout.write("\\n Run dmux again to resume. Goodbye 👋\\n\\n");
|
|
2817
|
+
// Exit process
|
|
2818
|
+
process.exit(0);
|
|
2819
|
+
}, 100);
|
|
2820
|
+
};
|
|
2821
|
+
useInput(async (input, key) => {
|
|
2822
|
+
const logService = LogService.getInstance();
|
|
2823
|
+
// Log all input for debugging (only first 50 chars to avoid spam)
|
|
2824
|
+
const inputPreview = input.length > 50 ? input.substring(0, 50) + "..." : input;
|
|
2825
|
+
logService.debug(\`Input: "\${inputPreview}"\`, "InputDebug");
|
|
2826
|
+
// Ignore input temporarily after popup operations (prevents buffered keys from being processed)
|
|
2827
|
+
if (ignoreInput) {
|
|
2099
2828
|
return;
|
|
2100
2829
|
}
|
|
2101
|
-
// Handle
|
|
2102
|
-
if (
|
|
2103
|
-
if (
|
|
2104
|
-
|
|
2105
|
-
|
|
2830
|
+
// Handle Ctrl+C for quit confirmation (must be first, before any other checks)
|
|
2831
|
+
if (key.ctrl && input === "c") {
|
|
2832
|
+
if (quitConfirmMode) {
|
|
2833
|
+
// Second Ctrl+C - actually quit
|
|
2834
|
+
cleanExit();
|
|
2106
2835
|
}
|
|
2107
|
-
else
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2836
|
+
else {
|
|
2837
|
+
// First Ctrl+C - show confirmation
|
|
2838
|
+
setQuitConfirmMode(true);
|
|
2839
|
+
// Reset after 3 seconds if user doesn't press Ctrl+C again
|
|
2840
|
+
setTimeout(() => {
|
|
2841
|
+
setQuitConfirmMode(false);
|
|
2842
|
+
}, 3000);
|
|
2112
2843
|
}
|
|
2113
|
-
// Let CleanTextInput handle all other key events
|
|
2114
2844
|
return;
|
|
2115
2845
|
}
|
|
2116
|
-
|
|
2117
|
-
|
|
2846
|
+
if (isCreatingPane || runningCommand || isUpdating || isLoading) {
|
|
2847
|
+
// Disable input while performing operations or loading
|
|
2848
|
+
return;
|
|
2849
|
+
}
|
|
2850
|
+
// Handle quit confirm mode - ESC cancels it
|
|
2851
|
+
if (quitConfirmMode) {
|
|
2118
2852
|
if (key.escape) {
|
|
2119
|
-
|
|
2120
|
-
return;
|
|
2121
|
-
}
|
|
2122
|
-
else if (key.upArrow) {
|
|
2123
|
-
actionSystem.setActionState(prev => ({
|
|
2124
|
-
...prev,
|
|
2125
|
-
choiceSelectedIndex: Math.max(0, prev.choiceSelectedIndex - 1)
|
|
2126
|
-
}));
|
|
2127
|
-
return;
|
|
2128
|
-
}
|
|
2129
|
-
else if (key.downArrow) {
|
|
2130
|
-
const maxIndex = actionSystem.actionState.choiceOptions.length - 1;
|
|
2131
|
-
actionSystem.setActionState(prev => ({
|
|
2132
|
-
...prev,
|
|
2133
|
-
choiceSelectedIndex: Math.min(maxIndex, prev.choiceSelectedIndex + 1)
|
|
2134
|
-
}));
|
|
2135
|
-
return;
|
|
2136
|
-
}
|
|
2137
|
-
else if (key.return) {
|
|
2138
|
-
const selectedOption = actionSystem.actionState.choiceOptions[actionSystem.actionState.choiceSelectedIndex];
|
|
2139
|
-
if (selectedOption && actionSystem.actionState.onChoiceSelect) {
|
|
2140
|
-
actionSystem.executeCallback(async () => actionSystem.actionState.onChoiceSelect(selectedOption.id));
|
|
2141
|
-
}
|
|
2853
|
+
setQuitConfirmMode(false);
|
|
2142
2854
|
return;
|
|
2143
2855
|
}
|
|
2144
|
-
return
|
|
2856
|
+
// Allow other inputs to continue (don't return early)
|
|
2145
2857
|
}
|
|
2146
2858
|
if (showFileCopyPrompt) {
|
|
2147
|
-
if (input ===
|
|
2859
|
+
if (input === "y" || input === "Y") {
|
|
2148
2860
|
setShowFileCopyPrompt(false);
|
|
2149
2861
|
const selectedPane = panes[selectedIndex];
|
|
2150
2862
|
if (selectedPane && selectedPane.worktreePath && currentCommandType) {
|
|
@@ -2152,7 +2864,7 @@ const DmuxApp = ({ panesFile, projectName, sessionName, settingsFile, projectRoo
|
|
|
2152
2864
|
// Mark as not first run and continue with command
|
|
2153
2865
|
const newSettings = {
|
|
2154
2866
|
...projectSettings,
|
|
2155
|
-
[currentCommandType ===
|
|
2867
|
+
[currentCommandType === "test" ? "firstTestRun" : "firstDevRun"]: true,
|
|
2156
2868
|
};
|
|
2157
2869
|
await saveSettings(newSettings);
|
|
2158
2870
|
// Now run the actual command
|
|
@@ -2160,14 +2872,14 @@ const DmuxApp = ({ panesFile, projectName, sessionName, settingsFile, projectRoo
|
|
|
2160
2872
|
}
|
|
2161
2873
|
setCurrentCommandType(null);
|
|
2162
2874
|
}
|
|
2163
|
-
else if (input ===
|
|
2875
|
+
else if (input === "n" || input === "N" || key.escape) {
|
|
2164
2876
|
setShowFileCopyPrompt(false);
|
|
2165
2877
|
const selectedPane = panes[selectedIndex];
|
|
2166
2878
|
if (selectedPane && currentCommandType) {
|
|
2167
2879
|
// Mark as not first run and continue without copying
|
|
2168
2880
|
const newSettings = {
|
|
2169
2881
|
...projectSettings,
|
|
2170
|
-
[currentCommandType ===
|
|
2882
|
+
[currentCommandType === "test" ? "firstTestRun" : "firstDevRun"]: true,
|
|
2171
2883
|
};
|
|
2172
2884
|
await saveSettings(newSettings);
|
|
2173
2885
|
// Now run the actual command
|
|
@@ -2177,36 +2889,13 @@ const DmuxApp = ({ panesFile, projectName, sessionName, settingsFile, projectRoo
|
|
|
2177
2889
|
}
|
|
2178
2890
|
return;
|
|
2179
2891
|
}
|
|
2180
|
-
if (showAgentChoiceDialog) {
|
|
2181
|
-
if (key.escape) {
|
|
2182
|
-
setShowAgentChoiceDialog(false);
|
|
2183
|
-
setShowNewPaneDialog(true);
|
|
2184
|
-
setNewPanePrompt(pendingPrompt);
|
|
2185
|
-
setPendingPrompt('');
|
|
2186
|
-
}
|
|
2187
|
-
else if (key.leftArrow || input === '1' || (input && input.toLowerCase() === 'c')) {
|
|
2188
|
-
setAgentChoice('claude');
|
|
2189
|
-
}
|
|
2190
|
-
else if (key.rightArrow || input === '2' || (input && input.toLowerCase() === 'o')) {
|
|
2191
|
-
setAgentChoice('opencode');
|
|
2192
|
-
}
|
|
2193
|
-
else if (key.return) {
|
|
2194
|
-
const chosen = agentChoice || (availableAgents[0] || 'claude');
|
|
2195
|
-
const promptValue = pendingPrompt;
|
|
2196
|
-
setShowAgentChoiceDialog(false);
|
|
2197
|
-
setPendingPrompt('');
|
|
2198
|
-
await createNewPaneHook(promptValue, chosen);
|
|
2199
|
-
setNewPanePrompt('');
|
|
2200
|
-
}
|
|
2201
|
-
return;
|
|
2202
|
-
}
|
|
2203
2892
|
if (showCommandPrompt) {
|
|
2204
2893
|
if (key.escape) {
|
|
2205
2894
|
setShowCommandPrompt(null);
|
|
2206
|
-
setCommandInput(
|
|
2895
|
+
setCommandInput("");
|
|
2207
2896
|
}
|
|
2208
2897
|
else if (key.return) {
|
|
2209
|
-
if (commandInput.trim() ===
|
|
2898
|
+
if (commandInput.trim() === "") {
|
|
2210
2899
|
// If empty, suggest a default command based on package manager
|
|
2211
2900
|
const suggested = await suggestCommand(showCommandPrompt);
|
|
2212
2901
|
if (suggested) {
|
|
@@ -2217,13 +2906,15 @@ const DmuxApp = ({ panesFile, projectName, sessionName, settingsFile, projectRoo
|
|
|
2217
2906
|
// User provided manual command
|
|
2218
2907
|
const newSettings = {
|
|
2219
2908
|
...projectSettings,
|
|
2220
|
-
[showCommandPrompt ===
|
|
2909
|
+
[showCommandPrompt === "test" ? "testCommand" : "devCommand"]: commandInput.trim(),
|
|
2221
2910
|
};
|
|
2222
2911
|
await saveSettings(newSettings);
|
|
2223
2912
|
const selectedPane = panes[selectedIndex];
|
|
2224
2913
|
if (selectedPane) {
|
|
2225
2914
|
// Check if first run
|
|
2226
|
-
const isFirstRun = showCommandPrompt ===
|
|
2915
|
+
const isFirstRun = showCommandPrompt === "test"
|
|
2916
|
+
? !projectSettings.firstTestRun
|
|
2917
|
+
: !projectSettings.firstDevRun;
|
|
2227
2918
|
if (isFirstRun) {
|
|
2228
2919
|
setCurrentCommandType(showCommandPrompt);
|
|
2229
2920
|
setShowCommandPrompt(null);
|
|
@@ -2232,173 +2923,210 @@ const DmuxApp = ({ panesFile, projectName, sessionName, settingsFile, projectRoo
|
|
|
2232
2923
|
else {
|
|
2233
2924
|
await runCommandInternal(showCommandPrompt, selectedPane);
|
|
2234
2925
|
setShowCommandPrompt(null);
|
|
2235
|
-
setCommandInput(
|
|
2926
|
+
setCommandInput("");
|
|
2236
2927
|
}
|
|
2237
2928
|
}
|
|
2238
2929
|
else {
|
|
2239
2930
|
setShowCommandPrompt(null);
|
|
2240
|
-
setCommandInput(
|
|
2931
|
+
setCommandInput("");
|
|
2241
2932
|
}
|
|
2242
2933
|
}
|
|
2243
2934
|
}
|
|
2244
2935
|
return;
|
|
2245
2936
|
}
|
|
2246
|
-
if (showNewPaneDialog) {
|
|
2247
|
-
if (key.escape) {
|
|
2248
|
-
setShowNewPaneDialog(false);
|
|
2249
|
-
setNewPanePrompt('');
|
|
2250
|
-
}
|
|
2251
|
-
// TextInput handles other input events
|
|
2252
|
-
return;
|
|
2253
|
-
}
|
|
2254
2937
|
// Handle directional navigation with spatial awareness based on card grid layout
|
|
2255
2938
|
if (key.upArrow || key.downArrow || key.leftArrow || key.rightArrow) {
|
|
2256
2939
|
let targetIndex = null;
|
|
2257
2940
|
if (key.upArrow) {
|
|
2258
|
-
targetIndex = findCardInDirection(selectedIndex,
|
|
2941
|
+
targetIndex = findCardInDirection(selectedIndex, "up");
|
|
2259
2942
|
}
|
|
2260
2943
|
else if (key.downArrow) {
|
|
2261
|
-
targetIndex = findCardInDirection(selectedIndex,
|
|
2944
|
+
targetIndex = findCardInDirection(selectedIndex, "down");
|
|
2262
2945
|
}
|
|
2263
2946
|
else if (key.leftArrow) {
|
|
2264
|
-
targetIndex = findCardInDirection(selectedIndex,
|
|
2947
|
+
targetIndex = findCardInDirection(selectedIndex, "left");
|
|
2265
2948
|
}
|
|
2266
2949
|
else if (key.rightArrow) {
|
|
2267
|
-
targetIndex = findCardInDirection(selectedIndex,
|
|
2950
|
+
targetIndex = findCardInDirection(selectedIndex, "right");
|
|
2268
2951
|
}
|
|
2269
2952
|
if (targetIndex !== null) {
|
|
2270
2953
|
setSelectedIndex(targetIndex);
|
|
2271
2954
|
}
|
|
2272
2955
|
return;
|
|
2273
2956
|
}
|
|
2274
|
-
if (
|
|
2275
|
-
// Open kebab menu for selected pane
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2957
|
+
if (input === "m" && selectedIndex < panes.length) {
|
|
2958
|
+
// Open kebab menu popup for selected pane
|
|
2959
|
+
await launchKebabMenuPopup(selectedIndex);
|
|
2960
|
+
}
|
|
2961
|
+
else if (input === "s") {
|
|
2962
|
+
// Open settings popup
|
|
2963
|
+
await launchSettingsPopup();
|
|
2964
|
+
}
|
|
2965
|
+
else if (input === "l") {
|
|
2966
|
+
// Open logs popup
|
|
2967
|
+
await launchLogsPopup();
|
|
2968
|
+
}
|
|
2969
|
+
else if (input === "?") {
|
|
2970
|
+
// Open keyboard shortcuts popup
|
|
2971
|
+
await launchShortcutsPopup();
|
|
2972
|
+
}
|
|
2973
|
+
else if (input === "L" && controlPaneId) {
|
|
2974
|
+
// Reset layout to sidebar configuration (Shift+L)
|
|
2975
|
+
enforceControlPaneSize(controlPaneId, SIDEBAR_WIDTH);
|
|
2976
|
+
setStatusMessage("Layout reset");
|
|
2977
|
+
setTimeout(() => setStatusMessage(""), 2000);
|
|
2978
|
+
}
|
|
2979
|
+
else if (input === "q") {
|
|
2290
2980
|
cleanExit();
|
|
2291
2981
|
}
|
|
2292
|
-
else if (input ===
|
|
2293
|
-
//
|
|
2294
|
-
if (
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
try {
|
|
2298
|
-
const url = await server.startTunnel();
|
|
2299
|
-
setTunnelUrl(url);
|
|
2300
|
-
setStatusMessage('');
|
|
2301
|
-
setShowQRCode(true);
|
|
2302
|
-
}
|
|
2303
|
-
catch (error) {
|
|
2304
|
-
setStatusMessage('Failed to create tunnel');
|
|
2305
|
-
setTimeout(() => setStatusMessage(''), 3000);
|
|
2306
|
-
}
|
|
2307
|
-
finally {
|
|
2308
|
-
setIsCreatingTunnel(false);
|
|
2309
|
-
}
|
|
2982
|
+
else if (input === "r" && server) {
|
|
2983
|
+
// Handle remote tunnel
|
|
2984
|
+
if (tunnelUrl) {
|
|
2985
|
+
// Tunnel exists - open popup with QR code
|
|
2986
|
+
await launchRemotePopup();
|
|
2310
2987
|
}
|
|
2311
|
-
else {
|
|
2312
|
-
//
|
|
2313
|
-
|
|
2988
|
+
else if (!tunnelCreating) {
|
|
2989
|
+
// Start tunnel creation
|
|
2990
|
+
setTunnelCreating(true);
|
|
2991
|
+
(async () => {
|
|
2992
|
+
try {
|
|
2993
|
+
const url = await server.startTunnel();
|
|
2994
|
+
setTunnelUrl(url);
|
|
2995
|
+
}
|
|
2996
|
+
catch (error) {
|
|
2997
|
+
setStatusMessage(\`Failed to create tunnel: \${error.message}\`);
|
|
2998
|
+
setTimeout(() => setStatusMessage(""), 3000);
|
|
2999
|
+
}
|
|
3000
|
+
finally {
|
|
3001
|
+
setTunnelCreating(false);
|
|
3002
|
+
}
|
|
3003
|
+
})();
|
|
2314
3004
|
}
|
|
3005
|
+
// If tunnelCreating is true, do nothing (already creating)
|
|
3006
|
+
return;
|
|
2315
3007
|
}
|
|
2316
|
-
else if (!isLoading &&
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
return;
|
|
3008
|
+
else if (!isLoading &&
|
|
3009
|
+
(input === "n" || (key.return && selectedIndex === panes.length))) {
|
|
3010
|
+
// Launch popup modal for new pane
|
|
3011
|
+
await launchNewPanePopup();
|
|
3012
|
+
return;
|
|
3013
|
+
}
|
|
3014
|
+
else if (!isLoading &&
|
|
3015
|
+
(input === "t" || (key.return && selectedIndex === panes.length + 1))) {
|
|
3016
|
+
// Create a new terminal pane without an agent
|
|
3017
|
+
try {
|
|
3018
|
+
setIsCreatingPane(true);
|
|
3019
|
+
setStatusMessage("Creating terminal pane...");
|
|
3020
|
+
// Create a simple tmux pane split
|
|
3021
|
+
const paneInfo = execSync(\`tmux split-window -h -P -F '#{pane_id}'\`, {
|
|
3022
|
+
encoding: "utf-8",
|
|
3023
|
+
}).trim();
|
|
3024
|
+
// Wait for pane creation to settle
|
|
3025
|
+
await new Promise((resolve) => setTimeout(resolve, 300));
|
|
3026
|
+
// The shell pane will be automatically detected by the shell pane detection system
|
|
3027
|
+
// No need to manually add it to the panes array
|
|
3028
|
+
setIsCreatingPane(false);
|
|
3029
|
+
setStatusMessage("Terminal pane created");
|
|
3030
|
+
setTimeout(() => setStatusMessage(""), 2000);
|
|
3031
|
+
// Force a reload to pick up the new shell pane
|
|
3032
|
+
await loadPanes();
|
|
3033
|
+
}
|
|
3034
|
+
catch (error) {
|
|
3035
|
+
setIsCreatingPane(false);
|
|
3036
|
+
setStatusMessage(\`Failed to create terminal pane: \${error.message}\`);
|
|
3037
|
+
setTimeout(() => setStatusMessage(""), 3000);
|
|
3038
|
+
}
|
|
3039
|
+
return;
|
|
2321
3040
|
}
|
|
2322
|
-
else if (input ===
|
|
3041
|
+
else if (input === "j" && selectedIndex < panes.length) {
|
|
2323
3042
|
// Jump to pane (NEW: using action system)
|
|
2324
3043
|
StateManager.getInstance().setDebugMessage(\`Jumping to pane: \${panes[selectedIndex].slug}\`);
|
|
2325
|
-
setTimeout(() => StateManager.getInstance().setDebugMessage(
|
|
3044
|
+
setTimeout(() => StateManager.getInstance().setDebugMessage(""), 2000);
|
|
2326
3045
|
actionSystem.executeAction(PaneAction.VIEW, panes[selectedIndex]);
|
|
2327
3046
|
}
|
|
2328
|
-
else if (input ===
|
|
3047
|
+
else if (input === "x" && selectedIndex < panes.length) {
|
|
2329
3048
|
// Close pane (NEW: using action system)
|
|
2330
3049
|
StateManager.getInstance().setDebugMessage(\`Closing pane: \${panes[selectedIndex].slug}\`);
|
|
2331
|
-
setTimeout(() => StateManager.getInstance().setDebugMessage(
|
|
3050
|
+
setTimeout(() => StateManager.getInstance().setDebugMessage(""), 2000);
|
|
2332
3051
|
actionSystem.executeAction(PaneAction.CLOSE, panes[selectedIndex]);
|
|
2333
3052
|
}
|
|
2334
3053
|
else if (key.return && selectedIndex < panes.length) {
|
|
2335
3054
|
// Jump to pane (NEW: using action system)
|
|
2336
3055
|
StateManager.getInstance().setDebugMessage(\`Jumping to pane: \${panes[selectedIndex].slug}\`);
|
|
2337
|
-
setTimeout(() => StateManager.getInstance().setDebugMessage(
|
|
3056
|
+
setTimeout(() => StateManager.getInstance().setDebugMessage(""), 2000);
|
|
2338
3057
|
actionSystem.executeAction(PaneAction.VIEW, panes[selectedIndex]);
|
|
2339
3058
|
}
|
|
2340
3059
|
});
|
|
2341
|
-
//
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
3060
|
+
// Calculate available height for content (terminal height - footer lines - active status messages)
|
|
3061
|
+
// Footer height varies based on state:
|
|
3062
|
+
// - Quit confirm mode: 2 lines (marginTop + 1 text line)
|
|
3063
|
+
// - Normal mode calculation:
|
|
3064
|
+
// - Base: 4 lines (marginTop + logs divider + logs line + keyboard shortcuts)
|
|
3065
|
+
// - Network section: +4 lines (divider, local IP, remote tunnel, divider) if serverPort exists
|
|
3066
|
+
// - Debug info: +1 line if DEBUG_DMUX
|
|
3067
|
+
// - Status line: +1 line if updateAvailable/currentBranch/debugMessage
|
|
3068
|
+
// - Status messages: +1 line per active message
|
|
3069
|
+
let footerLines = 2;
|
|
3070
|
+
if (quitConfirmMode) {
|
|
3071
|
+
footerLines = 2;
|
|
2349
3072
|
}
|
|
2350
|
-
|
|
3073
|
+
else {
|
|
3074
|
+
// Base footer (logs divider + logs + shortcuts - always shown)
|
|
3075
|
+
footerLines = 4; // marginTop + logs divider + logs + shortcuts
|
|
3076
|
+
// Add network section (now 2 lines for local IP + remote tunnel, plus 2 dividers)
|
|
3077
|
+
if (serverPort && serverPort > 0) {
|
|
3078
|
+
footerLines += 4;
|
|
3079
|
+
}
|
|
3080
|
+
// Add debug info
|
|
3081
|
+
if (process.env.DEBUG_DMUX) {
|
|
3082
|
+
footerLines += 1;
|
|
3083
|
+
}
|
|
3084
|
+
// Add status line
|
|
3085
|
+
if (updateAvailable || currentBranch || debugMessage) {
|
|
3086
|
+
footerLines += 1;
|
|
3087
|
+
}
|
|
3088
|
+
// Add line for each active status message
|
|
3089
|
+
if (statusMessage) {
|
|
3090
|
+
footerLines += 1;
|
|
3091
|
+
}
|
|
3092
|
+
if (actionSystem.actionState.statusMessage) {
|
|
3093
|
+
footerLines += 1;
|
|
3094
|
+
}
|
|
3095
|
+
}
|
|
3096
|
+
const contentHeight = Math.max(terminalHeight - footerLines, 10);
|
|
3097
|
+
return (React.createElement(Box, { flexDirection: "column", height: terminalHeight },
|
|
2351
3098
|
showRepaintSpinner && (React.createElement(Box, { marginTop: -10, marginLeft: -100 },
|
|
2352
3099
|
React.createElement(Text, null, "\\u27F3"))),
|
|
2353
|
-
React.createElement(
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
createNewPaneHook(promptValue);
|
|
2362
|
-
}
|
|
2363
|
-
else if (agents.length === 1) {
|
|
2364
|
-
setShowNewPaneDialog(false);
|
|
2365
|
-
setNewPanePrompt('');
|
|
2366
|
-
createNewPaneHook(promptValue, agents[0]);
|
|
2367
|
-
}
|
|
2368
|
-
else {
|
|
2369
|
-
setPendingPrompt(promptValue);
|
|
2370
|
-
setShowNewPaneDialog(false);
|
|
2371
|
-
setNewPanePrompt('');
|
|
2372
|
-
setShowAgentChoiceDialog(true);
|
|
2373
|
-
setAgentChoice(agentChoice || 'claude');
|
|
2374
|
-
}
|
|
2375
|
-
} })),
|
|
2376
|
-
showAgentChoiceDialog && (React.createElement(AgentChoiceDialog, { agentChoice: agentChoice })),
|
|
2377
|
-
isCreatingPane && (React.createElement(CreatingIndicator, { message: statusMessage })),
|
|
2378
|
-
showCommandPrompt && (React.createElement(CommandPromptDialog, { type: showCommandPrompt, value: commandInput, onChange: setCommandInput })),
|
|
2379
|
-
showFileCopyPrompt && (React.createElement(FileCopyPrompt, null)),
|
|
2380
|
-
showKebabMenu && kebabMenuPaneIndex !== null && panes[kebabMenuPaneIndex] && (React.createElement(KebabMenu, { selectedOption: kebabMenuOption, actions: kebabMenuActions, paneName: panes[kebabMenuPaneIndex].slug })),
|
|
2381
|
-
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 })),
|
|
2382
|
-
showHooksDialog && (React.createElement(HooksDialog, { hooks: hooksData, selectedIndex: hooksSelectedIndex })),
|
|
2383
|
-
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 })),
|
|
2384
|
-
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 })),
|
|
2385
|
-
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) => {
|
|
2386
|
-
actionSystem.setActionState(prev => ({ ...prev, inputValue: value }));
|
|
2387
|
-
} })),
|
|
2388
|
-
actionSystem.actionState.showProgressDialog && (React.createElement(ActionProgressDialog, { key: "progress-dialog", message: actionSystem.actionState.progressMessage, percent: actionSystem.actionState.progressPercent })),
|
|
2389
|
-
runningCommand && (React.createElement(RunningIndicator, null)),
|
|
2390
|
-
isUpdating && (React.createElement(UpdatingIndicator, null)),
|
|
2391
|
-
isCreatingTunnel && (React.createElement(Box, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", padding: 1, marginTop: 1 },
|
|
2392
|
-
React.createElement(Text, { bold: true, color: "cyan" }, "Creating tunnel..."),
|
|
2393
|
-
React.createElement(Box, { marginTop: 1 },
|
|
2394
|
-
React.createElement(Text, { dimColor: true }, "This may take a few moments...")))),
|
|
2395
|
-
statusMessage && (React.createElement(Box, { marginTop: 1 },
|
|
3100
|
+
React.createElement(Box, { flexDirection: "column", height: contentHeight, overflow: "hidden" },
|
|
3101
|
+
React.createElement(PanesGrid, { panes: panes, selectedIndex: selectedIndex, isLoading: isLoading, agentStatuses: agentStatuses }),
|
|
3102
|
+
isLoading && React.createElement(LoadingIndicator, null),
|
|
3103
|
+
showCommandPrompt && (React.createElement(CommandPromptDialog, { type: showCommandPrompt, value: commandInput, onChange: setCommandInput })),
|
|
3104
|
+
showFileCopyPrompt && React.createElement(FileCopyPrompt, null),
|
|
3105
|
+
runningCommand && React.createElement(RunningIndicator, null),
|
|
3106
|
+
isUpdating && React.createElement(UpdatingIndicator, null)),
|
|
3107
|
+
statusMessage && (React.createElement(Box, null,
|
|
2396
3108
|
React.createElement(Text, { color: "green" }, statusMessage))),
|
|
2397
|
-
actionSystem.actionState.statusMessage && (React.createElement(Box,
|
|
2398
|
-
React.createElement(Text, { color: actionSystem.actionState.statusType ===
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
3109
|
+
actionSystem.actionState.statusMessage && (React.createElement(Box, null,
|
|
3110
|
+
React.createElement(Text, { color: actionSystem.actionState.statusType === "error"
|
|
3111
|
+
? "red"
|
|
3112
|
+
: actionSystem.actionState.statusType === "success"
|
|
3113
|
+
? "green"
|
|
3114
|
+
: "cyan" }, actionSystem.actionState.statusMessage))),
|
|
3115
|
+
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: (() => {
|
|
3116
|
+
const spinnerFrames = [
|
|
3117
|
+
"⠋",
|
|
3118
|
+
"⠙",
|
|
3119
|
+
"⠹",
|
|
3120
|
+
"⠸",
|
|
3121
|
+
"⠼",
|
|
3122
|
+
"⠴",
|
|
3123
|
+
"⠦",
|
|
3124
|
+
"⠧",
|
|
3125
|
+
"⠇",
|
|
3126
|
+
"⠏",
|
|
3127
|
+
];
|
|
3128
|
+
return spinnerFrames[tunnelSpinnerFrame];
|
|
3129
|
+
})(), gridInfo: (() => {
|
|
2402
3130
|
if (!process.env.DEBUG_DMUX)
|
|
2403
3131
|
return undefined;
|
|
2404
3132
|
const cols = Math.max(1, Math.floor(terminalWidth / 37));
|
|
@@ -2406,23 +3134,21 @@ const DmuxApp = ({ panesFile, projectName, sessionName, settingsFile, projectRoo
|
|
|
2406
3134
|
const pos = getCardGridPosition(selectedIndex);
|
|
2407
3135
|
return \`Grid: \${cols} cols × \${rows} rows | Selected: row \${pos.row}, col \${pos.col} | Terminal: \${terminalWidth}w\`;
|
|
2408
3136
|
})() }),
|
|
2409
|
-
React.createElement(Text, { dimColor: true },
|
|
2410
|
-
updateAvailable && updateInfo && (React.createElement(Text, { color: "red", bold: true },
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
"
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
serverPort))),
|
|
2418
|
-
debugMessage && (React.createElement(Text, { dimColor: true },
|
|
3137
|
+
(updateAvailable || currentBranch || debugMessage) && (React.createElement(Text, { dimColor: true },
|
|
3138
|
+
updateAvailable && updateInfo && (React.createElement(Text, { color: "red", bold: true },
|
|
3139
|
+
"Update available: npm i -g dmux@latest",
|
|
3140
|
+
" ")),
|
|
3141
|
+
currentBranch && (React.createElement(Text, { color: "magenta", bold: true },
|
|
3142
|
+
"branch: ",
|
|
3143
|
+
currentBranch)),
|
|
3144
|
+
debugMessage && React.createElement(Text, { dimColor: true },
|
|
2419
3145
|
" \\u2022 ",
|
|
2420
3146
|
debugMessage)))));
|
|
2421
3147
|
};
|
|
2422
3148
|
export default DmuxApp;
|
|
2423
3149
|
//# sourceMappingURL=DmuxApp.js.map`,
|
|
2424
3150
|
mimeType: 'application/javascript',
|
|
2425
|
-
size:
|
|
3151
|
+
size: 94288
|
|
2426
3152
|
},
|
|
2427
3153
|
'EnhancedTextInput.js': {
|
|
2428
3154
|
content: `import React, { useState, useEffect } from 'react';
|
|
@@ -4787,26 +5513,174 @@ Expected function or array of functions, received type \${typeof e}.\`),ne)}func
|
|
|
4787
5513
|
},
|
|
4788
5514
|
'dashboard.js': {
|
|
4789
5515
|
content: `import{d as Mt,r as u,o as zt,a as Nt,n as I,_ as Ht,c as n,b as t,t as g,e as r,f as Ot,g as P,F as y,h as w,w as M,i as at,v as it,j as lt,k as a,l as z,m as Ft}from"./chunks/_plugin-vue_export-helper-Cvoq67hi.js";const Et=Mt({__name:"Dashboard",setup(vt,{expose:i}){i();const U=u("Loading..."),e=u(""),O=u(!1),F=u([]),o=u(null),p=u("Never"),h=u({}),f=u(new Set),J=u({}),x=u(localStorage.getItem("dmux-theme")||"dark"),A=u(new Set),b=u(new Set),K=u(!1),T=u(""),C=u(null),Q=u(!1),E=u([]),L=u(!1),j=u("prompt"),pt=u([]),W=u({}),S=u(null),k=u(null),R=u(!1),D=u(!1),Z=u(!1),V=u(null),G=u([]),N=u(!1),H=u(!1),q=u([]),X=u(!1);let _=null;const Y=()=>{_&&clearInterval(_),ot(),_=setInterval(()=>{ot()},2e3)},$=()=>{_&&(clearInterval(_),_=null)},ht=()=>{x.value=x.value==="dark"?"light":"dark",localStorage.setItem("dmux-theme",x.value),document.documentElement.setAttribute("data-theme",x.value)},mt=s=>{A.value.has(s)?A.value.delete(s):A.value.add(s),A.value=new Set(A.value)},yt=()=>{K.value=!0,T.value="",C.value=null,E.value=[],L.value=!1,j.value="prompt",I(()=>{const l=document.getElementById("pane-prompt");l&&l.focus()});const s=l=>{l.key==="Escape"&&(tt(),document.removeEventListener("keydown",s))};document.addEventListener("keydown",s)},tt=()=>{K.value=!1,T.value="",C.value=null,E.value=[],L.value=!1,j.value="prompt"},et=async()=>{if(!(j.value==="prompt"&&!T.value.trim())&&!(j.value==="agent"&&!C.value))try{Q.value=!0;const s={prompt:T.value.trim()};C.value&&(s.agent=C.value);const d=await(await fetch("/api/panes",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(s)})).json();d.needsAgentChoice?(L.value=!0,E.value=d.availableAgents,j.value="agent"):tt()}catch(s){console.error("Failed to create pane:",s),alert("Failed to create pane")}finally{Q.value=!1}},kt=s=>{C.value=s,et()},ct=s=>{U.value=s.projectName||"dmux",e.value=s.sessionName||"",O.value=!0,F.value=s.panes||[],o.value=new Date,p.value="Just now";for(const l of F.value)W.value[l.id]||dt(l.id)},ot=async()=>{try{const l=await(await fetch("/api/panes")).json();ct(l)}catch(s){console.error("Failed to fetch panes:",s),O.value=!1}},dt=async s=>{try{const d=await(await fetch(\`/api/panes/\${s}/actions\`)).json();W.value[s]=d.actions||[]}catch(l){console.error(\`Failed to fetch actions for pane \${s}:\`,l)}},ft=s=>{S.value===s?S.value=null:S.value=s},bt=async(s,l)=>{var d;try{R.value=!0,S.value=null;const v=await(await fetch(\`/api/panes/\${s.id}/actions/\${l.id}\`,{method:"POST"})).json();if(v.requiresInteraction){let m={};v.interactionType==="confirm"?m={type:"confirm",title:v.title||"Confirm",message:v.message,...v.confirmData}:v.interactionType==="choice"?m={type:"choice",title:v.title||"Choose",message:v.message,...v.choiceData}:v.interactionType==="input"&&(m={type:"input",title:v.title||"Input",message:v.message,...v.inputData,inputValue:((d=v.inputData)==null?void 0:d.defaultValue)||""},I(()=>{const nt=document.querySelector(".dialog-input");nt&&(nt.focus(),nt.select())})),m.paneId=s.id,k.value=m}}catch(c){console.error("Failed to execute action:",c),alert("Failed to execute action")}finally{R.value=!1}},B=()=>{k.value=null},_t=async s=>{var l;if(k.value)try{D.value=!0;const c=await(await fetch(\`/api/callbacks/confirm/\${k.value.callbackId}\`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({confirmed:s})})).json();if(c.requiresInteraction){let v={};c.interactionType==="confirm"?v={type:"confirm",title:c.title||"Confirm",message:c.message,...c.confirmData}:c.interactionType==="choice"?v={type:"choice",title:c.title||"Choose",message:c.message,...c.choiceData}:c.interactionType==="input"&&(v={type:"input",title:c.title||"Input",message:c.message,...c.inputData,inputValue:((l=c.inputData)==null?void 0:l.defaultValue)||""},I(()=>{const m=document.querySelector(".dialog-input");m&&(m.focus(),m.select())})),k.value=v}else B()}catch(d){console.error("Failed to confirm action:",d),alert("Failed to complete action")}finally{D.value=!1}},wt=async s=>{var l;if(k.value)try{D.value=!0;const c=await(await fetch(\`/api/callbacks/choice/\${k.value.callbackId}\`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({optionId:s})})).json();if(c.requiresInteraction){let v={};c.interactionType==="confirm"?v={type:"confirm",title:c.title||"Confirm",message:c.message,...c.confirmData}:c.interactionType==="choice"?v={type:"choice",title:c.title||"Choose",message:c.message,...c.choiceData}:c.interactionType==="input"&&(v={type:"input",title:c.title||"Input",message:c.message,...c.inputData,inputValue:((l=c.inputData)==null?void 0:l.defaultValue)||""},I(()=>{const m=document.querySelector(".dialog-input");m&&(m.focus(),m.select())})),k.value=v}else B()}catch(d){console.error("Failed to select choice:",d),alert("Failed to complete action")}finally{D.value=!1}},Ct=s=>s.split(\`
|
|
4790
|
-
\`).map(l=>{if(l.includes("|")){const d=l.split("|");if(d.length===2){const c=d[0],m=d[1].replace(/\\+/g,'<span style="color: #4ade80;">+</span>').replace(/-/g,'<span style="color: #f87171;">-</span>');return c+'<span style="opacity: 0.6;">|</span>'+m}}return l}).join("<br>"),St=async()=>{var s;if(k.value)try{D.value=!0;const d=await(await fetch(\`/api/callbacks/input/\${k.value.callbackId}\`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({value:k.value.inputValue})})).json();if(d.requiresInteraction){let c={};d.interactionType==="confirm"?c={type:"confirm",title:d.title||"Confirm",message:d.message,...d.confirmData}:d.interactionType==="choice"?c={type:"choice",title:d.title||"Choose",message:d.message,...d.choiceData}:d.interactionType==="input"&&(c={type:"input",title:d.title||"Input",message:d.message,...d.inputData,inputValue:((s=d.inputData)==null?void 0:s.defaultValue)||""},I(()=>{const v=document.querySelector(".dialog-input");v&&(v.focus(),v.select())})),k.value=c}else B()}catch(l){console.error("Failed to submit input:",l),alert("Failed to complete action")}finally{D.value=!1}},Dt=async(s,l)=>{try{b.value.add(s.id),b.value=new Set(b.value);const d=l.keys||[l.action];for(const c of d)await fetch(\`/api/keys/\${s.id}\`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({key:c})});setTimeout(()=>{b.value.delete(s.id),b.value=new Set(b.value)},1500)}catch(d){console.error("Failed to select option:",d),b.value.delete(s.id),b.value=new Set(b.value)}},Pt=async s=>{const l=h.value[s.id];if(!(!l||!l.trim()))try{f.value.add(s.id),f.value=new Set(f.value);for(const d of l)await fetch(\`/api/keys/\${s.id}\`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({key:d})});await fetch(\`/api/keys/\${s.id}\`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({key:"Enter"})}),J.value[s.id]=l.substring(0,50)+(l.length>50?"...":""),h.value[s.id]="",setTimeout(()=>{delete J.value[s.id],f.value.delete(s.id),f.value=new Set(f.value)},3e3)}catch(d){console.error("Failed to send prompt:",d),f.value.delete(s.id),f.value=new Set(f.value)}},xt=s=>{const l=s.target;l.style.height="auto",l.style.height=l.scrollHeight+"px"},At=async()=>{try{N.value=!0;const l=await(await fetch("/api/settings")).json();V.value=l.settings,G.value=l.definitions,Z.value=!0,H.value=!1}catch(s){console.error("Failed to load settings:",s),alert("Failed to load settings")}finally{N.value=!1}},rt=()=>{Z.value=!1,V.value=null,G.value=[],H.value=!1,q.value=[]},Tt=async(s,l,d)=>{try{N.value=!0,await fetch("/api/settings",{method:"PATCH",headers:{"Content-Type":"application/json"},body:JSON.stringify({key:s,value:l,scope:d})});const v=await(await fetch("/api/settings")).json();V.value=v.settings}catch(c){console.error("Failed to update setting:",c),alert("Failed to update setting")}finally{N.value=!1}},jt=async()=>{try{X.value=!0;const l=await(await fetch("/api/hooks")).json();q.value=l.hooks||[],H.value=!0}catch(s){console.error("Failed to load hooks:",s),alert("Failed to load hooks")}finally{X.value=!1}},gt=()=>{H.value=!1,q.value=[]},It=async()=>{const s="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";gt(),rt(),T.value=s,await I(),await et()},st=s=>{const l=s.target;S.value&&!l.closest(".action-menu-btn")&&!l.closest(".action-menu-dropdown")&&(S.value=null)};zt(()=>{document.documentElement.setAttribute("data-theme",x.value),Y(),document.addEventListener("click",st),document.addEventListener("visibilitychange",()=>{document.hidden?$():Y()})}),Nt(()=>{$(),document.removeEventListener("click",st)});const ut={projectName:U,sessionName:e,connected:O,panes:F,lastUpdate:o,timeSinceUpdate:p,promptInputs:h,sendingPrompts:f,queuedMessages:J,theme:x,expandedPrompts:A,loadingOptions:b,showCreateDialog:K,newPanePrompt:T,newPaneAgent:C,creatingPane:Q,availableAgents:E,needsAgentChoice:L,createStep:j,actions:pt,paneActions:W,showActionMenu:S,actionDialog:k,executingAction:R,actionDialogLoading:D,showSettingsDialog:Z,settingsData:V,settingDefinitions:G,loadingSettings:N,showHooksSection:H,hooksData:q,loadingHooks:X,get pollingInterval(){return _},set pollingInterval(s){_=s},startPolling:Y,stopPolling:$,toggleTheme:ht,togglePrompt:mt,openCreateDialog:yt,closeCreateDialog:tt,createPane:et,selectAgent:kt,updatePanesFromData:ct,fetchPanes:ot,fetchPaneActions:dt,toggleActionMenu:ft,executeAction:bt,closeActionDialog:B,confirmAction:_t,selectChoice:wt,colorizeDiffStat:Ct,submitInput:St,selectOption:Dt,sendPrompt:Pt,autoExpand:xt,openSettingsDialog:At,closeSettingsDialog:rt,updateSetting:Tt,openHooksSection:jt,closeHooksSection:gt,editHooksWithAgent:It,handleClickOutside:st};return Object.defineProperty(ut,"__isScriptSetup",{enumerable:!1,value:!0}),ut}}),Lt={class:"session-info"},Vt=["title"],qt={key:0,xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24"},Bt={key:1,xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24"},Ut={key:0},Jt={class:"container"},Kt={class:"actions-bar"},Qt=["disabled"],Wt=["disabled"],Rt={key:0,class:"no-panes"},Zt={key:1,class:"panes-grid"},Gt={class:"pane-header"},Xt={class:"pane-header-content"},Yt=["href"],$t={class:"pane-title"},te={class:"pane-meta"},ee={key:0,class:"pane-autopilot",title:"Autopilot enabled"},oe={class:"pane-id"},se=["onClick"],ne={key:0,class:"action-menu-dropdown"},ae=["onClick","disabled"],ie={class:"action-icon"},le={class:"action-label"},ce={class:"pane-prompt-section"},de=["onClick"],re={class:"prompt-header"},ge={class:"expand-icon"},ue={class:"prompt-text"},ve={key:0,class:"pane-prompt-full"},pe={key:1,class:"agent-summary"},he={key:2,class:"analyzer-error"},me={key:0,class:"options-dialog"},ye={class:"options-question"},ke={key:0,class:"options-warning"},fe={key:1,class:"analyzing-state"},be={key:2,class:"options-buttons"},_e=["onClick","disabled"],we={class:"prompt-input-wrapper"},Ce=["onUpdate:modelValue","placeholder","disabled"],Se=["onClick","disabled","title"],De={key:0,class:"button-loader"},Pe={key:1,xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 988.44 1200.05"},xe={key:0,class:"queued-message"},Ae={key:3,class:"dev-server-status"},Te=["href"],je={class:"action-dialog"},Ie={key:0},Me=["onKeydown"],ze={class:"dialog-buttons"},Ne=["disabled"],He={key:1},Oe={class:"agent-choices"},Fe=["onClick","disabled"],Ee={key:0,class:"action-dialog"},Le={key:0,class:"dialog-loading"},Ve={key:1,class:"dialog-buttons"},qe=["disabled"],Be=["disabled"],Ue={class:"action-dialog"},Je={key:0},Ke={key:1,class:"dialog-loading"},Qe={key:2},We={class:"choice-options"},Re=["onClick","disabled"],Ze={class:"choice-label"},Ge={key:0,class:"choice-description"},Xe={class:"dialog-buttons"},Ye=["disabled"],$e={class:"action-dialog"},to=["innerHTML"],eo={key:1,class:"dialog-loading"},oo={key:2},so=["placeholder"],no={class:"dialog-buttons"},ao=["disabled"],io=["disabled"],lo={class:"action-dialog settings-dialog"},co={key:0,class:"dialog-loading"},ro={key:1,class:"settings-list"},go={class:"setting-header"},uo={class:"setting-info"},vo={class:"setting-label"},po={class:"setting-description"},ho={key:0,class:"setting-control"},mo={class:"setting-value"},yo={key:0,class:"setting-scope"},ko={key:1,class:"setting-scope"},fo={class:"setting-buttons"},bo=["onClick","disabled"],_o=["onClick","disabled"],wo=["onClick","disabled"],Co=["onClick","disabled"],So={class:"setting-control"},Do={class:"setting-value"},Po={key:0,class:"setting-scope"},xo={key:1,class:"setting-scope"},Ao={class:"setting-option-label"},To={class:"setting-buttons"},jo=["onClick","disabled"],Io=["onClick","disabled"],Mo={class:"setting-control"},zo={class:"setting-buttons"},No=["disabled"],Ho={key:2,class:"hooks-section"},Oo={key:0,class:"dialog-loading"},Fo={key:1,class:"hooks-list"},Eo={class:"hook-name"};function Lo(vt,i,U,e,O,F){return a(),n(y,null,[t("header",null,[i[7]||(i[7]=t("img",{src:"https://cdn.formk.it/dmux/dmux.png",alt:"dmux",class:"logo"},null,-1)),t("h1",null,g(e.projectName),1),t("div",Lt,[t("button",{onClick:e.toggleTheme,class:"theme-toggle",title:e.theme==="dark"?"Switch to light mode":"Switch to dark mode"},[e.theme==="dark"?(a(),n("svg",qt,[...i[5]||(i[5]=[t("path",{d:"M12 2.25a.75.75 0 01.75.75v2.25a.75.75 0 01-1.5 0V3a.75.75 0 01.75-.75zM7.5 12a4.5 4.5 0 119 0 4.5 4.5 0 01-9 0zM18.894 6.166a.75.75 0 00-1.06-1.06l-1.591 1.59a.75.75 0 101.06 1.061l1.591-1.59zM21.75 12a.75.75 0 01-.75.75h-2.25a.75.75 0 010-1.5H21a.75.75 0 01.75.75zM17.834 18.894a.75.75 0 001.06-1.06l-1.59-1.591a.75.75 0 10-1.061 1.06l1.59 1.591zM12 18a.75.75 0 01.75.75V21a.75.75 0 01-1.5 0v-2.25A.75.75 0 0112 18zM7.758 17.303a.75.75 0 00-1.061-1.06l-1.591 1.59a.75.75 0 001.06 1.061l1.591-1.59zM6 12a.75.75 0 01-.75.75H3a.75.75 0 010-1.5h2.25A.75.75 0 016 12zM6.697 7.757a.75.75 0 001.06-1.06l-1.59-1.591a.75.75 0 00-1.061 1.06l1.59 1.591z"},null,-1)])])):(a(),n("svg",Bt,[...i[6]||(i[6]=[t("path",{d:"M9.528 1.718a.75.75 0 01.162.819A8.97 8.97 0 009 6.5a9 9 0 009 9 8.97 8.97 0 003.963-.69.75.75 0 01.981.98 10.503 10.503 0 01-9.694 6.46c-5.799 0-10.5-4.701-10.5-10.5 0-4.368 2.667-8.112 6.46-9.694a.75.75 0 01.818.162z"},null,-1)])]))],8,Vt),e.sessionName?(a(),n("span",Ut,g(e.sessionName),1)):r("v-if",!0),t("span",{class:"status-indicator",style:Ot({color:e.connected?"#4ade80":"#f87171"})},"●",4)])]),t("div",Jt,[t("main",null,[t("div",Kt,[t("button",{onClick:e.openCreateDialog,class:"create-pane-button",disabled:e.creatingPane},[...i[8]||(i[8]=[t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",fill:"currentColor"},[t("path",{d:"M12 4.5v15m7.5-7.5h-15",stroke:"currentColor","stroke-width":"2","stroke-linecap":"round","stroke-linejoin":"round"})],-1),P(" Create New Pane ",-1)])],8,Qt),t("button",{onClick:e.openSettingsDialog,class:"settings-button",disabled:e.loadingSettings,title:"Settings"},[...i[9]||(i[9]=[t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",fill:"currentColor"},[t("path",{d:"M10.5 1.875a1.125 1.125 0 012.25 0v.563c0 1.018.84 1.843 1.854 1.839a.75.75 0 01.585.217l.4.4a.75.75 0 01.216.585c-.004 1.014.821 1.854 1.839 1.854h.563a1.125 1.125 0 010 2.25h-.563c-1.018 0-1.843.84-1.839 1.854a.75.75 0 01-.217.585l-.4.4a.75.75 0 01-.585.216c-1.014-.004-1.854.821-1.854 1.839v.563a1.125 1.125 0 01-2.25 0v-.563c0-1.018-.84-1.843-1.854-1.839a.75.75 0 01-.585-.217l-.4-.4a.75.75 0 01-.216-.585c.004-1.014-.821-1.854-1.839-1.854H3.75a1.125 1.125 0 010-2.25h.563c1.018 0 1.843-.84 1.839-1.854a.75.75 0 01.217-.585l.4-.4a.75.75 0 01.585-.216c1.014.004 1.854-.821 1.854-1.839V1.875zM12 15a3 3 0 100-6 3 3 0 000 6z"})],-1),P(" Settings ",-1)])],8,Wt)]),e.panes.length===0?(a(),n("div",Rt,[...i[10]||(i[10]=[t("p",null,"No dmux panes active",-1),t("p",{class:"hint"},\`Click "Create New Pane" above or press 'n' in dmux\`,-1)])])):(a(),n("div",Zt,[(a(!0),n(y,null,w(e.panes,o=>(a(),n("div",{key:o.id,class:"pane-card"},[t("div",Gt,[t("div",Xt,[t("a",{href:"/panes/"+o.id,class:"pane-title-link"},[t("span",$t,g(o.slug),1),i[11]||(i[11]=t("span",{class:"pane-arrow"},"→",-1))],8,Yt),t("div",te,[o.autopilot?(a(),n("span",ee,"🤖")):r("v-if",!0),t("span",{class:z(["pane-agent",o.agent||""])},g(o.agent||"unknown"),3),t("span",oe,g(o.paneId),1)])]),t("button",{onClick:p=>e.toggleActionMenu(o.id),class:"action-menu-btn",title:"Actions"},[...i[12]||(i[12]=[t("span",null,"⋮",-1)])],8,se)]),r(" Action Menu Dropdown "),e.showActionMenu===o.id&&e.paneActions[o.id]?(a(),n("div",ne,[(a(!0),n(y,null,w(e.paneActions[o.id],p=>(a(),n("button",{key:p.id,onClick:h=>e.executeAction(o,p),class:"action-menu-item",disabled:e.executingAction},[t("span",ie,g(p.icon||"•"),1),t("span",le,g(p.label),1)],8,ae))),128))])):r("v-if",!0),t("div",ce,[t("div",{class:z(["pane-prompt-preview",{expanded:e.expandedPrompts.has(o.id)}]),onClick:p=>e.togglePrompt(o.id)},[t("div",re,[i[13]||(i[13]=t("span",{class:"prompt-label"},"Initial Prompt",-1)),t("span",ge,g(e.expandedPrompts.has(o.id)?"▼":"▶"),1)]),t("span",ue,g(o.prompt||"No prompt"),1)],10,de),e.expandedPrompts.has(o.id)?(a(),n("div",ve,g(o.prompt||"No prompt"),1)):r("v-if",!0)]),r(" Show agent summary when idle "),o.agentStatus==="idle"&&o.agentSummary?(a(),n("div",pe,g(o.agentSummary),1)):r("v-if",!0),r(" Show analyzer error if present "),o.analyzerError?(a(),n("div",he," ⚠ "+g(o.analyzerError),1)):r("v-if",!0),t("div",{class:"pane-interactive",onClick:i[0]||(i[0]=M(()=>{},["prevent"]))},[r(" Options Dialog (when waiting with options) "),o.agentStatus==="waiting"&&o.options&&o.options.length>0?(a(),n("div",me,[t("div",ye,g(o.optionsQuestion||"Choose an option:"),1),o.potentialHarm&&o.potentialHarm.hasRisk?(a(),n("div",ke," ⚠️ "+g(o.potentialHarm.description),1)):r("v-if",!0),e.loadingOptions.has(o.id)?(a(),n("div",fe,[...i[14]||(i[14]=[t("div",{class:"loader-spinner"},null,-1),t("span",null,"Processing selection...",-1)])])):(a(),n("div",be,[(a(!0),n(y,null,w(o.options,p=>(a(),n("button",{key:p.action,onClick:h=>e.selectOption(o,p),class:z(["option-button",{"option-button-danger":o.potentialHarm&&o.potentialHarm.hasRisk}]),disabled:e.loadingOptions.has(o.id)},g(p.action),11,_e))),128))]))])):o.agentStatus==="analyzing"?(a(),n(y,{key:1},[r(" Analyzing (show loader) "),i[15]||(i[15]=t("div",{class:"analyzing-state"},[t("div",{class:"loader-spinner"}),t("span",null,"Analyzing...")],-1))],2112)):(a(),n(y,{key:2},[r(" Working/Idle (show prompt input) "),t("div",null,[t("div",we,[at(t("textarea",{"onUpdate:modelValue":p=>e.promptInputs[o.id]=p,onInput:e.autoExpand,placeholder:o.agentStatus==="working"?"Queue a prompt...":"Send a prompt...",disabled:e.sendingPrompts.has(o.id),class:"prompt-textarea",rows:"1"},null,40,Ce),[[it,e.promptInputs[o.id]]]),t("button",{onClick:p=>e.sendPrompt(o),disabled:!e.promptInputs[o.id]||e.sendingPrompts.has(o.id),class:"send-button",title:o.agentStatus==="working"?"Queue prompt":"Send prompt"},[e.sendingPrompts.has(o.id)?(a(),n("span",De)):(a(),n("svg",Pe,[...i[16]||(i[16]=[t("path",{d:"M425.13,28.37L30.09,423.41C11.19,441.37.34,466.2,0,492.27c-.34,26.07,9.86,51.17,28.29,69.61,18.43,18.45,43.52,28.67,69.59,28.35,26.07-.31,50.91-11.14,68.88-30.02l233.16-233.52v776.64c0,34.56,18.43,66.48,48.36,83.76,29.93,17.28,66.8,17.28,96.72,0,29.93-17.28,48.36-49.21,48.36-83.76V328.85l231.72,231.36c24.63,23.41,59.74,32.18,92.48,23.09,32.74-9.08,58.32-34.68,67.38-67.43,9.05-32.75.25-67.85-23.18-92.46L566.73,28.37C548.63,10.16,524-.04,498.33.05c-.8-.06-1.6-.06-2.4,0-.8-.06-1.6-.06-2.4,0-25.65,0-50.25,10.19-68.4,28.32h0Z"},null,-1)])]))],8,Se)]),e.queuedMessages[o.id]?(a(),n("div",xe," ✓ "+g(e.queuedMessages[o.id]),1)):r("v-if",!0)])],2112))]),o.devStatus&&o.devStatus!=="stopped"?(a(),n("div",Ae,[i[17]||(i[17]=t("span",{class:"status-label"},"Dev Server:",-1)),t("span",{class:z(["status-badge",o.devStatus])},g(o.devStatus),3),o.devUrl?(a(),n("a",{key:0,href:o.devUrl,target:"_blank",class:"dev-link"},"↗",8,Te)):r("v-if",!0)])):r("v-if",!0)]))),128))]))]),r(" Create Pane Dialog "),e.showCreateDialog?(a(),n("div",{key:0,class:"action-dialog-overlay",onClick:M(e.closeCreateDialog,["self"])},[t("div",je,[i[21]||(i[21]=t("h3",null,"Create New Pane",-1)),e.createStep==="prompt"?(a(),n("div",Ie,[i[18]||(i[18]=t("label",{for:"pane-prompt"},"Provide an initial prompt for your agent",-1)),at(t("textarea",{id:"pane-prompt","onUpdate:modelValue":i[1]||(i[1]=o=>e.newPanePrompt=o),placeholder:"E.g., Fix the authentication bug, Add dark mode, etc.",rows:"4",onKeydown:[lt(M(e.createPane,["meta"]),["enter"]),lt(M(e.createPane,["ctrl"]),["enter"])]},null,40,Me),[[it,e.newPanePrompt]]),i[19]||(i[19]=t("div",{class:"dialog-hint"},[P(" 💡 Press "),t("kbd",null,"⌘ Enter"),P(" or "),t("kbd",null,"Ctrl Enter"),P(" to create ")],-1)),t("div",ze,[t("button",{onClick:e.closeCreateDialog,class:"dialog-btn"},"Cancel"),t("button",{onClick:e.createPane,disabled:!e.newPanePrompt.trim()||e.creatingPane,class:"dialog-btn dialog-btn-primary"},g(e.creatingPane?"Creating...":"Create Pane"),9,Ne)])])):e.createStep==="agent"?(a(),n("div",He,[i[20]||(i[20]=t("p",null,"Multiple agents available. Choose one:",-1)),t("div",Oe,[(a(!0),n(y,null,w(e.availableAgents,o=>(a(),n("button",{key:o,onClick:p=>e.selectAgent(o),class:"agent-choice-button",disabled:e.creatingPane},g(o),9,Fe))),128))]),t("div",{class:"dialog-buttons"},[t("button",{onClick:e.closeCreateDialog,class:"dialog-btn"},"Cancel")])])):r("v-if",!0)])])):r("v-if",!0),r(" Action Dialogs "),e.actionDialog?(a(),n("div",{key:1,class:"action-dialog-overlay",onClick:M(e.closeActionDialog,["self"])},[r(" Confirm Dialog "),e.actionDialog.type==="confirm"?(a(),n("div",Ee,[t("h3",null,g(e.actionDialog.title),1),t("p",null,g(e.actionDialog.message),1),e.actionDialogLoading?(a(),n("div",Le,[...i[22]||(i[22]=[t("div",{class:"loader-spinner"},null,-1),t("span",null,"Processing...",-1)])])):(a(),n("div",Ve,[t("button",{onClick:i[2]||(i[2]=o=>e.confirmAction(!1)),class:"dialog-btn",disabled:e.actionDialogLoading},"Cancel",8,qe),t("button",{onClick:i[3]||(i[3]=o=>e.confirmAction(!0)),class:"dialog-btn dialog-btn-primary",disabled:e.actionDialogLoading},"Confirm",8,Be)]))])):e.actionDialog.type==="choice"?(a(),n(y,{key:1},[r(" Choice Dialog "),t("div",Ue,[t("h3",null,g(e.actionDialog.title),1),e.actionDialog.message?(a(),n("p",Je,g(e.actionDialog.message),1)):r("v-if",!0),e.actionDialogLoading?(a(),n("div",Ke,[...i[23]||(i[23]=[t("div",{class:"loader-spinner"},null,-1),t("span",null,"Processing...",-1)])])):(a(),n("div",Qe,[t("div",We,[(a(!0),n(y,null,w(e.actionDialog.options,o=>(a(),n("button",{key:o.id,onClick:p=>e.selectChoice(o.id),class:z(["choice-option-btn",{danger:o.danger}]),disabled:e.actionDialogLoading},[t("div",Ze,g(o.label),1),o.description?(a(),n("div",Ge,g(o.description),1)):r("v-if",!0)],10,Re))),128))]),t("div",Xe,[t("button",{onClick:e.closeActionDialog,class:"dialog-btn",disabled:e.actionDialogLoading},"Cancel",8,Ye)])]))])],2112)):e.actionDialog.type==="input"?(a(),n(y,{key:2},[r(" Input Dialog "),t("div",$e,[t("h3",null,g(e.actionDialog.title),1),e.actionDialog.message?(a(),n("div",{key:0,class:"dialog-message",innerHTML:e.colorizeDiffStat(e.actionDialog.message)},null,8,to)):r("v-if",!0),e.actionDialogLoading?(a(),n("div",eo,[...i[24]||(i[24]=[t("div",{class:"loader-spinner"},null,-1),t("span",null,"Processing...",-1)])])):(a(),n("div",oo,[at(t("input",{type:"text","onUpdate:modelValue":i[4]||(i[4]=o=>e.actionDialog.inputValue=o),placeholder:e.actionDialog.placeholder,class:"dialog-input",onKeydown:lt(e.submitInput,["enter"])},null,40,so),[[it,e.actionDialog.inputValue]]),t("div",no,[t("button",{onClick:e.closeActionDialog,class:"dialog-btn",disabled:e.actionDialogLoading},"Cancel",8,ao),t("button",{onClick:e.submitInput,class:"dialog-btn dialog-btn-primary",disabled:e.actionDialogLoading},"Submit",8,io)])]))])],2112)):r("v-if",!0)])):r("v-if",!0),r(" Settings Dialog "),e.showSettingsDialog&&e.settingsData?(a(),n("div",{key:2,class:"action-dialog-overlay",onClick:M(e.closeSettingsDialog,["self"])},[t("div",lo,[i[30]||(i[30]=t("h3",null,"Settings",-1)),e.loadingSettings?(a(),n("div",co,[...i[25]||(i[25]=[t("div",{class:"loader-spinner"},null,-1),t("span",null,"Loading...",-1)])])):(a(),n("div",ro,[(a(!0),n(y,null,w(e.settingDefinitions,o=>{var p;return a(),n("div",{key:o.key,class:"setting-item"},[t("div",go,[t("div",uo,[t("div",vo,g(o.label),1),t("div",po,g(o.description),1)])]),r(" Boolean setting "),o.type==="boolean"?(a(),n("div",ho,[t("div",mo,[i[26]||(i[26]=P(" Current: ",-1)),t("strong",null,g(e.settingsData.merged[o.key]?"Enabled":"Disabled"),1),o.key in e.settingsData.project?(a(),n("span",yo,"(project)")):o.key in e.settingsData.global?(a(),n("span",ko,"(global)")):r("v-if",!0)]),t("div",fo,[t("button",{onClick:h=>e.updateSetting(o.key,!0,"global"),class:"setting-btn",disabled:e.loadingSettings}," Enable (global) ",8,bo),t("button",{onClick:h=>e.updateSetting(o.key,!1,"global"),class:"setting-btn",disabled:e.loadingSettings}," Disable (global) ",8,_o),t("button",{onClick:h=>e.updateSetting(o.key,!0,"project"),class:"setting-btn",disabled:e.loadingSettings}," Enable (project) ",8,wo),t("button",{onClick:h=>e.updateSetting(o.key,!1,"project"),class:"setting-btn",disabled:e.loadingSettings}," Disable (project) ",8,Co)])])):o.type==="select"?(a(),n(y,{key:1},[r(" Select setting "),t("div",So,[t("div",Do,[i[27]||(i[27]=P(" Current: ",-1)),t("strong",null,g(((p=o.options.find(h=>h.value===e.settingsData.merged[o.key]))==null?void 0:p.label)||"Not set"),1),o.key in e.settingsData.project?(a(),n("span",Po,"(project)")):o.key in e.settingsData.global?(a(),n("span",xo,"(global)")):r("v-if",!0)]),(a(!0),n(y,null,w(o.options,h=>(a(),n("div",{class:"setting-select-group",key:h.value},[t("div",Ao,g(h.label),1),t("div",To,[t("button",{onClick:f=>e.updateSetting(o.key,h.value,"global"),class:"setting-btn",disabled:e.loadingSettings}," Set global ",8,jo),t("button",{onClick:f=>e.updateSetting(o.key,h.value,"project"),class:"setting-btn",disabled:e.loadingSettings}," Set project ",8,Io)])]))),128))])],2112)):o.type==="action"?(a(),n(y,{key:2},[r(" Action setting "),t("div",Mo,[t("div",zo,[t("button",{onClick:e.openHooksSection,class:"setting-btn setting-btn-action",disabled:e.loadingSettings}," Open ",8,No)])])],2112)):r("v-if",!0)])}),128))])),r(" Hooks Section (shown when hooks action is triggered) "),e.showHooksSection?(a(),n("div",Ho,[t("div",{class:"hooks-header"},[t("button",{onClick:e.closeHooksSection,class:"back-btn",title:"Back to settings"},"← Back"),i[28]||(i[28]=t("h4",null,"Hooks Management",-1))]),e.loadingHooks?(a(),n("div",Oo,[...i[29]||(i[29]=[t("div",{class:"loader-spinner"},null,-1),t("span",null,"Loading hooks...",-1)])])):(a(),n("div",Fo,[(a(!0),n(y,null,w(e.hooksData,o=>(a(),n("div",{key:o.name,class:"hook-item"},[t("div",Eo,g(o.name),1),t("div",{class:z(["hook-status",{"hook-active":o.active}])},g(o.active?"✓ Active":"Inactive"),3)]))),128)),t("div",{class:"hooks-actions"},[t("button",{onClick:e.editHooksWithAgent,class:"dialog-btn dialog-btn-primary"}," Edit Hooks with Agent ")])]))])):r("v-if",!0),t("div",{class:"dialog-buttons"},[t("button",{onClick:e.closeSettingsDialog,class:"dialog-btn dialog-btn-primary"},"Close")])])])):r("v-if",!0)])],64)}const Vo=Ht(Et,[["render",Lo],["__file","/Users/justinschroeder/Projects/dmux/frontend/src/components/Dashboard.vue"]]),qo=Ft(Vo);qo.mount("#app");
|
|
5516
|
+
\`).map(l=>{if(l.includes("|")){const d=l.split("|");if(d.length===2){const c=d[0],m=d[1].replace(/\\+/g,'<span style="color: #4ade80;">+</span>').replace(/-/g,'<span style="color: #f87171;">-</span>');return c+'<span style="opacity: 0.6;">|</span>'+m}}return l}).join("<br>"),St=async()=>{var s;if(k.value)try{D.value=!0;const d=await(await fetch(\`/api/callbacks/input/\${k.value.callbackId}\`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({value:k.value.inputValue})})).json();if(d.requiresInteraction){let c={};d.interactionType==="confirm"?c={type:"confirm",title:d.title||"Confirm",message:d.message,...d.confirmData}:d.interactionType==="choice"?c={type:"choice",title:d.title||"Choose",message:d.message,...d.choiceData}:d.interactionType==="input"&&(c={type:"input",title:d.title||"Input",message:d.message,...d.inputData,inputValue:((s=d.inputData)==null?void 0:s.defaultValue)||""},I(()=>{const v=document.querySelector(".dialog-input");v&&(v.focus(),v.select())})),k.value=c}else B()}catch(l){console.error("Failed to submit input:",l),alert("Failed to complete action")}finally{D.value=!1}},Dt=async(s,l)=>{try{b.value.add(s.id),b.value=new Set(b.value);const d=l.keys||[l.action];for(const c of d)await fetch(\`/api/keys/\${s.id}\`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({key:c})});setTimeout(()=>{b.value.delete(s.id),b.value=new Set(b.value)},1500)}catch(d){console.error("Failed to select option:",d),b.value.delete(s.id),b.value=new Set(b.value)}},Pt=async s=>{const l=h.value[s.id];if(!(!l||!l.trim()))try{f.value.add(s.id),f.value=new Set(f.value);for(const d of l)await fetch(\`/api/keys/\${s.id}\`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({key:d})});await fetch(\`/api/keys/\${s.id}\`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({key:"Enter"})}),J.value[s.id]=l.substring(0,50)+(l.length>50?"...":""),h.value[s.id]="",setTimeout(()=>{delete J.value[s.id],f.value.delete(s.id),f.value=new Set(f.value)},3e3)}catch(d){console.error("Failed to send prompt:",d),f.value.delete(s.id),f.value=new Set(f.value)}},xt=s=>{const l=s.target;l.style.height="auto",l.style.height=l.scrollHeight+"px"},At=async()=>{try{N.value=!0;const l=await(await fetch("/api/settings")).json();V.value=l.settings,G.value=l.definitions,Z.value=!0,H.value=!1}catch(s){console.error("Failed to load settings:",s),alert("Failed to load settings")}finally{N.value=!1}},rt=()=>{Z.value=!1,V.value=null,G.value=[],H.value=!1,q.value=[]},Tt=async(s,l,d)=>{try{N.value=!0,await fetch("/api/settings",{method:"PATCH",headers:{"Content-Type":"application/json"},body:JSON.stringify({key:s,value:l,scope:d})});const v=await(await fetch("/api/settings")).json();V.value=v.settings}catch(c){console.error("Failed to update setting:",c),alert("Failed to update setting")}finally{N.value=!1}},jt=async()=>{try{X.value=!0;const l=await(await fetch("/api/hooks")).json();q.value=l.hooks||[],H.value=!0}catch(s){console.error("Failed to load hooks:",s),alert("Failed to load hooks")}finally{X.value=!1}},gt=()=>{H.value=!1,q.value=[]},It=async()=>{const s="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";gt(),rt(),T.value=s,await I(),await et()},st=s=>{const l=s.target;S.value&&!l.closest(".action-menu-btn")&&!l.closest(".action-menu-dropdown")&&(S.value=null)};zt(()=>{document.documentElement.setAttribute("data-theme",x.value),Y(),document.addEventListener("click",st),document.addEventListener("visibilitychange",()=>{document.hidden?$():Y()})}),Nt(()=>{$(),document.removeEventListener("click",st)});const ut={projectName:U,sessionName:e,connected:O,panes:F,lastUpdate:o,timeSinceUpdate:p,promptInputs:h,sendingPrompts:f,queuedMessages:J,theme:x,expandedPrompts:A,loadingOptions:b,showCreateDialog:K,newPanePrompt:T,newPaneAgent:C,creatingPane:Q,availableAgents:E,needsAgentChoice:L,createStep:j,actions:pt,paneActions:W,showActionMenu:S,actionDialog:k,executingAction:R,actionDialogLoading:D,showSettingsDialog:Z,settingsData:V,settingDefinitions:G,loadingSettings:N,showHooksSection:H,hooksData:q,loadingHooks:X,get pollingInterval(){return _},set pollingInterval(s){_=s},startPolling:Y,stopPolling:$,toggleTheme:ht,togglePrompt:mt,openCreateDialog:yt,closeCreateDialog:tt,createPane:et,selectAgent:kt,updatePanesFromData:ct,fetchPanes:ot,fetchPaneActions:dt,toggleActionMenu:ft,executeAction:bt,closeActionDialog:B,confirmAction:_t,selectChoice:wt,colorizeDiffStat:Ct,submitInput:St,selectOption:Dt,sendPrompt:Pt,autoExpand:xt,openSettingsDialog:At,closeSettingsDialog:rt,updateSetting:Tt,openHooksSection:jt,closeHooksSection:gt,editHooksWithAgent:It,handleClickOutside:st};return Object.defineProperty(ut,"__isScriptSetup",{enumerable:!1,value:!0}),ut}}),Lt={class:"session-info"},Vt=["title"],qt={key:0,xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24"},Bt={key:1,xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24"},Ut={key:0},Jt={class:"container"},Kt={class:"actions-bar"},Qt=["disabled"],Wt=["disabled"],Rt={key:0,class:"no-panes"},Zt={key:1,class:"panes-grid"},Gt={class:"pane-header"},Xt={class:"pane-header-content"},Yt=["href"],$t={class:"pane-title"},te={class:"pane-meta"},ee={key:0,class:"pane-autopilot",title:"Autopilot enabled"},oe={key:1,class:"pane-agent shell",title:"Shell pane"},se={class:"pane-id"},ne=["onClick"],ae={key:0,class:"action-menu-dropdown"},ie=["onClick","disabled"],le={class:"action-icon"},ce={class:"action-label"},de={class:"pane-prompt-section"},re=["onClick"],ge={class:"prompt-header"},ue={class:"expand-icon"},ve={class:"prompt-text"},pe={key:0,class:"pane-prompt-full"},he={key:1,class:"agent-summary"},me={key:2,class:"analyzer-error"},ye={key:0,class:"options-dialog"},ke={class:"options-question"},fe={key:0,class:"options-warning"},be={key:1,class:"analyzing-state"},_e={key:2,class:"options-buttons"},we=["onClick","disabled"],Ce={class:"prompt-input-wrapper"},Se=["onUpdate:modelValue","placeholder","disabled"],De=["onClick","disabled","title"],Pe={key:0,class:"button-loader"},xe={key:1,xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 988.44 1200.05"},Ae={key:0,class:"queued-message"},Te={key:3,class:"dev-server-status"},je=["href"],Ie={class:"action-dialog"},Me={key:0},ze=["onKeydown"],Ne={class:"dialog-buttons"},He=["disabled"],Oe={key:1},Fe={class:"agent-choices"},Ee=["onClick","disabled"],Le={key:0,class:"action-dialog"},Ve={key:0,class:"dialog-loading"},qe={key:1,class:"dialog-buttons"},Be=["disabled"],Ue=["disabled"],Je={class:"action-dialog"},Ke={key:0},Qe={key:1,class:"dialog-loading"},We={key:2},Re={class:"choice-options"},Ze=["onClick","disabled"],Ge={class:"choice-label"},Xe={key:0,class:"choice-description"},Ye={class:"dialog-buttons"},$e=["disabled"],to={class:"action-dialog"},eo=["innerHTML"],oo={key:1,class:"dialog-loading"},so={key:2},no=["placeholder"],ao={class:"dialog-buttons"},io=["disabled"],lo=["disabled"],co={class:"action-dialog settings-dialog"},ro={key:0,class:"dialog-loading"},go={key:1,class:"settings-list"},uo={class:"setting-header"},vo={class:"setting-info"},po={class:"setting-label"},ho={class:"setting-description"},mo={key:0,class:"setting-control"},yo={class:"setting-value"},ko={key:0,class:"setting-scope"},fo={key:1,class:"setting-scope"},bo={class:"setting-buttons"},_o=["onClick","disabled"],wo=["onClick","disabled"],Co=["onClick","disabled"],So=["onClick","disabled"],Do={class:"setting-control"},Po={class:"setting-value"},xo={key:0,class:"setting-scope"},Ao={key:1,class:"setting-scope"},To={class:"setting-option-label"},jo={class:"setting-buttons"},Io=["onClick","disabled"],Mo=["onClick","disabled"],zo={class:"setting-control"},No={class:"setting-buttons"},Ho=["disabled"],Oo={key:2,class:"hooks-section"},Fo={key:0,class:"dialog-loading"},Eo={key:1,class:"hooks-list"},Lo={class:"hook-name"};function Vo(vt,i,U,e,O,F){return a(),n(y,null,[t("header",null,[i[7]||(i[7]=t("img",{src:"https://cdn.formk.it/dmux/dmux.png",alt:"dmux",class:"logo"},null,-1)),t("h1",null,g(e.projectName),1),t("div",Lt,[t("button",{onClick:e.toggleTheme,class:"theme-toggle",title:e.theme==="dark"?"Switch to light mode":"Switch to dark mode"},[e.theme==="dark"?(a(),n("svg",qt,[...i[5]||(i[5]=[t("path",{d:"M12 2.25a.75.75 0 01.75.75v2.25a.75.75 0 01-1.5 0V3a.75.75 0 01.75-.75zM7.5 12a4.5 4.5 0 119 0 4.5 4.5 0 01-9 0zM18.894 6.166a.75.75 0 00-1.06-1.06l-1.591 1.59a.75.75 0 101.06 1.061l1.591-1.59zM21.75 12a.75.75 0 01-.75.75h-2.25a.75.75 0 010-1.5H21a.75.75 0 01.75.75zM17.834 18.894a.75.75 0 001.06-1.06l-1.59-1.591a.75.75 0 10-1.061 1.06l1.59 1.591zM12 18a.75.75 0 01.75.75V21a.75.75 0 01-1.5 0v-2.25A.75.75 0 0112 18zM7.758 17.303a.75.75 0 00-1.061-1.06l-1.591 1.59a.75.75 0 001.06 1.061l1.591-1.59zM6 12a.75.75 0 01-.75.75H3a.75.75 0 010-1.5h2.25A.75.75 0 016 12zM6.697 7.757a.75.75 0 001.06-1.06l-1.59-1.591a.75.75 0 00-1.061 1.06l1.59 1.591z"},null,-1)])])):(a(),n("svg",Bt,[...i[6]||(i[6]=[t("path",{d:"M9.528 1.718a.75.75 0 01.162.819A8.97 8.97 0 009 6.5a9 9 0 009 9 8.97 8.97 0 003.963-.69.75.75 0 01.981.98 10.503 10.503 0 01-9.694 6.46c-5.799 0-10.5-4.701-10.5-10.5 0-4.368 2.667-8.112 6.46-9.694a.75.75 0 01.818.162z"},null,-1)])]))],8,Vt),e.sessionName?(a(),n("span",Ut,g(e.sessionName),1)):r("v-if",!0),t("span",{class:"status-indicator",style:Ot({color:e.connected?"#4ade80":"#f87171"})},"●",4)])]),t("div",Jt,[t("main",null,[t("div",Kt,[t("button",{onClick:e.openCreateDialog,class:"create-pane-button",disabled:e.creatingPane},[...i[8]||(i[8]=[t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",fill:"currentColor"},[t("path",{d:"M12 4.5v15m7.5-7.5h-15",stroke:"currentColor","stroke-width":"2","stroke-linecap":"round","stroke-linejoin":"round"})],-1),P(" Create New Pane ",-1)])],8,Qt),t("button",{onClick:e.openSettingsDialog,class:"settings-button",disabled:e.loadingSettings,title:"Settings"},[...i[9]||(i[9]=[t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",fill:"currentColor"},[t("path",{d:"M10.5 1.875a1.125 1.125 0 012.25 0v.563c0 1.018.84 1.843 1.854 1.839a.75.75 0 01.585.217l.4.4a.75.75 0 01.216.585c-.004 1.014.821 1.854 1.839 1.854h.563a1.125 1.125 0 010 2.25h-.563c-1.018 0-1.843.84-1.839 1.854a.75.75 0 01-.217.585l-.4.4a.75.75 0 01-.585.216c-1.014-.004-1.854.821-1.854 1.839v.563a1.125 1.125 0 01-2.25 0v-.563c0-1.018-.84-1.843-1.854-1.839a.75.75 0 01-.585-.217l-.4-.4a.75.75 0 01-.216-.585c.004-1.014-.821-1.854-1.839-1.854H3.75a1.125 1.125 0 010-2.25h.563c1.018 0 1.843-.84 1.839-1.854a.75.75 0 01.217-.585l.4-.4a.75.75 0 01.585-.216c1.014.004 1.854-.821 1.854-1.839V1.875zM12 15a3 3 0 100-6 3 3 0 000 6z"})],-1),P(" Settings ",-1)])],8,Wt)]),e.panes.length===0?(a(),n("div",Rt,[...i[10]||(i[10]=[t("p",null,"No dmux panes active",-1),t("p",{class:"hint"},\`Click "Create New Pane" above or press 'n' in dmux\`,-1)])])):(a(),n("div",Zt,[(a(!0),n(y,null,w(e.panes,o=>(a(),n("div",{key:o.id,class:"pane-card"},[t("div",Gt,[t("div",Xt,[t("a",{href:"/panes/"+o.id,class:"pane-title-link"},[t("span",$t,g(o.slug),1),i[11]||(i[11]=t("span",{class:"pane-arrow"},"→",-1))],8,Yt),t("div",te,[o.autopilot?(a(),n("span",ee,"🤖")):r("v-if",!0),o.type==="shell"?(a(),n("span",oe,g(o.shellType||"shell"),1)):(a(),n("span",{key:2,class:z(["pane-agent",o.agent||""])},g(o.agent||"unknown"),3)),t("span",se,g(o.paneId),1)])]),t("button",{onClick:p=>e.toggleActionMenu(o.id),class:"action-menu-btn",title:"Actions"},[...i[12]||(i[12]=[t("span",null,"⋮",-1)])],8,ne)]),r(" Action Menu Dropdown "),e.showActionMenu===o.id&&e.paneActions[o.id]?(a(),n("div",ae,[(a(!0),n(y,null,w(e.paneActions[o.id],p=>(a(),n("button",{key:p.id,onClick:h=>e.executeAction(o,p),class:"action-menu-item",disabled:e.executingAction},[t("span",le,g(p.icon||"•"),1),t("span",ce,g(p.label),1)],8,ie))),128))])):r("v-if",!0),t("div",de,[t("div",{class:z(["pane-prompt-preview",{expanded:e.expandedPrompts.has(o.id)}]),onClick:p=>e.togglePrompt(o.id)},[t("div",ge,[i[13]||(i[13]=t("span",{class:"prompt-label"},"Initial Prompt",-1)),t("span",ue,g(e.expandedPrompts.has(o.id)?"▼":"▶"),1)]),t("span",ve,g(o.prompt||"No prompt"),1)],10,re),e.expandedPrompts.has(o.id)?(a(),n("div",pe,g(o.prompt||"No prompt"),1)):r("v-if",!0)]),r(" Show agent summary when idle "),o.agentStatus==="idle"&&o.agentSummary?(a(),n("div",he,g(o.agentSummary),1)):r("v-if",!0),r(" Show analyzer error if present "),o.analyzerError?(a(),n("div",me," ⚠ "+g(o.analyzerError),1)):r("v-if",!0),t("div",{class:"pane-interactive",onClick:i[0]||(i[0]=M(()=>{},["prevent"]))},[r(" Options Dialog (when waiting with options) "),o.agentStatus==="waiting"&&o.options&&o.options.length>0?(a(),n("div",ye,[t("div",ke,g(o.optionsQuestion||"Choose an option:"),1),o.potentialHarm&&o.potentialHarm.hasRisk?(a(),n("div",fe," ⚠️ "+g(o.potentialHarm.description),1)):r("v-if",!0),e.loadingOptions.has(o.id)?(a(),n("div",be,[...i[14]||(i[14]=[t("div",{class:"loader-spinner"},null,-1),t("span",null,"Processing selection...",-1)])])):(a(),n("div",_e,[(a(!0),n(y,null,w(o.options,p=>(a(),n("button",{key:p.action,onClick:h=>e.selectOption(o,p),class:z(["option-button",{"option-button-danger":o.potentialHarm&&o.potentialHarm.hasRisk}]),disabled:e.loadingOptions.has(o.id)},g(p.action),11,we))),128))]))])):o.agentStatus==="analyzing"?(a(),n(y,{key:1},[r(" Analyzing (show loader) "),i[15]||(i[15]=t("div",{class:"analyzing-state"},[t("div",{class:"loader-spinner"}),t("span",null,"Analyzing...")],-1))],2112)):(a(),n(y,{key:2},[r(" Working/Idle (show prompt input) "),t("div",null,[t("div",Ce,[at(t("textarea",{"onUpdate:modelValue":p=>e.promptInputs[o.id]=p,onInput:e.autoExpand,placeholder:o.agentStatus==="working"?"Queue a prompt...":"Send a prompt...",disabled:e.sendingPrompts.has(o.id),class:"prompt-textarea",rows:"1"},null,40,Se),[[it,e.promptInputs[o.id]]]),t("button",{onClick:p=>e.sendPrompt(o),disabled:!e.promptInputs[o.id]||e.sendingPrompts.has(o.id),class:"send-button",title:o.agentStatus==="working"?"Queue prompt":"Send prompt"},[e.sendingPrompts.has(o.id)?(a(),n("span",Pe)):(a(),n("svg",xe,[...i[16]||(i[16]=[t("path",{d:"M425.13,28.37L30.09,423.41C11.19,441.37.34,466.2,0,492.27c-.34,26.07,9.86,51.17,28.29,69.61,18.43,18.45,43.52,28.67,69.59,28.35,26.07-.31,50.91-11.14,68.88-30.02l233.16-233.52v776.64c0,34.56,18.43,66.48,48.36,83.76,29.93,17.28,66.8,17.28,96.72,0,29.93-17.28,48.36-49.21,48.36-83.76V328.85l231.72,231.36c24.63,23.41,59.74,32.18,92.48,23.09,32.74-9.08,58.32-34.68,67.38-67.43,9.05-32.75.25-67.85-23.18-92.46L566.73,28.37C548.63,10.16,524-.04,498.33.05c-.8-.06-1.6-.06-2.4,0-.8-.06-1.6-.06-2.4,0-25.65,0-50.25,10.19-68.4,28.32h0Z"},null,-1)])]))],8,De)]),e.queuedMessages[o.id]?(a(),n("div",Ae," ✓ "+g(e.queuedMessages[o.id]),1)):r("v-if",!0)])],2112))]),o.devStatus&&o.devStatus!=="stopped"?(a(),n("div",Te,[i[17]||(i[17]=t("span",{class:"status-label"},"Dev Server:",-1)),t("span",{class:z(["status-badge",o.devStatus])},g(o.devStatus),3),o.devUrl?(a(),n("a",{key:0,href:o.devUrl,target:"_blank",class:"dev-link"},"↗",8,je)):r("v-if",!0)])):r("v-if",!0)]))),128))]))]),r(" Create Pane Dialog "),e.showCreateDialog?(a(),n("div",{key:0,class:"action-dialog-overlay",onClick:M(e.closeCreateDialog,["self"])},[t("div",Ie,[i[21]||(i[21]=t("h3",null,"Create New Pane",-1)),e.createStep==="prompt"?(a(),n("div",Me,[i[18]||(i[18]=t("label",{for:"pane-prompt"},"Provide an initial prompt for your agent",-1)),at(t("textarea",{id:"pane-prompt","onUpdate:modelValue":i[1]||(i[1]=o=>e.newPanePrompt=o),placeholder:"E.g., Fix the authentication bug, Add dark mode, etc.",rows:"4",onKeydown:[lt(M(e.createPane,["meta"]),["enter"]),lt(M(e.createPane,["ctrl"]),["enter"])]},null,40,ze),[[it,e.newPanePrompt]]),i[19]||(i[19]=t("div",{class:"dialog-hint"},[P(" 💡 Press "),t("kbd",null,"⌘ Enter"),P(" or "),t("kbd",null,"Ctrl Enter"),P(" to create ")],-1)),t("div",Ne,[t("button",{onClick:e.closeCreateDialog,class:"dialog-btn"},"Cancel"),t("button",{onClick:e.createPane,disabled:!e.newPanePrompt.trim()||e.creatingPane,class:"dialog-btn dialog-btn-primary"},g(e.creatingPane?"Creating...":"Create Pane"),9,He)])])):e.createStep==="agent"?(a(),n("div",Oe,[i[20]||(i[20]=t("p",null,"Multiple agents available. Choose one:",-1)),t("div",Fe,[(a(!0),n(y,null,w(e.availableAgents,o=>(a(),n("button",{key:o,onClick:p=>e.selectAgent(o),class:"agent-choice-button",disabled:e.creatingPane},g(o),9,Ee))),128))]),t("div",{class:"dialog-buttons"},[t("button",{onClick:e.closeCreateDialog,class:"dialog-btn"},"Cancel")])])):r("v-if",!0)])])):r("v-if",!0),r(" Action Dialogs "),e.actionDialog?(a(),n("div",{key:1,class:"action-dialog-overlay",onClick:M(e.closeActionDialog,["self"])},[r(" Confirm Dialog "),e.actionDialog.type==="confirm"?(a(),n("div",Le,[t("h3",null,g(e.actionDialog.title),1),t("p",null,g(e.actionDialog.message),1),e.actionDialogLoading?(a(),n("div",Ve,[...i[22]||(i[22]=[t("div",{class:"loader-spinner"},null,-1),t("span",null,"Processing...",-1)])])):(a(),n("div",qe,[t("button",{onClick:i[2]||(i[2]=o=>e.confirmAction(!1)),class:"dialog-btn",disabled:e.actionDialogLoading},"Cancel",8,Be),t("button",{onClick:i[3]||(i[3]=o=>e.confirmAction(!0)),class:"dialog-btn dialog-btn-primary",disabled:e.actionDialogLoading},"Confirm",8,Ue)]))])):e.actionDialog.type==="choice"?(a(),n(y,{key:1},[r(" Choice Dialog "),t("div",Je,[t("h3",null,g(e.actionDialog.title),1),e.actionDialog.message?(a(),n("p",Ke,g(e.actionDialog.message),1)):r("v-if",!0),e.actionDialogLoading?(a(),n("div",Qe,[...i[23]||(i[23]=[t("div",{class:"loader-spinner"},null,-1),t("span",null,"Processing...",-1)])])):(a(),n("div",We,[t("div",Re,[(a(!0),n(y,null,w(e.actionDialog.options,o=>(a(),n("button",{key:o.id,onClick:p=>e.selectChoice(o.id),class:z(["choice-option-btn",{danger:o.danger}]),disabled:e.actionDialogLoading},[t("div",Ge,g(o.label),1),o.description?(a(),n("div",Xe,g(o.description),1)):r("v-if",!0)],10,Ze))),128))]),t("div",Ye,[t("button",{onClick:e.closeActionDialog,class:"dialog-btn",disabled:e.actionDialogLoading},"Cancel",8,$e)])]))])],2112)):e.actionDialog.type==="input"?(a(),n(y,{key:2},[r(" Input Dialog "),t("div",to,[t("h3",null,g(e.actionDialog.title),1),e.actionDialog.message?(a(),n("div",{key:0,class:"dialog-message",innerHTML:e.colorizeDiffStat(e.actionDialog.message)},null,8,eo)):r("v-if",!0),e.actionDialogLoading?(a(),n("div",oo,[...i[24]||(i[24]=[t("div",{class:"loader-spinner"},null,-1),t("span",null,"Processing...",-1)])])):(a(),n("div",so,[at(t("input",{type:"text","onUpdate:modelValue":i[4]||(i[4]=o=>e.actionDialog.inputValue=o),placeholder:e.actionDialog.placeholder,class:"dialog-input",onKeydown:lt(e.submitInput,["enter"])},null,40,no),[[it,e.actionDialog.inputValue]]),t("div",ao,[t("button",{onClick:e.closeActionDialog,class:"dialog-btn",disabled:e.actionDialogLoading},"Cancel",8,io),t("button",{onClick:e.submitInput,class:"dialog-btn dialog-btn-primary",disabled:e.actionDialogLoading},"Submit",8,lo)])]))])],2112)):r("v-if",!0)])):r("v-if",!0),r(" Settings Dialog "),e.showSettingsDialog&&e.settingsData?(a(),n("div",{key:2,class:"action-dialog-overlay",onClick:M(e.closeSettingsDialog,["self"])},[t("div",co,[i[30]||(i[30]=t("h3",null,"Settings",-1)),e.loadingSettings?(a(),n("div",ro,[...i[25]||(i[25]=[t("div",{class:"loader-spinner"},null,-1),t("span",null,"Loading...",-1)])])):(a(),n("div",go,[(a(!0),n(y,null,w(e.settingDefinitions,o=>{var p;return a(),n("div",{key:o.key,class:"setting-item"},[t("div",uo,[t("div",vo,[t("div",po,g(o.label),1),t("div",ho,g(o.description),1)])]),r(" Boolean setting "),o.type==="boolean"?(a(),n("div",mo,[t("div",yo,[i[26]||(i[26]=P(" Current: ",-1)),t("strong",null,g(e.settingsData.merged[o.key]?"Enabled":"Disabled"),1),o.key in e.settingsData.project?(a(),n("span",ko,"(project)")):o.key in e.settingsData.global?(a(),n("span",fo,"(global)")):r("v-if",!0)]),t("div",bo,[t("button",{onClick:h=>e.updateSetting(o.key,!0,"global"),class:"setting-btn",disabled:e.loadingSettings}," Enable (global) ",8,_o),t("button",{onClick:h=>e.updateSetting(o.key,!1,"global"),class:"setting-btn",disabled:e.loadingSettings}," Disable (global) ",8,wo),t("button",{onClick:h=>e.updateSetting(o.key,!0,"project"),class:"setting-btn",disabled:e.loadingSettings}," Enable (project) ",8,Co),t("button",{onClick:h=>e.updateSetting(o.key,!1,"project"),class:"setting-btn",disabled:e.loadingSettings}," Disable (project) ",8,So)])])):o.type==="select"?(a(),n(y,{key:1},[r(" Select setting "),t("div",Do,[t("div",Po,[i[27]||(i[27]=P(" Current: ",-1)),t("strong",null,g(((p=o.options.find(h=>h.value===e.settingsData.merged[o.key]))==null?void 0:p.label)||"Not set"),1),o.key in e.settingsData.project?(a(),n("span",xo,"(project)")):o.key in e.settingsData.global?(a(),n("span",Ao,"(global)")):r("v-if",!0)]),(a(!0),n(y,null,w(o.options,h=>(a(),n("div",{class:"setting-select-group",key:h.value},[t("div",To,g(h.label),1),t("div",jo,[t("button",{onClick:f=>e.updateSetting(o.key,h.value,"global"),class:"setting-btn",disabled:e.loadingSettings}," Set global ",8,Io),t("button",{onClick:f=>e.updateSetting(o.key,h.value,"project"),class:"setting-btn",disabled:e.loadingSettings}," Set project ",8,Mo)])]))),128))])],2112)):o.type==="action"?(a(),n(y,{key:2},[r(" Action setting "),t("div",zo,[t("div",No,[t("button",{onClick:e.openHooksSection,class:"setting-btn setting-btn-action",disabled:e.loadingSettings}," Open ",8,Ho)])])],2112)):r("v-if",!0)])}),128))])),r(" Hooks Section (shown when hooks action is triggered) "),e.showHooksSection?(a(),n("div",Oo,[t("div",{class:"hooks-header"},[t("button",{onClick:e.closeHooksSection,class:"back-btn",title:"Back to settings"},"← Back"),i[28]||(i[28]=t("h4",null,"Hooks Management",-1))]),e.loadingHooks?(a(),n("div",Fo,[...i[29]||(i[29]=[t("div",{class:"loader-spinner"},null,-1),t("span",null,"Loading hooks...",-1)])])):(a(),n("div",Eo,[(a(!0),n(y,null,w(e.hooksData,o=>(a(),n("div",{key:o.name,class:"hook-item"},[t("div",Lo,g(o.name),1),t("div",{class:z(["hook-status",{"hook-active":o.active}])},g(o.active?"✓ Active":"Inactive"),3)]))),128)),t("div",{class:"hooks-actions"},[t("button",{onClick:e.editHooksWithAgent,class:"dialog-btn dialog-btn-primary"}," Edit Hooks with Agent ")])]))])):r("v-if",!0),t("div",{class:"dialog-buttons"},[t("button",{onClick:e.closeSettingsDialog,class:"dialog-btn dialog-btn-primary"},"Close")])])])):r("v-if",!0)])],64)}const qo=Ht(Et,[["render",Vo],["__file","/Users/justinschroeder/Projects/dmux/frontend/src/components/Dashboard.vue"]]),Bo=Ft(qo);Bo.mount("#app");
|
|
4791
5517
|
`,
|
|
4792
5518
|
mimeType: 'application/javascript',
|
|
4793
|
-
size:
|
|
5519
|
+
size: 27357
|
|
5520
|
+
},
|
|
5521
|
+
'decorative-pane.js': {
|
|
5522
|
+
content: `#!/usr/bin/env node
|
|
5523
|
+
// Decorative pane renderer - displays ASCII art with animated falling binary characters
|
|
5524
|
+
// This runs continuously without showing a command prompt
|
|
5525
|
+
import { ASCII_ART as ASCII_ART_EXPORTS } from "./utils/asciiArt.js";
|
|
5526
|
+
// Parse the ASCII art string into an array of lines
|
|
5527
|
+
const ASCII_ART = ASCII_ART_EXPORTS.dmuxWelcome.trim().split("\\n");
|
|
5528
|
+
const FILL_CHAR = "·";
|
|
5529
|
+
const ORANGE = "\\x1b[38;5;208m"; // ANSI 256-color orange
|
|
5530
|
+
const DIM_GRAY = "\\x1b[38;5;238m"; // Dim gray for fill dots
|
|
5531
|
+
const RESET = "\\x1b[0m"; // Reset color
|
|
5532
|
+
// Static drop settings
|
|
5533
|
+
const TAIL_LENGTH = 8; // Length of the fading tail
|
|
5534
|
+
const NUM_STATIC_DROPS = 150; // Number of drops to render in static view
|
|
5535
|
+
// Shades from bright to dim for the tail effect (orange)
|
|
5536
|
+
const SHADES = [
|
|
5537
|
+
"\\x1b[38;5;214m", // Bright orange
|
|
5538
|
+
"\\x1b[38;5;208m", // Orange
|
|
5539
|
+
"\\x1b[38;5;202m", // Darker orange
|
|
5540
|
+
"\\x1b[38;5;166m", // Even darker
|
|
5541
|
+
"\\x1b[38;5;130m", // Very dark orange
|
|
5542
|
+
"\\x1b[38;5;94m", // Brown-orange
|
|
5543
|
+
"\\x1b[38;5;58m", // Dark brown
|
|
5544
|
+
"\\x1b[38;5;236m", // Almost black
|
|
5545
|
+
];
|
|
5546
|
+
/**
|
|
5547
|
+
* Generate random static drops that look like a paused animation
|
|
5548
|
+
*/
|
|
5549
|
+
function generateStaticDrops(width, height) {
|
|
5550
|
+
const drops = [];
|
|
5551
|
+
for (let i = 0; i < NUM_STATIC_DROPS; i++) {
|
|
5552
|
+
// Random column
|
|
5553
|
+
const column = Math.floor(Math.random() * width);
|
|
5554
|
+
// Random position in the screen (can be anywhere including partially visible)
|
|
5555
|
+
const y = Math.floor(Math.random() * (height + TAIL_LENGTH));
|
|
5556
|
+
// Random binary characters
|
|
5557
|
+
const chars = Array.from({ length: TAIL_LENGTH }, () => Math.random() > 0.5 ? "1" : "0");
|
|
5558
|
+
drops.push({ column, y, chars });
|
|
5559
|
+
}
|
|
5560
|
+
return drops;
|
|
5561
|
+
}
|
|
5562
|
+
/**
|
|
5563
|
+
* Render static drops to a grid
|
|
5564
|
+
*/
|
|
5565
|
+
function renderStaticDrops(drops, grid, height) {
|
|
5566
|
+
for (const drop of drops) {
|
|
5567
|
+
for (let i = 0; i < drop.chars.length; i++) {
|
|
5568
|
+
const row = Math.floor(drop.y - i);
|
|
5569
|
+
if (row >= 0 &&
|
|
5570
|
+
row < height &&
|
|
5571
|
+
drop.column >= 0 &&
|
|
5572
|
+
drop.column < grid[row].length) {
|
|
5573
|
+
const shadeIndex = Math.min(i, SHADES.length - 1);
|
|
5574
|
+
grid[row][drop.column] = {
|
|
5575
|
+
char: drop.chars[i],
|
|
5576
|
+
color: SHADES[shadeIndex],
|
|
5577
|
+
};
|
|
5578
|
+
}
|
|
5579
|
+
}
|
|
5580
|
+
}
|
|
5581
|
+
}
|
|
5582
|
+
function render(width, height) {
|
|
5583
|
+
// Generate random static drops for this render
|
|
5584
|
+
const drops = generateStaticDrops(width, height);
|
|
5585
|
+
// Create a grid for the background layer (falling characters)
|
|
5586
|
+
const backgroundGrid = Array.from({ length: height }, () => Array.from({ length: width }, () => null));
|
|
5587
|
+
// Render all drops to the background grid
|
|
5588
|
+
renderStaticDrops(drops, backgroundGrid, height);
|
|
5589
|
+
const artHeight = ASCII_ART.length;
|
|
5590
|
+
const artWidth = Math.max(...ASCII_ART.map((line) => line.length));
|
|
5591
|
+
// Calculate vertical centering for ASCII art
|
|
5592
|
+
const topPadding = Math.floor((height - artHeight) / 2);
|
|
5593
|
+
const lines = [];
|
|
5594
|
+
// Build each line by combining background and foreground
|
|
5595
|
+
for (let row = 0; row < height; row++) {
|
|
5596
|
+
const isArtRow = row >= topPadding && row < topPadding + artHeight;
|
|
5597
|
+
const artLine = isArtRow ? ASCII_ART[row - topPadding] : null;
|
|
5598
|
+
let line = "";
|
|
5599
|
+
for (let col = 0; col < width; col++) {
|
|
5600
|
+
if (isArtRow && artLine) {
|
|
5601
|
+
const trimmedArt = artLine.trimEnd();
|
|
5602
|
+
const leftPadding = Math.max(0, Math.floor((width - trimmedArt.length) / 2));
|
|
5603
|
+
const artCol = col - leftPadding;
|
|
5604
|
+
// If we're in the art region and the art has a character here
|
|
5605
|
+
if (artCol >= 0 && artCol < trimmedArt.length) {
|
|
5606
|
+
const artChar = trimmedArt[artCol];
|
|
5607
|
+
// ASCII art takes precedence - render in orange
|
|
5608
|
+
line += ORANGE + artChar + RESET;
|
|
5609
|
+
}
|
|
5610
|
+
else {
|
|
5611
|
+
// Outside art region - show background or fill char
|
|
5612
|
+
const bg = backgroundGrid[row][col];
|
|
5613
|
+
if (bg) {
|
|
5614
|
+
line += bg.color + bg.char + RESET;
|
|
5615
|
+
}
|
|
5616
|
+
else {
|
|
5617
|
+
line += DIM_GRAY + FILL_CHAR + RESET;
|
|
5618
|
+
}
|
|
5619
|
+
}
|
|
5620
|
+
}
|
|
5621
|
+
else {
|
|
5622
|
+
// Not an art row - show background or fill char
|
|
5623
|
+
const bg = backgroundGrid[row][col];
|
|
5624
|
+
if (bg) {
|
|
5625
|
+
line += bg.color + bg.char + RESET;
|
|
5626
|
+
}
|
|
5627
|
+
else {
|
|
5628
|
+
line += DIM_GRAY + FILL_CHAR + RESET;
|
|
5629
|
+
}
|
|
5630
|
+
}
|
|
5631
|
+
}
|
|
5632
|
+
lines.push(line);
|
|
5633
|
+
}
|
|
5634
|
+
// Clear screen and render
|
|
5635
|
+
process.stdout.write("\\x1b[2J\\x1b[H"); // Clear screen and home cursor
|
|
5636
|
+
process.stdout.write(lines.join("\\n"));
|
|
5637
|
+
}
|
|
5638
|
+
// Initial render
|
|
5639
|
+
const initialWidth = process.stdout.columns || 80;
|
|
5640
|
+
const initialHeight = process.stdout.rows || 24;
|
|
5641
|
+
render(initialWidth, initialHeight);
|
|
5642
|
+
// Re-render only on terminal resize (static, no animation)
|
|
5643
|
+
process.stdout.on("resize", () => {
|
|
5644
|
+
const width = process.stdout.columns || 80;
|
|
5645
|
+
const height = process.stdout.rows || 24;
|
|
5646
|
+
render(width, height);
|
|
5647
|
+
});
|
|
5648
|
+
// Keep the process running
|
|
5649
|
+
process.stdin.resume();
|
|
5650
|
+
// Handle Ctrl+C gracefully (though this pane will be killed by tmux)
|
|
5651
|
+
process.on("SIGINT", () => {
|
|
5652
|
+
process.exit(0);
|
|
5653
|
+
});
|
|
5654
|
+
process.on("SIGTERM", () => {
|
|
5655
|
+
process.exit(0);
|
|
5656
|
+
});
|
|
5657
|
+
//# sourceMappingURL=decorative-pane.js.map`,
|
|
5658
|
+
mimeType: 'application/javascript',
|
|
5659
|
+
size: 5486
|
|
4794
5660
|
},
|
|
4795
5661
|
'index.js': {
|
|
4796
5662
|
content: `#!/usr/bin/env node
|
|
4797
5663
|
import { execSync } from 'child_process';
|
|
4798
5664
|
import fs from 'fs/promises';
|
|
5665
|
+
import * as fsSync from 'fs';
|
|
4799
5666
|
import path from 'path';
|
|
4800
5667
|
import { fileURLToPath } from 'url';
|
|
4801
5668
|
import { render } from 'ink';
|
|
4802
5669
|
import React from 'react';
|
|
4803
5670
|
import { createHash } from 'crypto';
|
|
5671
|
+
import { createRequire } from 'module';
|
|
4804
5672
|
import DmuxApp from './DmuxApp.js';
|
|
4805
5673
|
import { AutoUpdater } from './AutoUpdater.js';
|
|
4806
5674
|
import readline from 'readline';
|
|
4807
5675
|
import { DmuxServer } from './server/index.js';
|
|
4808
5676
|
import { StateManager } from './shared/StateManager.js';
|
|
5677
|
+
import { LogService } from './services/LogService.js';
|
|
5678
|
+
import { createWelcomePane } from './utils/welcomePane.js';
|
|
5679
|
+
import { TMUX_COLORS } from './theme/colors.js';
|
|
5680
|
+
import { SIDEBAR_WIDTH } from './utils/layoutManager.js';
|
|
4809
5681
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
5682
|
+
const require = createRequire(import.meta.url);
|
|
5683
|
+
const packageJson = require('../package.json');
|
|
4810
5684
|
class Dmux {
|
|
4811
5685
|
panesFile;
|
|
4812
5686
|
settingsFile;
|
|
@@ -4845,6 +5719,11 @@ class Dmux {
|
|
|
4845
5719
|
async init() {
|
|
4846
5720
|
// Set up global signal handlers for clean exit
|
|
4847
5721
|
this.setupGlobalSignalHandlers();
|
|
5722
|
+
// Set up hooks for this session (if in tmux)
|
|
5723
|
+
if (process.env.TMUX) {
|
|
5724
|
+
this.setupResizeHook();
|
|
5725
|
+
this.setupPaneSplitHook();
|
|
5726
|
+
}
|
|
4848
5727
|
// Ensure .dmux directory exists and is in .gitignore
|
|
4849
5728
|
await this.ensureDmuxDirectory();
|
|
4850
5729
|
// Check for migration from old config location
|
|
@@ -4856,13 +5735,16 @@ class Dmux {
|
|
|
4856
5735
|
projectRoot: this.projectRoot,
|
|
4857
5736
|
panes: [],
|
|
4858
5737
|
settings: {},
|
|
4859
|
-
lastUpdated: new Date().toISOString()
|
|
5738
|
+
lastUpdated: new Date().toISOString(),
|
|
5739
|
+
controlPaneId: undefined,
|
|
5740
|
+
controlPaneSize: 40 // Sidebar width
|
|
4860
5741
|
};
|
|
4861
5742
|
await fs.writeFile(this.panesFile, JSON.stringify(initialConfig, null, 2));
|
|
4862
5743
|
}
|
|
4863
5744
|
// Check for updates in background if needed
|
|
4864
5745
|
this.checkForUpdatesBackground();
|
|
4865
5746
|
const inTmux = process.env.TMUX !== undefined;
|
|
5747
|
+
const isDev = process.env.DMUX_DEV === 'true';
|
|
4866
5748
|
if (!inTmux) {
|
|
4867
5749
|
// Check if project-specific session already exists
|
|
4868
5750
|
try {
|
|
@@ -4875,27 +5757,116 @@ class Dmux {
|
|
|
4875
5757
|
execSync(\`tmux new-session -d -s \${this.sessionName}\`, { stdio: 'inherit' });
|
|
4876
5758
|
// Enable pane borders to show titles
|
|
4877
5759
|
execSync(\`tmux set-option -t \${this.sessionName} pane-border-status top\`, { stdio: 'inherit' });
|
|
5760
|
+
// Set border colors (foreground only - respects user's terminal background)
|
|
5761
|
+
execSync(\`tmux set-option -t \${this.sessionName} pane-active-border-style "fg=colour\${TMUX_COLORS.activeBorder}"\`, { stdio: 'inherit' });
|
|
5762
|
+
execSync(\`tmux set-option -t \${this.sessionName} pane-border-style "fg=colour\${TMUX_COLORS.inactiveBorder}"\`, { stdio: 'inherit' });
|
|
5763
|
+
// Set pane border format
|
|
5764
|
+
execSync(\`tmux set-option -t \${this.sessionName} pane-border-format " #{pane_title} "\`, { stdio: 'inherit' });
|
|
4878
5765
|
// Set pane title for the main dmux pane
|
|
4879
|
-
execSync(\`tmux select-pane -t \${this.sessionName} -T "dmux
|
|
4880
|
-
// Send dmux command to the new session
|
|
4881
|
-
|
|
5766
|
+
execSync(\`tmux select-pane -t \${this.sessionName} -T "dmux v\${packageJson.version} - \${this.projectName}"\`, { stdio: 'inherit' });
|
|
5767
|
+
// Send dmux command to the new session (use dev command if in dev mode)
|
|
5768
|
+
// In dev mode, use current directory if we're in a worktree, otherwise use projectRoot
|
|
5769
|
+
let devDirectory = this.projectRoot;
|
|
5770
|
+
if (isDev && this.isWorktree()) {
|
|
5771
|
+
devDirectory = process.cwd();
|
|
5772
|
+
}
|
|
5773
|
+
// Determine the dmux command to use
|
|
5774
|
+
let dmuxCommand;
|
|
5775
|
+
if (isDev) {
|
|
5776
|
+
dmuxCommand = \`cd "\${devDirectory}" && pnpm dev:watch\`;
|
|
5777
|
+
}
|
|
5778
|
+
else {
|
|
5779
|
+
// Check if we're running from a local installation
|
|
5780
|
+
// __dirname is 'dist' when compiled, so '../dmux' points to the wrapper
|
|
5781
|
+
const localDmuxPath = path.join(__dirname, '..', 'dmux');
|
|
5782
|
+
if (fsSync.existsSync(localDmuxPath)) {
|
|
5783
|
+
// Use absolute path to local dmux (works for both local builds and global installs)
|
|
5784
|
+
dmuxCommand = \`"\${localDmuxPath}"\`;
|
|
5785
|
+
}
|
|
5786
|
+
else {
|
|
5787
|
+
// Fallback to global dmux command
|
|
5788
|
+
dmuxCommand = 'dmux';
|
|
5789
|
+
}
|
|
5790
|
+
}
|
|
5791
|
+
execSync(\`tmux send-keys -t \${this.sessionName} "\${dmuxCommand}" Enter\`, { stdio: 'inherit' });
|
|
4882
5792
|
}
|
|
4883
5793
|
execSync(\`tmux attach-session -t \${this.sessionName}\`, { stdio: 'inherit' });
|
|
4884
5794
|
return;
|
|
4885
5795
|
}
|
|
4886
5796
|
// Enable pane borders to show titles
|
|
4887
|
-
|
|
4888
|
-
|
|
4889
|
-
}
|
|
4890
|
-
catch {
|
|
4891
|
-
|
|
4892
|
-
}
|
|
5797
|
+
// NOTE: Temporarily disabled to test if border updates cause UI shifts
|
|
5798
|
+
// try {
|
|
5799
|
+
// execSync(\`tmux set-option pane-border-status top\`, { stdio: 'pipe' });
|
|
5800
|
+
// } catch {
|
|
5801
|
+
// // Ignore if it fails
|
|
5802
|
+
// }
|
|
4893
5803
|
// Set pane title for the current pane running dmux
|
|
5804
|
+
// NOTE: Temporarily disabled to test if title updates cause UI shifts
|
|
5805
|
+
// try {
|
|
5806
|
+
// execSync(\`tmux select-pane -T "dmux v\${packageJson.version} - \${this.projectName}"\`, { stdio: 'pipe' });
|
|
5807
|
+
// } catch {
|
|
5808
|
+
// // Ignore if it fails (might not have permission or tmux version doesn't support it)
|
|
5809
|
+
// }
|
|
5810
|
+
// Get current pane ID (control pane for left sidebar)
|
|
5811
|
+
let controlPaneId;
|
|
4894
5812
|
try {
|
|
4895
|
-
|
|
5813
|
+
// Get current pane ID
|
|
5814
|
+
controlPaneId = execSync('tmux display-message -p "#{pane_id}"', {
|
|
5815
|
+
encoding: 'utf-8',
|
|
5816
|
+
stdio: 'pipe',
|
|
5817
|
+
}).trim();
|
|
5818
|
+
// Load existing config
|
|
5819
|
+
const configContent = await fs.readFile(this.panesFile, 'utf-8');
|
|
5820
|
+
const config = JSON.parse(configContent);
|
|
5821
|
+
// Ensure panes array exists
|
|
5822
|
+
if (!config.panes) {
|
|
5823
|
+
config.panes = [];
|
|
5824
|
+
}
|
|
5825
|
+
// ALWAYS update controlPaneId with current pane (it changes on restart)
|
|
5826
|
+
// This ensures layout manager always has the correct control pane ID
|
|
5827
|
+
const needsUpdate = config.controlPaneId !== controlPaneId;
|
|
5828
|
+
config.controlPaneId = controlPaneId;
|
|
5829
|
+
config.controlPaneSize = SIDEBAR_WIDTH;
|
|
5830
|
+
// If this is initial load or control pane changed, resize the sidebar
|
|
5831
|
+
if (needsUpdate) {
|
|
5832
|
+
// Resize control pane to sidebar width
|
|
5833
|
+
execSync(\`tmux resize-pane -t '\${controlPaneId}' -x \${SIDEBAR_WIDTH}\`, { stdio: 'pipe' });
|
|
5834
|
+
// Refresh client
|
|
5835
|
+
execSync('tmux refresh-client', { stdio: 'pipe' });
|
|
5836
|
+
// Save updated config
|
|
5837
|
+
config.lastUpdated = new Date().toISOString();
|
|
5838
|
+
await fs.writeFile(this.panesFile, JSON.stringify(config, null, 2));
|
|
5839
|
+
}
|
|
5840
|
+
// Create welcome pane if there are no dmux panes and no existing welcome pane
|
|
5841
|
+
// Check if welcome pane actually exists, not just if it's in config (handles tmux restarts)
|
|
5842
|
+
const { welcomePaneExists } = await import('./utils/welcomePane.js');
|
|
5843
|
+
const hasValidWelcomePane = config.welcomePaneId && welcomePaneExists(config.welcomePaneId);
|
|
5844
|
+
if (controlPaneId && config.panes && config.panes.length === 0) {
|
|
5845
|
+
if (!hasValidWelcomePane) {
|
|
5846
|
+
// Create new welcome pane
|
|
5847
|
+
const welcomePaneId = await createWelcomePane(controlPaneId);
|
|
5848
|
+
if (welcomePaneId) {
|
|
5849
|
+
config.welcomePaneId = welcomePaneId;
|
|
5850
|
+
config.lastUpdated = new Date().toISOString();
|
|
5851
|
+
await fs.writeFile(this.panesFile, JSON.stringify(config, null, 2));
|
|
5852
|
+
LogService.getInstance().debug(\`Created welcome pane: \${welcomePaneId}\`, 'Setup');
|
|
5853
|
+
}
|
|
5854
|
+
}
|
|
5855
|
+
else {
|
|
5856
|
+
// Welcome pane exists from previous session - fix the layout
|
|
5857
|
+
LogService.getInstance().debug('Welcome pane exists, applying correct layout', 'Setup');
|
|
5858
|
+
// Apply correct layout: sidebar (40) | welcome pane (rest)
|
|
5859
|
+
// Use "latest" mode so window auto-follows terminal size
|
|
5860
|
+
execSync(\`tmux set-window-option window-size latest\`, { stdio: 'pipe' });
|
|
5861
|
+
execSync(\`tmux set-window-option main-pane-width \${SIDEBAR_WIDTH}\`, { stdio: 'pipe' });
|
|
5862
|
+
execSync(\`tmux select-layout main-vertical\`, { stdio: 'pipe' });
|
|
5863
|
+
execSync(\`tmux refresh-client\`, { stdio: 'pipe' });
|
|
5864
|
+
}
|
|
5865
|
+
}
|
|
4896
5866
|
}
|
|
4897
|
-
catch {
|
|
4898
|
-
// Ignore
|
|
5867
|
+
catch (error) {
|
|
5868
|
+
// Ignore errors in sidebar setup - will work without it
|
|
5869
|
+
LogService.getInstance().error('Failed to set up sidebar layout', 'Setup', undefined, error instanceof Error ? error : undefined);
|
|
4899
5870
|
}
|
|
4900
5871
|
// Update state manager with project info
|
|
4901
5872
|
this.stateManager.updateProjectInfo(this.projectName, this.sessionName, this.projectRoot, this.panesFile);
|
|
@@ -4908,24 +5879,29 @@ class Dmux {
|
|
|
4908
5879
|
// Don't log the local URL - tunnel will be created on demand when "r" is pressed
|
|
4909
5880
|
}
|
|
4910
5881
|
catch (err) {
|
|
4911
|
-
|
|
5882
|
+
LogService.getInstance().error('Failed to start HTTP server', 'Setup', undefined, err instanceof Error ? err : undefined);
|
|
4912
5883
|
// Continue without server - not critical for main functionality
|
|
4913
5884
|
}
|
|
4914
|
-
//
|
|
5885
|
+
// Add test logs to verify logging system functionality
|
|
5886
|
+
const logService = LogService.getInstance();
|
|
5887
|
+
logService.debug(\`dmux started for project: \${this.projectName}\`, 'startup');
|
|
5888
|
+
logService.debug(\`Project root: \${this.projectRoot}\`, 'startup');
|
|
5889
|
+
logService.debug(\`HTTP server running on port \${serverInfo.port}\`, 'startup');
|
|
5890
|
+
logService.debug('Debug log: System initialized successfully', 'startup');
|
|
5891
|
+
// Add a sample warning and error for testing
|
|
5892
|
+
if (process.env.DMUX_DEV === 'true') {
|
|
5893
|
+
logService.debug('Development mode enabled - this is a test warning', 'startup');
|
|
5894
|
+
logService.debug('Press [l] to view logs, [L] to reset layout', 'startup');
|
|
5895
|
+
}
|
|
5896
|
+
// Suppress console output from LogService to prevent interference with Ink UI
|
|
5897
|
+
LogService.getInstance().setSuppressConsole(true);
|
|
5898
|
+
// Clear screen before launching Ink - minimal clearing to avoid artifacts
|
|
5899
|
+
// Don't use \\x1b[3J as it can cause layout shifts
|
|
4915
5900
|
process.stdout.write('\\x1b[2J\\x1b[H'); // Clear screen and move cursor to home
|
|
4916
|
-
|
|
4917
|
-
//
|
|
4918
|
-
|
|
4919
|
-
|
|
4920
|
-
}
|
|
4921
|
-
catch { }
|
|
4922
|
-
// Force tmux to refresh before Ink renders
|
|
4923
|
-
try {
|
|
4924
|
-
execSync('tmux refresh-client', { stdio: 'pipe' });
|
|
4925
|
-
}
|
|
4926
|
-
catch { }
|
|
4927
|
-
// Small delay to let the clear take effect before Ink renders
|
|
4928
|
-
await new Promise(resolve => setTimeout(resolve, 100));
|
|
5901
|
+
// Ensure cursor is truly at home position and scrollback is clear
|
|
5902
|
+
process.stdout.write('\\x1b[1;1H'); // Force cursor to row 1, column 1
|
|
5903
|
+
// Small delay to let terminal settle
|
|
5904
|
+
await new Promise(resolve => setTimeout(resolve, 50));
|
|
4929
5905
|
// Launch the Ink app
|
|
4930
5906
|
const app = render(React.createElement(DmuxApp, {
|
|
4931
5907
|
panesFile: this.panesFile,
|
|
@@ -4935,7 +5911,8 @@ class Dmux {
|
|
|
4935
5911
|
projectRoot: this.projectRoot,
|
|
4936
5912
|
autoUpdater: this.autoUpdater,
|
|
4937
5913
|
serverPort: serverInfo.port,
|
|
4938
|
-
server: this.server
|
|
5914
|
+
server: this.server,
|
|
5915
|
+
controlPaneId
|
|
4939
5916
|
}), {
|
|
4940
5917
|
exitOnCtrlC: false // Disable automatic exit on Ctrl+C
|
|
4941
5918
|
});
|
|
@@ -4954,6 +5931,26 @@ class Dmux {
|
|
|
4954
5931
|
return false;
|
|
4955
5932
|
}
|
|
4956
5933
|
}
|
|
5934
|
+
isWorktree() {
|
|
5935
|
+
try {
|
|
5936
|
+
// Check if current directory is different from project root
|
|
5937
|
+
const cwd = process.cwd();
|
|
5938
|
+
if (cwd === this.projectRoot) {
|
|
5939
|
+
return false;
|
|
5940
|
+
}
|
|
5941
|
+
// Check if we're in a git worktree by checking if .git is a file (not a directory)
|
|
5942
|
+
const gitPath = path.join(cwd, '.git');
|
|
5943
|
+
if (fsSync.existsSync(gitPath)) {
|
|
5944
|
+
const stats = fsSync.statSync(gitPath);
|
|
5945
|
+
// In a worktree, .git is a file, not a directory
|
|
5946
|
+
return stats.isFile();
|
|
5947
|
+
}
|
|
5948
|
+
return false;
|
|
5949
|
+
}
|
|
5950
|
+
catch {
|
|
5951
|
+
return false;
|
|
5952
|
+
}
|
|
5953
|
+
}
|
|
4957
5954
|
getProjectRoot() {
|
|
4958
5955
|
// Return cached value if available
|
|
4959
5956
|
if (Dmux.cachedProjectRoot) {
|
|
@@ -5155,8 +6152,57 @@ class Dmux {
|
|
|
5155
6152
|
getAutoUpdater() {
|
|
5156
6153
|
return this.autoUpdater;
|
|
5157
6154
|
}
|
|
6155
|
+
setupResizeHook() {
|
|
6156
|
+
try {
|
|
6157
|
+
// Set up session-specific hook that sends SIGUSR1 to dmux process on resize
|
|
6158
|
+
// This works inside tmux where normal SIGWINCH may not propagate
|
|
6159
|
+
const pid = process.pid;
|
|
6160
|
+
execSync(\`tmux set-hook -t '\${this.sessionName}' client-resized 'run-shell "kill -USR1 \${pid} 2>/dev/null || true"'\`, { stdio: 'pipe' });
|
|
6161
|
+
LogService.getInstance().debug(\`Set up resize hook for session \${this.sessionName}\`, 'Setup');
|
|
6162
|
+
}
|
|
6163
|
+
catch (error) {
|
|
6164
|
+
LogService.getInstance().debug('Failed to set up resize hook', 'Setup');
|
|
6165
|
+
}
|
|
6166
|
+
}
|
|
6167
|
+
setupPaneSplitHook() {
|
|
6168
|
+
try {
|
|
6169
|
+
// Set up hook that sends SIGUSR2 to dmux process when a pane is split
|
|
6170
|
+
// This allows us to detect manually created panes via Ctrl+b %
|
|
6171
|
+
const pid = process.pid;
|
|
6172
|
+
execSync(\`tmux set-hook -t '\${this.sessionName}' after-split-window 'run-shell "kill -USR2 \${pid} 2>/dev/null || true"'\`, { stdio: 'pipe' });
|
|
6173
|
+
LogService.getInstance().debug(\`Set up pane split detection hook for session \${this.sessionName}\`, 'Setup');
|
|
6174
|
+
}
|
|
6175
|
+
catch (error) {
|
|
6176
|
+
LogService.getInstance().debug('Failed to set up pane split hook', 'Setup');
|
|
6177
|
+
}
|
|
6178
|
+
}
|
|
6179
|
+
cleanupResizeHook() {
|
|
6180
|
+
try {
|
|
6181
|
+
// Remove session-specific hook
|
|
6182
|
+
execSync(\`tmux set-hook -u -t '\${this.sessionName}' client-resized\`, { stdio: 'pipe' });
|
|
6183
|
+
LogService.getInstance().debug('Cleaned up resize hook', 'Setup');
|
|
6184
|
+
}
|
|
6185
|
+
catch {
|
|
6186
|
+
// Ignore cleanup errors
|
|
6187
|
+
}
|
|
6188
|
+
}
|
|
6189
|
+
cleanupPaneSplitHook() {
|
|
6190
|
+
try {
|
|
6191
|
+
// Remove pane split hook
|
|
6192
|
+
execSync(\`tmux set-hook -u -t '\${this.sessionName}' after-split-window\`, { stdio: 'pipe' });
|
|
6193
|
+
LogService.getInstance().debug('Cleaned up pane split hook', 'Setup');
|
|
6194
|
+
}
|
|
6195
|
+
catch {
|
|
6196
|
+
// Ignore cleanup errors
|
|
6197
|
+
}
|
|
6198
|
+
}
|
|
5158
6199
|
setupGlobalSignalHandlers() {
|
|
5159
6200
|
const cleanTerminalExit = () => {
|
|
6201
|
+
// Clean up hooks
|
|
6202
|
+
if (process.env.TMUX) {
|
|
6203
|
+
this.cleanupResizeHook();
|
|
6204
|
+
this.cleanupPaneSplitHook();
|
|
6205
|
+
}
|
|
5160
6206
|
// Clear screen multiple times to ensure no artifacts
|
|
5161
6207
|
process.stdout.write('\\x1b[2J\\x1b[H'); // Clear screen and move to home
|
|
5162
6208
|
process.stdout.write('\\x1b[3J'); // Clear scrollback buffer
|
|
@@ -5179,6 +6225,14 @@ class Dmux {
|
|
|
5179
6225
|
// Handle Ctrl+C and SIGTERM
|
|
5180
6226
|
process.on('SIGINT', cleanTerminalExit);
|
|
5181
6227
|
process.on('SIGTERM', cleanTerminalExit);
|
|
6228
|
+
// Handle SIGUSR2 for pane split detection
|
|
6229
|
+
// This signal is sent by tmux hook when a new pane is created
|
|
6230
|
+
process.on('SIGUSR2', () => {
|
|
6231
|
+
// Log that a pane split was detected
|
|
6232
|
+
LogService.getInstance().debug('Pane split detected via SIGUSR2, triggering immediate detection', 'shellDetection');
|
|
6233
|
+
// Emit a custom event to trigger immediate shell pane detection
|
|
6234
|
+
process.emit('pane-split-detected');
|
|
6235
|
+
});
|
|
5182
6236
|
// Handle uncaught exceptions and unhandled rejections
|
|
5183
6237
|
process.on('uncaughtException', (error) => {
|
|
5184
6238
|
console.error('Uncaught exception:', error);
|
|
@@ -5194,7 +6248,51 @@ const dmux = new Dmux();
|
|
|
5194
6248
|
dmux.init().catch(() => process.exit(1));
|
|
5195
6249
|
//# sourceMappingURL=index.js.map`,
|
|
5196
6250
|
mimeType: 'application/javascript',
|
|
5197
|
-
size:
|
|
6251
|
+
size: 27883
|
|
6252
|
+
},
|
|
6253
|
+
'spacer-pane.js': {
|
|
6254
|
+
content: `#!/usr/bin/env node
|
|
6255
|
+
"use strict";
|
|
6256
|
+
// Spacer pane - displays only gray dots, no ASCII art
|
|
6257
|
+
// Used to fill empty space in layouts
|
|
6258
|
+
const DIM_GRAY = "\\x1b[38;5;238m"; // Same gray as decorative-pane
|
|
6259
|
+
const RESET = "\\x1b[0m";
|
|
6260
|
+
const FILL_CHAR = "·";
|
|
6261
|
+
function render(width, height) {
|
|
6262
|
+
const lines = [];
|
|
6263
|
+
for (let row = 0; row < height; row++) {
|
|
6264
|
+
let line = "";
|
|
6265
|
+
for (let col = 0; col < width; col++) {
|
|
6266
|
+
line += DIM_GRAY + FILL_CHAR + RESET;
|
|
6267
|
+
}
|
|
6268
|
+
lines.push(line);
|
|
6269
|
+
}
|
|
6270
|
+
// Clear screen and render
|
|
6271
|
+
process.stdout.write("\\x1b[2J\\x1b[H"); // Clear screen and home cursor
|
|
6272
|
+
process.stdout.write(lines.join("\\n"));
|
|
6273
|
+
}
|
|
6274
|
+
// Initial render
|
|
6275
|
+
const initialWidth = process.stdout.columns || 80;
|
|
6276
|
+
const initialHeight = process.stdout.rows || 24;
|
|
6277
|
+
render(initialWidth, initialHeight);
|
|
6278
|
+
// Re-render on terminal resize
|
|
6279
|
+
process.stdout.on("resize", () => {
|
|
6280
|
+
const width = process.stdout.columns || 80;
|
|
6281
|
+
const height = process.stdout.rows || 24;
|
|
6282
|
+
render(width, height);
|
|
6283
|
+
});
|
|
6284
|
+
// Keep the process running
|
|
6285
|
+
process.stdin.resume();
|
|
6286
|
+
// Handle Ctrl+C gracefully
|
|
6287
|
+
process.on("SIGINT", () => {
|
|
6288
|
+
process.exit(0);
|
|
6289
|
+
});
|
|
6290
|
+
process.on("SIGTERM", () => {
|
|
6291
|
+
process.exit(0);
|
|
6292
|
+
});
|
|
6293
|
+
//# sourceMappingURL=spacer-pane.js.map`,
|
|
6294
|
+
mimeType: 'application/javascript',
|
|
6295
|
+
size: 1234
|
|
5198
6296
|
},
|
|
5199
6297
|
'terminal.html': {
|
|
5200
6298
|
content: `<!DOCTYPE html>
|