dmux 3.1.0 → 3.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/CleanTextInput.d.ts +9 -0
- package/dist/CleanTextInput.d.ts.map +1 -1
- package/dist/CleanTextInput.js +127 -47
- package/dist/CleanTextInput.js.map +1 -1
- package/dist/DmuxApp.d.ts +2 -2
- package/dist/DmuxApp.d.ts.map +1 -1
- package/dist/DmuxApp.js +1408 -751
- package/dist/DmuxApp.js.map +1 -1
- package/dist/actions/paneActions.d.ts.map +1 -1
- package/dist/actions/paneActions.js +68 -11
- package/dist/actions/paneActions.js.map +1 -1
- package/dist/actions/types.d.ts +1 -0
- package/dist/actions/types.d.ts.map +1 -1
- package/dist/actions/types.js +1 -0
- package/dist/actions/types.js.map +1 -1
- package/dist/components/ActionInputDialog.d.ts.map +1 -1
- package/dist/components/ActionInputDialog.js +3 -2
- package/dist/components/ActionInputDialog.js.map +1 -1
- package/dist/components/DialogBox.d.ts +16 -0
- package/dist/components/DialogBox.d.ts.map +1 -0
- package/dist/components/DialogBox.js +12 -0
- package/dist/components/DialogBox.js.map +1 -0
- package/dist/components/FooterHelp.d.ts +9 -0
- package/dist/components/FooterHelp.d.ts.map +1 -1
- package/dist/components/FooterHelp.js +41 -5
- package/dist/components/FooterHelp.js.map +1 -1
- package/dist/components/PaneCard.d.ts +3 -1
- package/dist/components/PaneCard.d.ts.map +1 -1
- package/dist/components/PaneCard.js +50 -33
- package/dist/components/PaneCard.js.map +1 -1
- package/dist/components/PanesGrid.d.ts +3 -5
- package/dist/components/PanesGrid.d.ts.map +1 -1
- package/dist/components/PanesGrid.js +40 -10
- package/dist/components/PanesGrid.js.map +1 -1
- package/dist/dashboard.js +1 -1
- package/dist/decorative-pane.d.ts +3 -0
- package/dist/decorative-pane.d.ts.map +1 -0
- package/dist/decorative-pane.js +136 -0
- package/dist/decorative-pane.js.map +1 -0
- package/dist/hooks/useActionSystem.d.ts +14 -1
- package/dist/hooks/useActionSystem.d.ts.map +1 -1
- package/dist/hooks/useActionSystem.js +93 -4
- package/dist/hooks/useActionSystem.js.map +1 -1
- package/dist/hooks/useNavigation.js +1 -1
- package/dist/hooks/useNavigation.js.map +1 -1
- package/dist/hooks/usePaneCreation.d.ts +1 -2
- package/dist/hooks/usePaneCreation.d.ts.map +1 -1
- package/dist/hooks/usePaneCreation.js +30 -29
- package/dist/hooks/usePaneCreation.js.map +1 -1
- package/dist/hooks/usePaneRunner.d.ts.map +1 -1
- package/dist/hooks/usePaneRunner.js +8 -3
- package/dist/hooks/usePaneRunner.js.map +1 -1
- package/dist/hooks/usePanes.d.ts.map +1 -1
- package/dist/hooks/usePanes.js +210 -37
- package/dist/hooks/usePanes.js.map +1 -1
- package/dist/hooks/useWorktreeActions.d.ts.map +1 -1
- package/dist/hooks/useWorktreeActions.js +7 -13
- package/dist/hooks/useWorktreeActions.js.map +1 -1
- package/dist/index.js +217 -29
- package/dist/index.js.map +1 -1
- package/dist/popups/agentChoicePopup.d.ts +7 -0
- package/dist/popups/agentChoicePopup.d.ts.map +1 -0
- package/dist/popups/agentChoicePopup.js +74 -0
- package/dist/popups/agentChoicePopup.js.map +1 -0
- package/dist/popups/choicePopup.d.ts +7 -0
- package/dist/popups/choicePopup.d.ts.map +1 -0
- package/dist/popups/choicePopup.js +64 -0
- package/dist/popups/choicePopup.js.map +1 -0
- package/dist/popups/components/FileList.d.ts +13 -0
- package/dist/popups/components/FileList.d.ts.map +1 -0
- package/dist/popups/components/FileList.js +61 -0
- package/dist/popups/components/FileList.js.map +1 -0
- package/dist/popups/components/PopupContainer.d.ts +14 -0
- package/dist/popups/components/PopupContainer.d.ts.map +1 -0
- package/dist/popups/components/PopupContainer.js +15 -0
- package/dist/popups/components/PopupContainer.js.map +1 -0
- package/dist/popups/components/PopupInputBox.d.ts +11 -0
- package/dist/popups/components/PopupInputBox.d.ts.map +1 -0
- package/dist/popups/components/PopupInputBox.js +10 -0
- package/dist/popups/components/PopupInputBox.js.map +1 -0
- package/dist/popups/components/PopupWrapper.d.ts +37 -0
- package/dist/popups/components/PopupWrapper.d.ts.map +1 -0
- package/dist/popups/components/PopupWrapper.js +88 -0
- package/dist/popups/components/PopupWrapper.js.map +1 -0
- package/dist/popups/components/index.d.ts +8 -0
- package/dist/popups/components/index.d.ts.map +1 -0
- package/dist/popups/components/index.js +8 -0
- package/dist/popups/components/index.js.map +1 -0
- package/dist/popups/config.d.ts +40 -0
- package/dist/popups/config.d.ts.map +1 -0
- package/dist/popups/config.js +40 -0
- package/dist/popups/config.js.map +1 -0
- package/dist/popups/confirmPopup.d.ts +7 -0
- package/dist/popups/confirmPopup.d.ts.map +1 -0
- package/dist/popups/confirmPopup.js +72 -0
- package/dist/popups/confirmPopup.js.map +1 -0
- package/dist/popups/hooksPopup.d.ts +7 -0
- package/dist/popups/hooksPopup.d.ts.map +1 -0
- package/dist/popups/hooksPopup.js +71 -0
- package/dist/popups/hooksPopup.js.map +1 -0
- package/dist/popups/inputPopup.d.ts +7 -0
- package/dist/popups/inputPopup.d.ts.map +1 -0
- package/dist/popups/inputPopup.js +48 -0
- package/dist/popups/inputPopup.js.map +1 -0
- package/dist/popups/kebabMenuPopup.d.ts +7 -0
- package/dist/popups/kebabMenuPopup.d.ts.map +1 -0
- package/dist/popups/kebabMenuPopup.js +52 -0
- package/dist/popups/kebabMenuPopup.js.map +1 -0
- package/dist/popups/logsPopup.d.ts +12 -0
- package/dist/popups/logsPopup.d.ts.map +1 -0
- package/dist/popups/logsPopup.js +364 -0
- package/dist/popups/logsPopup.js.map +1 -0
- package/dist/popups/mergePopup.d.ts +7 -0
- package/dist/popups/mergePopup.d.ts.map +1 -0
- package/dist/popups/mergePopup.js +310 -0
- package/dist/popups/mergePopup.js.map +1 -0
- package/dist/popups/newPanePopup.d.ts +7 -0
- package/dist/popups/newPanePopup.d.ts.map +1 -0
- package/dist/popups/newPanePopup.js +234 -0
- package/dist/popups/newPanePopup.js.map +1 -0
- package/dist/popups/progressPopup.d.ts +8 -0
- package/dist/popups/progressPopup.d.ts.map +1 -0
- package/dist/popups/progressPopup.js +54 -0
- package/dist/popups/progressPopup.js.map +1 -0
- package/dist/popups/remotePopup.d.ts +6 -0
- package/dist/popups/remotePopup.d.ts.map +1 -0
- package/dist/popups/remotePopup.js +166 -0
- package/dist/popups/remotePopup.js.map +1 -0
- package/dist/popups/settingsPopup.d.ts +7 -0
- package/dist/popups/settingsPopup.d.ts.map +1 -0
- package/dist/popups/settingsPopup.js +212 -0
- package/dist/popups/settingsPopup.js.map +1 -0
- package/dist/popups/shortcutsPopup.d.ts +6 -0
- package/dist/popups/shortcutsPopup.d.ts.map +1 -0
- package/dist/popups/shortcutsPopup.js +74 -0
- package/dist/popups/shortcutsPopup.js.map +1 -0
- package/dist/popups/templates/SimpleInputPopup.d.ts +15 -0
- package/dist/popups/templates/SimpleInputPopup.d.ts.map +1 -0
- package/dist/popups/templates/SimpleInputPopup.js +28 -0
- package/dist/popups/templates/SimpleInputPopup.js.map +1 -0
- package/dist/server/embedded-assets.d.ts.map +1 -1
- package/dist/server/embedded-assets.js +2077 -968
- package/dist/server/embedded-assets.js.map +1 -1
- package/dist/server/index.js +1 -1
- package/dist/server/index.js.map +1 -1
- package/dist/server/routes.d.ts +1 -1
- package/dist/server/routes.d.ts.map +1 -1
- package/dist/server/routes.js +73 -6
- package/dist/server/routes.js.map +1 -1
- package/dist/services/ConfigWatcher.d.ts.map +1 -1
- package/dist/services/ConfigWatcher.js +10 -3
- package/dist/services/ConfigWatcher.js.map +1 -1
- package/dist/services/LogService.d.ts +112 -0
- package/dist/services/LogService.d.ts.map +1 -0
- package/dist/services/LogService.js +252 -0
- package/dist/services/LogService.js.map +1 -0
- package/dist/services/PaneWorkerManager.d.ts.map +1 -1
- package/dist/services/PaneWorkerManager.js +35 -9
- package/dist/services/PaneWorkerManager.js.map +1 -1
- package/dist/services/TunnelService.d.ts +1 -0
- package/dist/services/TunnelService.d.ts.map +1 -1
- package/dist/services/TunnelService.js +56 -15
- package/dist/services/TunnelService.js.map +1 -1
- package/dist/shared/StateManager.d.ts +49 -1
- package/dist/shared/StateManager.d.ts.map +1 -1
- package/dist/shared/StateManager.js +97 -2
- package/dist/shared/StateManager.js.map +1 -1
- package/dist/spacer-pane.d.ts +8 -0
- package/dist/spacer-pane.d.ts.map +1 -0
- package/dist/spacer-pane.js +40 -0
- package/dist/spacer-pane.js.map +1 -0
- package/dist/theme/colors.d.ts +25 -0
- package/dist/theme/colors.d.ts.map +1 -0
- package/dist/theme/colors.js +33 -0
- package/dist/theme/colors.js.map +1 -0
- package/dist/types.d.ts +14 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/asciiArt.d.ts +26 -0
- package/dist/utils/asciiArt.d.ts.map +1 -0
- package/dist/utils/asciiArt.js +88 -0
- package/dist/utils/asciiArt.js.map +1 -0
- package/dist/utils/conflictResolutionPane.d.ts.map +1 -1
- package/dist/utils/conflictResolutionPane.js +8 -4
- package/dist/utils/conflictResolutionPane.js.map +1 -1
- package/dist/utils/fileScanner.d.ts +23 -0
- package/dist/utils/fileScanner.d.ts.map +1 -0
- package/dist/utils/fileScanner.js +123 -0
- package/dist/utils/fileScanner.js.map +1 -0
- package/dist/utils/generated-agents-doc.d.ts +1 -1
- package/dist/utils/generated-agents-doc.js +1 -1
- package/dist/utils/hooks.d.ts +3 -3
- package/dist/utils/hooks.d.ts.map +1 -1
- package/dist/utils/hooks.js +70 -41
- package/dist/utils/hooks.js.map +1 -1
- package/dist/utils/hooksDocs.d.ts +1 -1
- package/dist/utils/layoutManager.d.ts +45 -0
- package/dist/utils/layoutManager.d.ts.map +1 -0
- package/dist/utils/layoutManager.js +500 -0
- package/dist/utils/layoutManager.js.map +1 -0
- package/dist/utils/paneCreation.d.ts.map +1 -1
- package/dist/utils/paneCreation.js +125 -11
- package/dist/utils/paneCreation.js.map +1 -1
- package/dist/utils/popup.d.ts +97 -0
- package/dist/utils/popup.d.ts.map +1 -0
- package/dist/utils/popup.js +509 -0
- package/dist/utils/popup.js.map +1 -0
- package/dist/utils/postPaneCleanup.d.ts +12 -0
- package/dist/utils/postPaneCleanup.d.ts.map +1 -0
- package/dist/utils/postPaneCleanup.js +53 -0
- package/dist/utils/postPaneCleanup.js.map +1 -0
- package/dist/utils/shellPaneDetection.d.ts +44 -0
- package/dist/utils/shellPaneDetection.d.ts.map +1 -0
- package/dist/utils/shellPaneDetection.js +175 -0
- package/dist/utils/shellPaneDetection.js.map +1 -0
- package/dist/utils/tmux.d.ts +53 -1
- package/dist/utils/tmux.d.ts.map +1 -1
- package/dist/utils/tmux.js +352 -84
- package/dist/utils/tmux.js.map +1 -1
- package/dist/utils/welcomePane.d.ts +22 -0
- package/dist/utils/welcomePane.d.ts.map +1 -0
- package/dist/utils/welcomePane.js +119 -0
- package/dist/utils/welcomePane.js.map +1 -0
- package/dist/utils/welcomePaneManager.d.ts +36 -0
- package/dist/utils/welcomePaneManager.d.ts.map +1 -0
- package/dist/utils/welcomePaneManager.js +160 -0
- package/dist/utils/welcomePaneManager.js.map +1 -0
- package/dist/workers/PaneWorker.js +2 -0
- package/dist/workers/PaneWorker.js.map +1 -1
- package/package.json +5 -2
- package/dist/components/NewPaneDialog.d.ts +0 -9
- package/dist/components/NewPaneDialog.d.ts.map +0 -1
- package/dist/components/NewPaneDialog.js +0 -11
- package/dist/components/NewPaneDialog.js.map +0 -1
|
@@ -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,195 +1103,245 @@ 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);
|
|
1218
|
+
// Spinner state - shows for a few frames to force render
|
|
1219
|
+
const [showRepaintSpinner, setShowRepaintSpinner] = useState(false);
|
|
1150
1220
|
const { projectSettings, saveSettings } = useProjectSettings(settingsFile);
|
|
1151
|
-
// Hooks management state
|
|
1152
|
-
const [showHooksDialog, setShowHooksDialog] = useState(false);
|
|
1153
|
-
const [hooksSelectedIndex, setHooksSelectedIndex] = useState(0);
|
|
1154
|
-
const [hooksData, setHooksData] = useState([]);
|
|
1155
1221
|
const [showCommandPrompt, setShowCommandPrompt] = useState(null);
|
|
1156
|
-
const [commandInput, setCommandInput] = useState(
|
|
1222
|
+
const [commandInput, setCommandInput] = useState("");
|
|
1157
1223
|
const [showFileCopyPrompt, setShowFileCopyPrompt] = useState(false);
|
|
1158
1224
|
const [currentCommandType, setCurrentCommandType] = useState(null);
|
|
1159
1225
|
const [runningCommand, setRunningCommand] = useState(false);
|
|
1160
1226
|
const [quitConfirmMode, setQuitConfirmMode] = useState(false);
|
|
1161
|
-
const [showKebabMenu, setShowKebabMenu] = useState(false);
|
|
1162
|
-
const [kebabMenuPaneIndex, setKebabMenuPaneIndex] = useState(null);
|
|
1163
|
-
const [kebabMenuOption, setKebabMenuOption] = useState(0);
|
|
1164
|
-
const [kebabMenuActions, setKebabMenuActions] = useState([]);
|
|
1165
1227
|
// Debug message state - for temporary logging messages
|
|
1166
|
-
const [debugMessage, setDebugMessage] = useState(
|
|
1228
|
+
const [debugMessage, setDebugMessage] = useState("");
|
|
1229
|
+
// Current git branch state (for dev builds)
|
|
1230
|
+
const [currentBranch, setCurrentBranch] = useState(null);
|
|
1167
1231
|
// Update state handled by hook
|
|
1168
|
-
const { updateInfo, showUpdateDialog, isUpdating, performUpdate, skipUpdate, dismissUpdate, updateAvailable } = useAutoUpdater(autoUpdater, setStatusMessage);
|
|
1232
|
+
const { updateInfo, showUpdateDialog, isUpdating, performUpdate, skipUpdate, dismissUpdate, updateAvailable, } = useAutoUpdater(autoUpdater, setStatusMessage);
|
|
1169
1233
|
const { exit } = useApp();
|
|
1234
|
+
// Flag to ignore input temporarily after popup closes (prevents buffered keys)
|
|
1235
|
+
const [ignoreInput, setIgnoreInput] = useState(false);
|
|
1170
1236
|
// Agent selection state
|
|
1171
1237
|
const { availableAgents } = useAgentDetection();
|
|
1172
|
-
const [showAgentChoiceDialog, setShowAgentChoiceDialog] = useState(false);
|
|
1173
1238
|
const [agentChoice, setAgentChoice] = useState(null);
|
|
1174
|
-
|
|
1239
|
+
// Popup support detection
|
|
1240
|
+
const [popupsSupported, setPopupsSupported] = useState(false);
|
|
1175
1241
|
// Track terminal dimensions for responsive layout
|
|
1176
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
|
+
}, []);
|
|
1177
1267
|
// Panes state and persistence (skipLoading will be updated after actionSystem is initialized)
|
|
1178
1268
|
const { panes, setPanes, isLoading, loadPanes, savePanes } = usePanes(panesFile, false);
|
|
1179
1269
|
// Track intentionally closed panes to prevent race condition
|
|
1180
1270
|
// When a user closes a pane, we add it to this set. If the worker detects
|
|
1181
1271
|
// the pane is gone (which it will), we check this set first before re-saving.
|
|
1182
1272
|
const intentionallyClosedPanes = React.useRef(new Set());
|
|
1183
|
-
// Action system
|
|
1184
|
-
const actionSystem = useActionSystem({
|
|
1185
|
-
panes,
|
|
1186
|
-
savePanes,
|
|
1187
|
-
sessionName,
|
|
1188
|
-
projectName,
|
|
1189
|
-
onPaneRemove: (paneId) => {
|
|
1190
|
-
// Mark this pane as intentionally closed
|
|
1191
|
-
intentionallyClosedPanes.current.add(paneId);
|
|
1192
|
-
const updated = panes.filter(p => p.id !== paneId);
|
|
1193
|
-
setPanes(updated);
|
|
1194
|
-
// Clean up the tracking after a delay (in case of race conditions)
|
|
1195
|
-
setTimeout(() => {
|
|
1196
|
-
intentionallyClosedPanes.current.delete(paneId);
|
|
1197
|
-
}, 5000);
|
|
1198
|
-
},
|
|
1199
|
-
});
|
|
1200
1273
|
// Pane runner
|
|
1201
|
-
const { copyNonGitFiles, runCommandInternal, monitorTestOutput, monitorDevOutput, attachBackgroundWindow } = usePaneRunner({
|
|
1274
|
+
const { copyNonGitFiles, runCommandInternal, monitorTestOutput, monitorDevOutput, attachBackgroundWindow, } = usePaneRunner({
|
|
1202
1275
|
panes,
|
|
1203
1276
|
savePanes,
|
|
1204
1277
|
projectSettings,
|
|
1205
1278
|
setStatusMessage,
|
|
1206
1279
|
setRunningCommand,
|
|
1207
1280
|
});
|
|
1208
|
-
// Force repaint helper
|
|
1209
|
-
const forceRepaint = () =>
|
|
1281
|
+
// Force repaint helper - shows spinner for a few frames to force full re-render
|
|
1282
|
+
const forceRepaint = () => {
|
|
1283
|
+
setForceRepaintTrigger((prev) => prev + 1);
|
|
1284
|
+
setShowRepaintSpinner(true);
|
|
1285
|
+
// Hide spinner after a few frames (enough to trigger multiple renders)
|
|
1286
|
+
setTimeout(() => setShowRepaintSpinner(false), 100);
|
|
1287
|
+
};
|
|
1210
1288
|
// Force repaint effect - ensures Ink re-renders when trigger changes
|
|
1211
1289
|
useEffect(() => {
|
|
1212
1290
|
if (forceRepaintTrigger > 0) {
|
|
1213
1291
|
// Small delay to ensure terminal is ready
|
|
1214
1292
|
const timer = setTimeout(() => {
|
|
1215
1293
|
try {
|
|
1216
|
-
execSync(
|
|
1294
|
+
execSync("tmux refresh-client", { stdio: "pipe" });
|
|
1217
1295
|
}
|
|
1218
1296
|
catch { }
|
|
1219
1297
|
}, 50);
|
|
1220
1298
|
return () => clearTimeout(timer);
|
|
1221
1299
|
}
|
|
1222
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]);
|
|
1223
1345
|
// Pane creation
|
|
1224
1346
|
const { createNewPane: createNewPaneHook } = usePaneCreation({
|
|
1225
1347
|
panes,
|
|
@@ -1227,7 +1349,6 @@ const DmuxApp = ({ panesFile, projectName, sessionName, settingsFile, projectRoo
|
|
|
1227
1349
|
projectName,
|
|
1228
1350
|
setIsCreatingPane,
|
|
1229
1351
|
setStatusMessage,
|
|
1230
|
-
setNewPanePrompt,
|
|
1231
1352
|
loadPanes,
|
|
1232
1353
|
panesFile,
|
|
1233
1354
|
availableAgents,
|
|
@@ -1237,8 +1358,8 @@ const DmuxApp = ({ panesFile, projectName, sessionName, settingsFile, projectRoo
|
|
|
1237
1358
|
useEffect(() => {
|
|
1238
1359
|
const statusDetector = getStatusDetector();
|
|
1239
1360
|
const handleStatusUpdate = (event) => {
|
|
1240
|
-
setPanes(prevPanes => {
|
|
1241
|
-
const updatedPanes = prevPanes.map(pane => {
|
|
1361
|
+
setPanes((prevPanes) => {
|
|
1362
|
+
const updatedPanes = prevPanes.map((pane) => {
|
|
1242
1363
|
if (pane.id === event.paneId) {
|
|
1243
1364
|
const updated = {
|
|
1244
1365
|
...pane,
|
|
@@ -1262,22 +1383,23 @@ const DmuxApp = ({ panesFile, projectName, sessionName, settingsFile, projectRoo
|
|
|
1262
1383
|
updated.analyzerError = event.analyzerError;
|
|
1263
1384
|
}
|
|
1264
1385
|
// Clear option dialog data when transitioning away from 'waiting' state
|
|
1265
|
-
if (event.status !==
|
|
1386
|
+
if (event.status !== "waiting" && pane.agentStatus === "waiting") {
|
|
1266
1387
|
updated.optionsQuestion = undefined;
|
|
1267
1388
|
updated.options = undefined;
|
|
1268
1389
|
updated.potentialHarm = undefined;
|
|
1269
1390
|
}
|
|
1270
1391
|
// Clear summary when transitioning away from 'idle' state
|
|
1271
|
-
if (event.status !==
|
|
1392
|
+
if (event.status !== "idle" && pane.agentStatus === "idle") {
|
|
1272
1393
|
updated.agentSummary = undefined;
|
|
1273
1394
|
}
|
|
1274
1395
|
// Clear analyzer error when successfully getting a new analysis
|
|
1275
1396
|
// or when transitioning to 'working' status
|
|
1276
|
-
if (event.status ===
|
|
1397
|
+
if (event.status === "working") {
|
|
1277
1398
|
updated.analyzerError = undefined;
|
|
1278
1399
|
}
|
|
1279
|
-
else if (event.status ===
|
|
1280
|
-
if (event.analyzerError === undefined &&
|
|
1400
|
+
else if (event.status === "waiting" || event.status === "idle") {
|
|
1401
|
+
if (event.analyzerError === undefined &&
|
|
1402
|
+
(event.optionsQuestion || event.summary)) {
|
|
1281
1403
|
updated.analyzerError = undefined;
|
|
1282
1404
|
}
|
|
1283
1405
|
}
|
|
@@ -1286,15 +1408,15 @@ const DmuxApp = ({ panesFile, projectName, sessionName, settingsFile, projectRoo
|
|
|
1286
1408
|
return pane;
|
|
1287
1409
|
});
|
|
1288
1410
|
// Persist to disk - ConfigWatcher will handle syncing to StateManager
|
|
1289
|
-
savePanes(updatedPanes).catch(err => {
|
|
1290
|
-
console.error(
|
|
1411
|
+
savePanes(updatedPanes).catch((err) => {
|
|
1412
|
+
console.error("Failed to save panes after status update:", err);
|
|
1291
1413
|
});
|
|
1292
1414
|
return updatedPanes;
|
|
1293
1415
|
});
|
|
1294
1416
|
};
|
|
1295
|
-
statusDetector.on(
|
|
1417
|
+
statusDetector.on("status-updated", handleStatusUpdate);
|
|
1296
1418
|
return () => {
|
|
1297
|
-
statusDetector.off(
|
|
1419
|
+
statusDetector.off("status-updated", handleStatusUpdate);
|
|
1298
1420
|
};
|
|
1299
1421
|
}, [setPanes, savePanes]);
|
|
1300
1422
|
// Note: No need to sync panes with StateManager here.
|
|
@@ -1319,62 +1441,29 @@ const DmuxApp = ({ panesFile, projectName, sessionName, settingsFile, projectRoo
|
|
|
1319
1441
|
const handleTermination = () => {
|
|
1320
1442
|
cleanExit();
|
|
1321
1443
|
};
|
|
1322
|
-
process.on(
|
|
1323
|
-
//
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
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
|
+
}
|
|
1328
1451
|
return () => {
|
|
1329
|
-
process.removeListener(
|
|
1452
|
+
process.removeListener("SIGTERM", handleTermination);
|
|
1330
1453
|
};
|
|
1331
1454
|
}, []);
|
|
1332
|
-
// Auto-show new pane dialog when starting with no panes
|
|
1333
|
-
useEffect(() => {
|
|
1334
|
-
// Only show the dialog if:
|
|
1335
|
-
// 1. Initial load is complete (!isLoading)
|
|
1336
|
-
// 2. We have no panes
|
|
1337
|
-
// 3. We're not already showing the dialog
|
|
1338
|
-
// 4. We're not showing any other dialogs or prompts
|
|
1339
|
-
if (!isLoading &&
|
|
1340
|
-
panes.length === 0 &&
|
|
1341
|
-
!showNewPaneDialog &&
|
|
1342
|
-
!actionSystem.actionState.showConfirmDialog &&
|
|
1343
|
-
!actionSystem.actionState.showChoiceDialog &&
|
|
1344
|
-
!actionSystem.actionState.showInputDialog &&
|
|
1345
|
-
!actionSystem.actionState.showProgressDialog &&
|
|
1346
|
-
!showCommandPrompt &&
|
|
1347
|
-
!showFileCopyPrompt &&
|
|
1348
|
-
!showAgentChoiceDialog &&
|
|
1349
|
-
!isCreatingPane &&
|
|
1350
|
-
!runningCommand &&
|
|
1351
|
-
!isUpdating) {
|
|
1352
|
-
setShowNewPaneDialog(true);
|
|
1353
|
-
}
|
|
1354
|
-
}, [isLoading, panes.length, showNewPaneDialog, actionSystem.actionState.showConfirmDialog, actionSystem.actionState.showChoiceDialog, actionSystem.actionState.showInputDialog, actionSystem.actionState.showProgressDialog, showCommandPrompt, showFileCopyPrompt, showAgentChoiceDialog, isCreatingPane, runningCommand, isUpdating]);
|
|
1355
1455
|
// Update checking moved to useAutoUpdater
|
|
1356
1456
|
// Set default agent choice when detection completes
|
|
1357
1457
|
useEffect(() => {
|
|
1358
1458
|
if (agentChoice == null && availableAgents.length > 0) {
|
|
1359
|
-
setAgentChoice(availableAgents[0] ||
|
|
1459
|
+
setAgentChoice(availableAgents[0] || "claude");
|
|
1360
1460
|
}
|
|
1361
1461
|
}, [availableAgents]);
|
|
1362
|
-
//
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
// Check if this pane was intentionally closed
|
|
1368
|
-
// If so, don't re-save - the close action already handled it
|
|
1369
|
-
if (intentionallyClosedPanes.current.has(paneId)) {
|
|
1370
|
-
return;
|
|
1371
|
-
}
|
|
1372
|
-
// Pane was removed unexpectedly (e.g., user killed tmux pane manually)
|
|
1373
|
-
// Remove it from our tracking
|
|
1374
|
-
const updatedPanes = panes.filter(p => p.id !== paneId);
|
|
1375
|
-
savePanes(updatedPanes);
|
|
1376
|
-
},
|
|
1377
|
-
});
|
|
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!
|
|
1378
1467
|
// loadPanes moved to usePanes
|
|
1379
1468
|
// getPanePositions moved to utils/tmux
|
|
1380
1469
|
// Navigation logic moved to hook
|
|
@@ -1382,136 +1471,1056 @@ const DmuxApp = ({ panesFile, projectName, sessionName, settingsFile, projectRoo
|
|
|
1382
1471
|
// findCardInDirection provided by useNavigation
|
|
1383
1472
|
// savePanes moved to usePanes
|
|
1384
1473
|
// applySmartLayout moved to utils/tmux
|
|
1385
|
-
const
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
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
|
+
}
|
|
1392
1481
|
try {
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
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
|
+
}
|
|
1397
1539
|
}
|
|
1398
|
-
catch {
|
|
1399
|
-
|
|
1400
|
-
|
|
1540
|
+
catch (error) {
|
|
1541
|
+
setStatusMessage(\`Failed to launch popup: \${error.message}\`);
|
|
1542
|
+
setTimeout(() => setStatusMessage(""), 3000);
|
|
1401
1543
|
}
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
//
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
// 2. Fill with blank lines to push content off screen
|
|
1410
|
-
process.stdout.write('\\n'.repeat(100));
|
|
1411
|
-
// 3. Clear tmux history and send clear command
|
|
1412
|
-
try {
|
|
1413
|
-
execSync('tmux clear-history', { stdio: 'pipe' });
|
|
1414
|
-
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;
|
|
1415
1551
|
}
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
// 4. Force tmux to refresh the display
|
|
1420
|
-
try {
|
|
1421
|
-
execSync('tmux refresh-client', { stdio: 'pipe' });
|
|
1552
|
+
const selectedPane = panes[paneIndex];
|
|
1553
|
+
if (!selectedPane) {
|
|
1554
|
+
return;
|
|
1422
1555
|
}
|
|
1423
|
-
catch { }
|
|
1424
|
-
// Get current pane count to determine layout
|
|
1425
|
-
const paneCount = parseInt(execSync('tmux list-panes | wc -l', { encoding: 'utf-8' }).trim());
|
|
1426
|
-
// Enable pane borders to show titles
|
|
1427
1556
|
try {
|
|
1428
|
-
|
|
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
|
+
}
|
|
1429
1614
|
}
|
|
1430
|
-
catch {
|
|
1431
|
-
|
|
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;
|
|
1432
1626
|
}
|
|
1433
|
-
// Create new pane
|
|
1434
|
-
const paneInfo = execSync(\`tmux split-window -h -P -F '#{pane_id}'\`, { encoding: 'utf-8' }).trim();
|
|
1435
|
-
// Wait for pane creation to settle
|
|
1436
|
-
await new Promise(resolve => setTimeout(resolve, 500));
|
|
1437
|
-
// Set pane title to match the slug
|
|
1438
1627
|
try {
|
|
1439
|
-
|
|
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
|
+
}
|
|
1440
1663
|
}
|
|
1441
|
-
catch {
|
|
1442
|
-
|
|
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;
|
|
1443
1676
|
}
|
|
1444
|
-
// Apply smart layout based on pane count
|
|
1445
|
-
const newPaneCount = paneCount + 1;
|
|
1446
|
-
applySmartLayout(newPaneCount);
|
|
1447
|
-
// Create git worktree and cd into it
|
|
1448
|
-
// This MUST happen before launching Claude to ensure we're in the right directory
|
|
1449
1677
|
try {
|
|
1450
|
-
//
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
//
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
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
|
+
}
|
|
1461
1706
|
}
|
|
1462
1707
|
catch (error) {
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
// Even if worktree creation failed, try to cd to the directory in case it exists
|
|
1466
|
-
execSync(\`tmux send-keys -t '\${paneInfo}' 'cd "\${worktreePath}" 2>/dev/null || (echo "ERROR: Failed to create/enter worktree \${slug}" && pwd)' Enter\`, { stdio: 'pipe' });
|
|
1467
|
-
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
1708
|
+
setStatusMessage(\`Failed to launch popup: \${error.message}\`);
|
|
1709
|
+
setTimeout(() => setStatusMessage(""), 3000);
|
|
1468
1710
|
}
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
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
|
+
}
|
|
1481
1782
|
}
|
|
1482
|
-
else {
|
|
1483
|
-
|
|
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);
|
|
1484
1795
|
}
|
|
1485
|
-
// Send Claude command to new pane
|
|
1486
|
-
escapedCmd = claudeCmd.replace(/'/g, "'\\\\''");
|
|
1487
|
-
execSync(\`tmux send-keys -t '\${paneInfo}' '\${escapedCmd}'\`, { stdio: 'pipe' });
|
|
1488
|
-
execSync(\`tmux send-keys -t '\${paneInfo}' Enter\`, { stdio: 'pipe' });
|
|
1489
1796
|
}
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
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();
|
|
1505
1846
|
}
|
|
1506
1847
|
}
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
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 = "";
|
|
1515
2524
|
let stableContentCount = 0;
|
|
1516
2525
|
let promptHandled = false;
|
|
1517
2526
|
// More comprehensive trust prompt patterns
|
|
@@ -1540,14 +2549,15 @@ const DmuxApp = ({ panesFile, projectName, sessionName, settingsFile, projectRoo
|
|
|
1540
2549
|
/❯\\s*1\\.\\s*Yes,\\s*proceed/i, // New Claude numbered menu format
|
|
1541
2550
|
/Enter to confirm.*Esc to exit/i, // New Claude confirmation format
|
|
1542
2551
|
/1\\.\\s*Yes,\\s*proceed/i, // Yes proceed option
|
|
1543
|
-
/2\\.\\s*No,\\s*exit/i // No exit option
|
|
2552
|
+
/2\\.\\s*No,\\s*exit/i, // No exit option
|
|
1544
2553
|
];
|
|
1545
2554
|
for (let i = 0; i < maxChecks; i++) {
|
|
1546
|
-
await new Promise(resolve => setTimeout(resolve, checkInterval));
|
|
2555
|
+
await new Promise((resolve) => setTimeout(resolve, checkInterval));
|
|
1547
2556
|
try {
|
|
1548
2557
|
// Capture the pane content
|
|
1549
2558
|
const paneContent = capturePaneContent(paneInfo, 30);
|
|
1550
|
-
if (i % 10 === 0) {
|
|
2559
|
+
if (i % 10 === 0) {
|
|
2560
|
+
// Log every 10 checks (every second)
|
|
1551
2561
|
}
|
|
1552
2562
|
// Check if content has stabilized (same for 3 checks = prompt is waiting)
|
|
1553
2563
|
if (paneContent === lastContent) {
|
|
@@ -1558,14 +2568,15 @@ const DmuxApp = ({ panesFile, projectName, sessionName, settingsFile, projectRoo
|
|
|
1558
2568
|
lastContent = paneContent;
|
|
1559
2569
|
}
|
|
1560
2570
|
// Look for trust prompt in the current content
|
|
1561
|
-
const hasTrustPrompt = trustPromptPatterns.some(pattern => pattern.test(paneContent));
|
|
2571
|
+
const hasTrustPrompt = trustPromptPatterns.some((pattern) => pattern.test(paneContent));
|
|
1562
2572
|
// Also check if we see specific Claude permission text
|
|
1563
|
-
const hasClaudePermissionPrompt = paneContent.includes(
|
|
1564
|
-
paneContent.includes(
|
|
1565
|
-
paneContent.includes(
|
|
1566
|
-
paneContent.includes(
|
|
1567
|
-
(paneContent.includes(
|
|
1568
|
-
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) {
|
|
1569
2580
|
// Content is stable and we found a prompt
|
|
1570
2581
|
if (stableContentCount >= 2) {
|
|
1571
2582
|
// Check if this is the new Claude numbered menu format
|
|
@@ -1573,44 +2584,57 @@ const DmuxApp = ({ panesFile, projectName, sessionName, settingsFile, projectRoo
|
|
|
1573
2584
|
/Enter to confirm.*Esc to exit/i.test(paneContent);
|
|
1574
2585
|
if (isNewClaudeFormat) {
|
|
1575
2586
|
// For new Claude format, just press Enter to confirm default "Yes, proceed"
|
|
1576
|
-
execSync(\`tmux send-keys -t '\${paneInfo}' Enter\`, {
|
|
2587
|
+
execSync(\`tmux send-keys -t '\${paneInfo}' Enter\`, {
|
|
2588
|
+
stdio: "pipe",
|
|
2589
|
+
});
|
|
1577
2590
|
}
|
|
1578
2591
|
else {
|
|
1579
2592
|
// Try multiple response methods for older formats
|
|
1580
2593
|
// Method 1: Send 'y' followed by Enter (most explicit)
|
|
1581
|
-
execSync(\`tmux send-keys -t '\${paneInfo}' 'y'\`, {
|
|
1582
|
-
|
|
1583
|
-
|
|
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
|
+
});
|
|
1584
2601
|
// Method 2: Just Enter (if it's a yes/no with default yes)
|
|
1585
|
-
await new Promise(resolve => setTimeout(resolve, 100));
|
|
1586
|
-
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
|
+
});
|
|
1587
2606
|
}
|
|
1588
2607
|
// Mark as handled to avoid duplicate responses
|
|
1589
2608
|
promptHandled = true;
|
|
1590
2609
|
// Wait and check if prompt is gone
|
|
1591
|
-
await new Promise(resolve => setTimeout(resolve, 500));
|
|
2610
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
1592
2611
|
// Verify the prompt is gone
|
|
1593
2612
|
const updatedContent = capturePaneContent(paneInfo, 10);
|
|
1594
2613
|
// If trust prompt is gone, check if we need to resend the Claude command
|
|
1595
|
-
const promptGone = !trustPromptPatterns.some(p => p.test(updatedContent));
|
|
2614
|
+
const promptGone = !trustPromptPatterns.some((p) => p.test(updatedContent));
|
|
1596
2615
|
if (promptGone) {
|
|
1597
2616
|
// Check if Claude is running or if we need to restart it
|
|
1598
|
-
const claudeRunning = updatedContent.includes(
|
|
1599
|
-
updatedContent.includes(
|
|
1600
|
-
updatedContent.includes(
|
|
1601
|
-
(prompt &&
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
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
|
+
});
|
|
1606
2628
|
}
|
|
1607
2629
|
break;
|
|
1608
2630
|
}
|
|
1609
2631
|
}
|
|
1610
2632
|
}
|
|
1611
2633
|
// If we see Claude is already running without prompts, we're done
|
|
1612
|
-
if (!hasTrustPrompt &&
|
|
1613
|
-
|
|
2634
|
+
if (!hasTrustPrompt &&
|
|
2635
|
+
!hasClaudePermissionPrompt &&
|
|
2636
|
+
(paneContent.includes("Claude") ||
|
|
2637
|
+
paneContent.includes("Assistant"))) {
|
|
1614
2638
|
break;
|
|
1615
2639
|
}
|
|
1616
2640
|
}
|
|
@@ -1620,37 +2644,35 @@ const DmuxApp = ({ panesFile, projectName, sessionName, settingsFile, projectRoo
|
|
|
1620
2644
|
}
|
|
1621
2645
|
};
|
|
1622
2646
|
// Start monitoring for trust prompt in background
|
|
1623
|
-
autoApproveTrust().catch(err => {
|
|
1624
|
-
});
|
|
2647
|
+
autoApproveTrust().catch((err) => { });
|
|
1625
2648
|
}
|
|
1626
2649
|
// Keep focus on the new pane
|
|
1627
|
-
execSync(\`tmux select-pane -t '\${paneInfo}'\`, { stdio:
|
|
2650
|
+
execSync(\`tmux select-pane -t '\${paneInfo}'\`, { stdio: "pipe" });
|
|
1628
2651
|
// Save pane info
|
|
1629
2652
|
const newPane = {
|
|
1630
2653
|
id: \`dmux-\${Date.now()}\`,
|
|
1631
2654
|
slug,
|
|
1632
|
-
prompt: prompt ||
|
|
2655
|
+
prompt: prompt || "No initial prompt",
|
|
1633
2656
|
paneId: paneInfo,
|
|
1634
2657
|
worktreePath,
|
|
1635
|
-
agent
|
|
2658
|
+
agent,
|
|
1636
2659
|
};
|
|
1637
2660
|
const updatedPanes = [...panes, newPane];
|
|
1638
2661
|
await savePanes(updatedPanes);
|
|
1639
2662
|
// Switch back to the original pane (where dmux is running)
|
|
1640
|
-
execSync(\`tmux select-pane -t '\${originalPaneId}'\`, { stdio:
|
|
2663
|
+
execSync(\`tmux select-pane -t '\${originalPaneId}'\`, { stdio: "pipe" });
|
|
1641
2664
|
// Re-set the title for the dmux pane
|
|
1642
2665
|
try {
|
|
1643
|
-
execSync(\`tmux select-pane -t '\${originalPaneId}' -T "dmux
|
|
2666
|
+
execSync(\`tmux select-pane -t '\${originalPaneId}' -T "dmux v\${packageJson.version} - \${projectName}"\`, { stdio: "pipe" });
|
|
1644
2667
|
}
|
|
1645
2668
|
catch {
|
|
1646
2669
|
// Ignore if setting title fails
|
|
1647
2670
|
}
|
|
1648
2671
|
// Clear the screen and redraw the UI
|
|
1649
|
-
process.stdout.write(
|
|
2672
|
+
process.stdout.write("\\x1b[2J\\x1b[H");
|
|
1650
2673
|
// Reset the creating pane flag and refresh
|
|
1651
2674
|
setIsCreatingPane(false);
|
|
1652
|
-
setStatusMessage(
|
|
1653
|
-
setNewPanePrompt('');
|
|
2675
|
+
setStatusMessage("");
|
|
1654
2676
|
// Force a reload of panes to ensure UI is up to date
|
|
1655
2677
|
await loadPanes();
|
|
1656
2678
|
};
|
|
@@ -1658,28 +2680,32 @@ const DmuxApp = ({ panesFile, projectName, sessionName, settingsFile, projectRoo
|
|
|
1658
2680
|
try {
|
|
1659
2681
|
// Enable pane borders to show titles (if not already enabled)
|
|
1660
2682
|
try {
|
|
1661
|
-
execSync(\`tmux set-option -g pane-border-status top\`, { stdio:
|
|
2683
|
+
execSync(\`tmux set-option -g pane-border-status top\`, { stdio: "pipe" });
|
|
1662
2684
|
}
|
|
1663
2685
|
catch {
|
|
1664
2686
|
// Ignore if already set or fails
|
|
1665
2687
|
}
|
|
1666
|
-
execSync(\`tmux select-pane -t '\${paneId}'\`, { stdio:
|
|
1667
|
-
|
|
1668
|
-
|
|
2688
|
+
execSync(\`tmux select-pane -t '\${paneId}'\`, { stdio: "pipe" });
|
|
2689
|
+
// Clear screen after jump to remove artifacts
|
|
2690
|
+
clearScreen();
|
|
2691
|
+
setStatusMessage("Jumped to pane");
|
|
2692
|
+
setTimeout(() => setStatusMessage(""), 2000);
|
|
1669
2693
|
}
|
|
1670
2694
|
catch {
|
|
1671
|
-
setStatusMessage(
|
|
1672
|
-
setTimeout(() => setStatusMessage(
|
|
2695
|
+
setStatusMessage("Failed to jump - pane may be closed");
|
|
2696
|
+
setTimeout(() => setStatusMessage(""), 2000);
|
|
1673
2697
|
}
|
|
1674
2698
|
};
|
|
1675
2699
|
const runCommand = async (type, pane) => {
|
|
1676
2700
|
if (!pane.worktreePath) {
|
|
1677
|
-
setStatusMessage(
|
|
1678
|
-
setTimeout(() => setStatusMessage(
|
|
2701
|
+
setStatusMessage("No worktree path for this pane");
|
|
2702
|
+
setTimeout(() => setStatusMessage(""), 2000);
|
|
1679
2703
|
return;
|
|
1680
2704
|
}
|
|
1681
|
-
const command = type ===
|
|
1682
|
-
const isFirstRun = type ===
|
|
2705
|
+
const command = type === "test" ? projectSettings.testCommand : projectSettings.devCommand;
|
|
2706
|
+
const isFirstRun = type === "test"
|
|
2707
|
+
? !projectSettings.firstTestRun
|
|
2708
|
+
: !projectSettings.firstDevRun;
|
|
1683
2709
|
if (!command) {
|
|
1684
2710
|
// No command configured, prompt user
|
|
1685
2711
|
setShowCommandPrompt(type);
|
|
@@ -1698,444 +2724,139 @@ const DmuxApp = ({ panesFile, projectName, sessionName, settingsFile, projectRoo
|
|
|
1698
2724
|
setRunningCommand(true);
|
|
1699
2725
|
setStatusMessage(\`Starting \${type} in background window...\`);
|
|
1700
2726
|
// Kill existing window if present
|
|
1701
|
-
const existingWindowId = type ===
|
|
2727
|
+
const existingWindowId = type === "test" ? pane.testWindowId : pane.devWindowId;
|
|
1702
2728
|
if (existingWindowId) {
|
|
1703
2729
|
try {
|
|
1704
|
-
execSync(\`tmux kill-window -t '\${existingWindowId}'\`, {
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
}
|
|
1708
|
-
// Create a new background window for the command
|
|
1709
|
-
const windowName = \`\${pane.slug}-\${type}\`;
|
|
1710
|
-
const windowId = execSync(\`tmux new-window -d -n '\${windowName}' -P -F '#{window_id}'\`, { encoding: 'utf-8', stdio: 'pipe' }).trim();
|
|
1711
|
-
// Create a log file to capture output
|
|
1712
|
-
const logFile = \`/tmp/dmux-\${pane.id}-\${type}.log\`;
|
|
1713
|
-
// Build the command with output capture
|
|
1714
|
-
const fullCommand = type === 'test'
|
|
1715
|
-
? \`cd "\${pane.worktreePath}" && \${command} 2>&1 | tee \${logFile}\`
|
|
1716
|
-
: \`cd "\${pane.worktreePath}" && \${command} 2>&1 | tee \${logFile}\`;
|
|
1717
|
-
// Send the command to the new window
|
|
1718
|
-
execSync(\`tmux send-keys -t '\${windowId}' '\${fullCommand.replace(/'/g, "'\\\\''")}'\`, { stdio: 'pipe' });
|
|
1719
|
-
execSync(\`tmux send-keys -t '\${windowId}' Enter\`, { stdio: 'pipe' });
|
|
1720
|
-
// Update pane with window info
|
|
1721
|
-
const updatedPane = {
|
|
1722
|
-
...pane,
|
|
1723
|
-
[type === 'test' ? 'testWindowId' : 'devWindowId']: windowId,
|
|
1724
|
-
[type === 'test' ? 'testStatus' : 'devStatus']: 'running'
|
|
1725
|
-
};
|
|
1726
|
-
const updatedPanes = panes.map(p => p.id === pane.id ? updatedPane : p);
|
|
1727
|
-
await savePanes(updatedPanes);
|
|
1728
|
-
// Start monitoring the output
|
|
1729
|
-
if (type === 'test') {
|
|
1730
|
-
// For tests, monitor for completion
|
|
1731
|
-
setTimeout(() => monitorTestOutput(pane.id, logFile), 2000);
|
|
1732
|
-
}
|
|
1733
|
-
else {
|
|
1734
|
-
// For dev, monitor for server URL
|
|
1735
|
-
setTimeout(() => monitorDevOutput(pane.id, logFile), 2000);
|
|
1736
|
-
}
|
|
1737
|
-
setRunningCommand(false);
|
|
1738
|
-
setStatusMessage(\`\${type === 'test' ? 'Test' : 'Dev server'} started in background\`);
|
|
1739
|
-
setTimeout(() => setStatusMessage(''), 3000);
|
|
1740
|
-
}
|
|
1741
|
-
catch (error) {
|
|
1742
|
-
setRunningCommand(false);
|
|
1743
|
-
setStatusMessage(\`Failed to run \${type} command\`);
|
|
1744
|
-
setTimeout(() => setStatusMessage(''), 3000);
|
|
1745
|
-
}
|
|
1746
|
-
};
|
|
1747
|
-
// Update handling moved to useAutoUpdater
|
|
1748
|
-
// Helper function to clear screen artifacts
|
|
1749
|
-
const clearScreen = () => {
|
|
1750
|
-
// CRITICAL: Force Ink to re-render FIRST, before clearing
|
|
1751
|
-
// This prevents blank screen by ensuring React starts rendering immediately
|
|
1752
|
-
setForceRepaintTrigger(prev => prev + 1);
|
|
1753
|
-
// Multiple clearing strategies to prevent artifacts
|
|
1754
|
-
// 1. Clear screen with ANSI codes
|
|
1755
|
-
process.stdout.write('\\x1b[2J\\x1b[H');
|
|
1756
|
-
// 2. Clear tmux history
|
|
1757
|
-
try {
|
|
1758
|
-
execSync('tmux clear-history', { stdio: 'pipe' });
|
|
1759
|
-
}
|
|
1760
|
-
catch { }
|
|
1761
|
-
// 3. Force tmux to refresh the display
|
|
1762
|
-
try {
|
|
1763
|
-
execSync('tmux refresh-client', { stdio: 'pipe' });
|
|
1764
|
-
}
|
|
1765
|
-
catch { }
|
|
1766
|
-
};
|
|
1767
|
-
// Cleanup function for exit
|
|
1768
|
-
const cleanExit = () => {
|
|
1769
|
-
// Clear screen before exiting Ink
|
|
1770
|
-
process.stdout.write('\\x1b[2J\\x1b[3J\\x1b[H');
|
|
1771
|
-
// Exit the Ink app (this cleans up the React tree)
|
|
1772
|
-
exit();
|
|
1773
|
-
// Give Ink a moment to clean up its rendering, then do final cleanup
|
|
1774
|
-
setTimeout(() => {
|
|
1775
|
-
// Multiple aggressive clearing strategies
|
|
1776
|
-
process.stdout.write('\\x1b[2J\\x1b[H'); // Clear screen and move cursor to home
|
|
1777
|
-
process.stdout.write('\\x1b[3J'); // Clear scrollback buffer
|
|
1778
|
-
process.stdout.write('\\x1b[0m'); // Reset all attributes
|
|
1779
|
-
// Clear tmux history and pane
|
|
1780
|
-
try {
|
|
1781
|
-
execSync('tmux clear-history', { stdio: 'pipe' });
|
|
1782
|
-
execSync('tmux send-keys C-l', { stdio: 'pipe' });
|
|
1783
|
-
}
|
|
1784
|
-
catch { }
|
|
1785
|
-
// One more final clear
|
|
1786
|
-
process.stdout.write('\\x1b[2J\\x1b[H');
|
|
1787
|
-
// Show clean goodbye message
|
|
1788
|
-
process.stdout.write('\\n Run dmux again to resume. Goodbye 👋\\n\\n');
|
|
1789
|
-
// Exit process
|
|
1790
|
-
process.exit(0);
|
|
1791
|
-
}, 100);
|
|
1792
|
-
};
|
|
1793
|
-
useInput(async (input, key) => {
|
|
1794
|
-
// Handle Ctrl+C for quit confirmation (must be first, before any other checks)
|
|
1795
|
-
if (key.ctrl && input === 'c') {
|
|
1796
|
-
if (quitConfirmMode) {
|
|
1797
|
-
// Second Ctrl+C - actually quit
|
|
1798
|
-
cleanExit();
|
|
1799
|
-
}
|
|
1800
|
-
else {
|
|
1801
|
-
// First Ctrl+C - show confirmation
|
|
1802
|
-
setQuitConfirmMode(true);
|
|
1803
|
-
// Reset after 3 seconds if user doesn't press Ctrl+C again
|
|
1804
|
-
setTimeout(() => {
|
|
1805
|
-
setQuitConfirmMode(false);
|
|
1806
|
-
}, 3000);
|
|
1807
|
-
}
|
|
1808
|
-
return;
|
|
1809
|
-
}
|
|
1810
|
-
if (isCreatingPane || runningCommand || isUpdating || isLoading || isCreatingTunnel) {
|
|
1811
|
-
// Disable input while performing operations or loading
|
|
1812
|
-
return;
|
|
1813
|
-
}
|
|
1814
|
-
// Handle kebab menu navigation
|
|
1815
|
-
if (showKebabMenu && kebabMenuPaneIndex !== null) {
|
|
1816
|
-
const currentPane = panes[kebabMenuPaneIndex];
|
|
1817
|
-
const availableActions = kebabMenuActions;
|
|
1818
|
-
if (key.escape) {
|
|
1819
|
-
setShowKebabMenu(false);
|
|
1820
|
-
setKebabMenuPaneIndex(null);
|
|
1821
|
-
setKebabMenuOption(0);
|
|
1822
|
-
setKebabMenuActions([]);
|
|
1823
|
-
return;
|
|
1824
|
-
}
|
|
1825
|
-
else if (key.upArrow) {
|
|
1826
|
-
setKebabMenuOption(Math.max(0, kebabMenuOption - 1));
|
|
1827
|
-
return;
|
|
1828
|
-
}
|
|
1829
|
-
else if (key.downArrow) {
|
|
1830
|
-
setKebabMenuOption(Math.min(availableActions.length - 1, kebabMenuOption + 1));
|
|
1831
|
-
return;
|
|
1832
|
-
}
|
|
1833
|
-
else if (key.return) {
|
|
1834
|
-
// Execute the selected menu action
|
|
1835
|
-
setShowKebabMenu(false);
|
|
1836
|
-
const selectedAction = availableActions[kebabMenuOption];
|
|
1837
|
-
if (selectedAction) {
|
|
1838
|
-
// Use the action system to execute
|
|
1839
|
-
actionSystem.executeAction(selectedAction.id, currentPane, { mainBranch: getMainBranch() });
|
|
1840
|
-
}
|
|
1841
|
-
setKebabMenuPaneIndex(null);
|
|
1842
|
-
setKebabMenuOption(0);
|
|
1843
|
-
setKebabMenuActions([]);
|
|
1844
|
-
return;
|
|
1845
|
-
}
|
|
1846
|
-
// Don't process other inputs while menu is open
|
|
1847
|
-
return;
|
|
1848
|
-
}
|
|
1849
|
-
// Handle quit confirm mode - ESC cancels it
|
|
1850
|
-
if (quitConfirmMode) {
|
|
1851
|
-
if (key.escape) {
|
|
1852
|
-
setQuitConfirmMode(false);
|
|
1853
|
-
return;
|
|
1854
|
-
}
|
|
1855
|
-
// Allow other inputs to continue (don't return early)
|
|
1856
|
-
}
|
|
1857
|
-
// Handle QR code view
|
|
1858
|
-
if (showQRCode) {
|
|
1859
|
-
if (key.escape) {
|
|
1860
|
-
setShowQRCode(false);
|
|
1861
|
-
}
|
|
1862
|
-
return;
|
|
1863
|
-
}
|
|
1864
|
-
// Handle hooks dialog
|
|
1865
|
-
if (showHooksDialog) {
|
|
1866
|
-
if (key.escape) {
|
|
1867
|
-
setShowHooksDialog(false);
|
|
1868
|
-
setHooksSelectedIndex(0);
|
|
1869
|
-
// Go back to settings dialog
|
|
1870
|
-
setShowSettingsDialog(true);
|
|
1871
|
-
setSettingsMode('list');
|
|
1872
|
-
return;
|
|
1873
|
-
}
|
|
1874
|
-
else if (key.upArrow) {
|
|
1875
|
-
setHooksSelectedIndex(Math.max(0, hooksSelectedIndex - 1));
|
|
1876
|
-
return;
|
|
1877
|
-
}
|
|
1878
|
-
else if (key.downArrow) {
|
|
1879
|
-
setHooksSelectedIndex(Math.min(hooksData.length - 1, hooksSelectedIndex + 1));
|
|
1880
|
-
return;
|
|
1881
|
-
}
|
|
1882
|
-
else if (input === 'e') {
|
|
1883
|
-
// Edit hooks using an agent
|
|
1884
|
-
setShowHooksDialog(false);
|
|
1885
|
-
setHooksSelectedIndex(0);
|
|
1886
|
-
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";
|
|
1887
|
-
setPendingPrompt(prompt);
|
|
1888
|
-
setNewPanePrompt(prompt);
|
|
1889
|
-
// Choose agent
|
|
1890
|
-
const agents = availableAgents;
|
|
1891
|
-
if (agents.length === 0) {
|
|
1892
|
-
createNewPaneHook(prompt);
|
|
1893
|
-
}
|
|
1894
|
-
else if (agents.length === 1) {
|
|
1895
|
-
createNewPaneHook(prompt, agents[0]);
|
|
1896
|
-
}
|
|
1897
|
-
else {
|
|
1898
|
-
setShowAgentChoiceDialog(true);
|
|
1899
|
-
setAgentChoice(agentChoice || 'claude');
|
|
1900
|
-
}
|
|
1901
|
-
return;
|
|
1902
|
-
}
|
|
1903
|
-
return;
|
|
1904
|
-
}
|
|
1905
|
-
// Handle settings dialog
|
|
1906
|
-
if (showSettingsDialog) {
|
|
1907
|
-
if (key.escape) {
|
|
1908
|
-
if (settingsMode === 'list') {
|
|
1909
|
-
// Close settings dialog
|
|
1910
|
-
setShowSettingsDialog(false);
|
|
1911
|
-
setSettingsMode('list');
|
|
1912
|
-
setSettingsSelectedIndex(0);
|
|
1913
|
-
setSettingsEditingKey(undefined);
|
|
1914
|
-
}
|
|
1915
|
-
else {
|
|
1916
|
-
// Go back to list
|
|
1917
|
-
setSettingsMode('list');
|
|
1918
|
-
setSettingsEditingKey(undefined);
|
|
1919
|
-
setSettingsEditingValueIndex(0);
|
|
1920
|
-
setSettingsScopeIndex(0);
|
|
1921
|
-
}
|
|
1922
|
-
return;
|
|
1923
|
-
}
|
|
1924
|
-
else if (key.upArrow) {
|
|
1925
|
-
if (settingsMode === 'list') {
|
|
1926
|
-
setSettingsSelectedIndex(Math.max(0, settingsSelectedIndex - 1));
|
|
1927
|
-
}
|
|
1928
|
-
else if (settingsMode === 'edit') {
|
|
1929
|
-
const currentDef = SETTING_DEFINITIONS[settingsSelectedIndex];
|
|
1930
|
-
const maxIndex = currentDef.type === 'boolean' ? 1 : (currentDef.options?.length || 1) - 1;
|
|
1931
|
-
setSettingsEditingValueIndex(Math.max(0, settingsEditingValueIndex - 1));
|
|
1932
|
-
}
|
|
1933
|
-
else if (settingsMode === 'scope') {
|
|
1934
|
-
setSettingsScopeIndex(Math.max(0, settingsScopeIndex - 1));
|
|
1935
|
-
}
|
|
1936
|
-
return;
|
|
1937
|
-
}
|
|
1938
|
-
else if (key.downArrow) {
|
|
1939
|
-
if (settingsMode === 'list') {
|
|
1940
|
-
setSettingsSelectedIndex(Math.min(SETTING_DEFINITIONS.length - 1, settingsSelectedIndex + 1));
|
|
1941
|
-
}
|
|
1942
|
-
else if (settingsMode === 'edit') {
|
|
1943
|
-
const currentDef = SETTING_DEFINITIONS[settingsSelectedIndex];
|
|
1944
|
-
const maxIndex = currentDef.type === 'boolean' ? 1 : (currentDef.options?.length || 1) - 1;
|
|
1945
|
-
setSettingsEditingValueIndex(Math.min(maxIndex, settingsEditingValueIndex + 1));
|
|
1946
|
-
}
|
|
1947
|
-
else if (settingsMode === 'scope') {
|
|
1948
|
-
setSettingsScopeIndex(Math.min(1, settingsScopeIndex + 1));
|
|
1949
|
-
}
|
|
1950
|
-
return;
|
|
1951
|
-
}
|
|
1952
|
-
else if (key.return) {
|
|
1953
|
-
if (settingsMode === 'list') {
|
|
1954
|
-
const currentDef = SETTING_DEFINITIONS[settingsSelectedIndex];
|
|
1955
|
-
// Handle action type - trigger the action instead of editing
|
|
1956
|
-
if (currentDef.type === 'action') {
|
|
1957
|
-
if (currentDef.key === 'hooks') {
|
|
1958
|
-
// Show hooks dialog
|
|
1959
|
-
setShowSettingsDialog(false);
|
|
1960
|
-
const { hasHook } = await import('./utils/hooks.js');
|
|
1961
|
-
const allHookTypes = [
|
|
1962
|
-
'before_pane_create',
|
|
1963
|
-
'pane_created',
|
|
1964
|
-
'worktree_created',
|
|
1965
|
-
'before_pane_close',
|
|
1966
|
-
'pane_closed',
|
|
1967
|
-
'before_worktree_remove',
|
|
1968
|
-
'worktree_removed',
|
|
1969
|
-
'pre_merge',
|
|
1970
|
-
'post_merge',
|
|
1971
|
-
'run_test',
|
|
1972
|
-
'run_dev',
|
|
1973
|
-
];
|
|
1974
|
-
const hooks = allHookTypes.map(hookName => ({
|
|
1975
|
-
name: hookName,
|
|
1976
|
-
active: hasHook(projectRoot || process.cwd(), hookName)
|
|
1977
|
-
}));
|
|
1978
|
-
setHooksData(hooks);
|
|
1979
|
-
setShowHooksDialog(true);
|
|
1980
|
-
setHooksSelectedIndex(0);
|
|
1981
|
-
}
|
|
1982
|
-
return;
|
|
1983
|
-
}
|
|
1984
|
-
// Enter edit mode for regular settings
|
|
1985
|
-
setSettingsEditingKey(currentDef.key);
|
|
1986
|
-
setSettingsMode('edit');
|
|
1987
|
-
// Set initial value index based on current setting
|
|
1988
|
-
const currentValue = settingsManager.getSetting(currentDef.key);
|
|
1989
|
-
if (currentDef.type === 'boolean') {
|
|
1990
|
-
setSettingsEditingValueIndex(currentValue ? 0 : 1);
|
|
1991
|
-
}
|
|
1992
|
-
else if (currentDef.type === 'select' && currentDef.options) {
|
|
1993
|
-
const optIndex = currentDef.options.findIndex(o => o.value === currentValue);
|
|
1994
|
-
setSettingsEditingValueIndex(Math.max(0, optIndex));
|
|
1995
|
-
}
|
|
1996
|
-
}
|
|
1997
|
-
else if (settingsMode === 'edit') {
|
|
1998
|
-
// Go to scope selection
|
|
1999
|
-
setSettingsMode('scope');
|
|
2000
|
-
setSettingsScopeIndex(0);
|
|
2001
|
-
}
|
|
2002
|
-
else if (settingsMode === 'scope') {
|
|
2003
|
-
// Save the setting
|
|
2004
|
-
const currentDef = SETTING_DEFINITIONS[settingsSelectedIndex];
|
|
2005
|
-
const scope = settingsScopeIndex === 0 ? 'global' : 'project';
|
|
2006
|
-
// Only save if this is not an action type (actions don't have values)
|
|
2007
|
-
if (currentDef.type !== 'action') {
|
|
2008
|
-
// Calculate the new value
|
|
2009
|
-
let newValue;
|
|
2010
|
-
if (currentDef.type === 'boolean') {
|
|
2011
|
-
newValue = settingsEditingValueIndex === 0;
|
|
2012
|
-
}
|
|
2013
|
-
else if (currentDef.type === 'select' && currentDef.options) {
|
|
2014
|
-
newValue = currentDef.options[settingsEditingValueIndex]?.value || '';
|
|
2015
|
-
}
|
|
2016
|
-
// Update the setting - cast key to proper type since we know it's not an action
|
|
2017
|
-
settingsManager.updateSetting(currentDef.key, newValue, scope);
|
|
2018
|
-
}
|
|
2019
|
-
// Reset to list view
|
|
2020
|
-
setSettingsMode('list');
|
|
2021
|
-
setSettingsEditingKey(undefined);
|
|
2022
|
-
setSettingsEditingValueIndex(0);
|
|
2023
|
-
setSettingsScopeIndex(0);
|
|
2024
|
-
setStatusMessage(\`Setting saved (\${scope})\`);
|
|
2025
|
-
setTimeout(() => setStatusMessage(''), 2000);
|
|
2026
|
-
}
|
|
2027
|
-
return;
|
|
2028
|
-
}
|
|
2029
|
-
return;
|
|
2030
|
-
}
|
|
2031
|
-
// Handle action system confirm dialog
|
|
2032
|
-
if (actionSystem.actionState.showConfirmDialog) {
|
|
2033
|
-
if (key.escape) {
|
|
2034
|
-
if (actionSystem.actionState.onConfirmNo) {
|
|
2035
|
-
actionSystem.executeCallback(actionSystem.actionState.onConfirmNo);
|
|
2036
|
-
}
|
|
2037
|
-
else {
|
|
2038
|
-
actionSystem.setActionState(prev => ({ ...prev, showConfirmDialog: false }));
|
|
2039
|
-
}
|
|
2040
|
-
return;
|
|
2041
|
-
}
|
|
2042
|
-
else if (key.upArrow) {
|
|
2043
|
-
actionSystem.setActionState(prev => ({
|
|
2044
|
-
...prev,
|
|
2045
|
-
confirmSelectedIndex: Math.max(0, prev.confirmSelectedIndex - 1)
|
|
2046
|
-
}));
|
|
2047
|
-
return;
|
|
2048
|
-
}
|
|
2049
|
-
else if (key.downArrow) {
|
|
2050
|
-
actionSystem.setActionState(prev => ({
|
|
2051
|
-
...prev,
|
|
2052
|
-
confirmSelectedIndex: Math.min(1, prev.confirmSelectedIndex + 1)
|
|
2053
|
-
}));
|
|
2054
|
-
return;
|
|
2055
|
-
}
|
|
2056
|
-
else if (key.return) {
|
|
2057
|
-
// Execute based on selected index
|
|
2058
|
-
if (actionSystem.actionState.confirmSelectedIndex === 0) {
|
|
2059
|
-
if (actionSystem.actionState.onConfirmYes) {
|
|
2060
|
-
actionSystem.executeCallback(actionSystem.actionState.onConfirmYes);
|
|
2061
|
-
}
|
|
2062
|
-
}
|
|
2063
|
-
else {
|
|
2064
|
-
if (actionSystem.actionState.onConfirmNo) {
|
|
2065
|
-
actionSystem.executeCallback(actionSystem.actionState.onConfirmNo);
|
|
2066
|
-
}
|
|
2067
|
-
else {
|
|
2068
|
-
actionSystem.setActionState(prev => ({ ...prev, showConfirmDialog: false }));
|
|
2069
|
-
}
|
|
2070
|
-
}
|
|
2071
|
-
return;
|
|
2072
|
-
}
|
|
2073
|
-
else if (input === 'y' || input === 'Y') {
|
|
2074
|
-
// Shortcut: yes
|
|
2075
|
-
if (actionSystem.actionState.onConfirmYes) {
|
|
2076
|
-
actionSystem.executeCallback(actionSystem.actionState.onConfirmYes);
|
|
2730
|
+
execSync(\`tmux kill-window -t '\${existingWindowId}'\`, {
|
|
2731
|
+
stdio: "pipe",
|
|
2732
|
+
});
|
|
2077
2733
|
}
|
|
2078
|
-
|
|
2734
|
+
catch { }
|
|
2079
2735
|
}
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
}
|
|
2088
|
-
|
|
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" });
|
|
2089
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) {
|
|
2090
2828
|
return;
|
|
2091
2829
|
}
|
|
2092
|
-
// Handle
|
|
2093
|
-
if (
|
|
2094
|
-
if (
|
|
2095
|
-
|
|
2096
|
-
|
|
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();
|
|
2097
2835
|
}
|
|
2098
|
-
else
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
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);
|
|
2103
2843
|
}
|
|
2104
|
-
// Let CleanTextInput handle all other key events
|
|
2105
2844
|
return;
|
|
2106
2845
|
}
|
|
2107
|
-
|
|
2108
|
-
|
|
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) {
|
|
2109
2852
|
if (key.escape) {
|
|
2110
|
-
|
|
2111
|
-
return;
|
|
2112
|
-
}
|
|
2113
|
-
else if (key.upArrow) {
|
|
2114
|
-
actionSystem.setActionState(prev => ({
|
|
2115
|
-
...prev,
|
|
2116
|
-
choiceSelectedIndex: Math.max(0, prev.choiceSelectedIndex - 1)
|
|
2117
|
-
}));
|
|
2118
|
-
return;
|
|
2119
|
-
}
|
|
2120
|
-
else if (key.downArrow) {
|
|
2121
|
-
const maxIndex = actionSystem.actionState.choiceOptions.length - 1;
|
|
2122
|
-
actionSystem.setActionState(prev => ({
|
|
2123
|
-
...prev,
|
|
2124
|
-
choiceSelectedIndex: Math.min(maxIndex, prev.choiceSelectedIndex + 1)
|
|
2125
|
-
}));
|
|
2126
|
-
return;
|
|
2127
|
-
}
|
|
2128
|
-
else if (key.return) {
|
|
2129
|
-
const selectedOption = actionSystem.actionState.choiceOptions[actionSystem.actionState.choiceSelectedIndex];
|
|
2130
|
-
if (selectedOption && actionSystem.actionState.onChoiceSelect) {
|
|
2131
|
-
actionSystem.executeCallback(async () => actionSystem.actionState.onChoiceSelect(selectedOption.id));
|
|
2132
|
-
}
|
|
2853
|
+
setQuitConfirmMode(false);
|
|
2133
2854
|
return;
|
|
2134
2855
|
}
|
|
2135
|
-
return
|
|
2856
|
+
// Allow other inputs to continue (don't return early)
|
|
2136
2857
|
}
|
|
2137
2858
|
if (showFileCopyPrompt) {
|
|
2138
|
-
if (input ===
|
|
2859
|
+
if (input === "y" || input === "Y") {
|
|
2139
2860
|
setShowFileCopyPrompt(false);
|
|
2140
2861
|
const selectedPane = panes[selectedIndex];
|
|
2141
2862
|
if (selectedPane && selectedPane.worktreePath && currentCommandType) {
|
|
@@ -2143,7 +2864,7 @@ const DmuxApp = ({ panesFile, projectName, sessionName, settingsFile, projectRoo
|
|
|
2143
2864
|
// Mark as not first run and continue with command
|
|
2144
2865
|
const newSettings = {
|
|
2145
2866
|
...projectSettings,
|
|
2146
|
-
[currentCommandType ===
|
|
2867
|
+
[currentCommandType === "test" ? "firstTestRun" : "firstDevRun"]: true,
|
|
2147
2868
|
};
|
|
2148
2869
|
await saveSettings(newSettings);
|
|
2149
2870
|
// Now run the actual command
|
|
@@ -2151,14 +2872,14 @@ const DmuxApp = ({ panesFile, projectName, sessionName, settingsFile, projectRoo
|
|
|
2151
2872
|
}
|
|
2152
2873
|
setCurrentCommandType(null);
|
|
2153
2874
|
}
|
|
2154
|
-
else if (input ===
|
|
2875
|
+
else if (input === "n" || input === "N" || key.escape) {
|
|
2155
2876
|
setShowFileCopyPrompt(false);
|
|
2156
2877
|
const selectedPane = panes[selectedIndex];
|
|
2157
2878
|
if (selectedPane && currentCommandType) {
|
|
2158
2879
|
// Mark as not first run and continue without copying
|
|
2159
2880
|
const newSettings = {
|
|
2160
2881
|
...projectSettings,
|
|
2161
|
-
[currentCommandType ===
|
|
2882
|
+
[currentCommandType === "test" ? "firstTestRun" : "firstDevRun"]: true,
|
|
2162
2883
|
};
|
|
2163
2884
|
await saveSettings(newSettings);
|
|
2164
2885
|
// Now run the actual command
|
|
@@ -2168,36 +2889,13 @@ const DmuxApp = ({ panesFile, projectName, sessionName, settingsFile, projectRoo
|
|
|
2168
2889
|
}
|
|
2169
2890
|
return;
|
|
2170
2891
|
}
|
|
2171
|
-
if (showAgentChoiceDialog) {
|
|
2172
|
-
if (key.escape) {
|
|
2173
|
-
setShowAgentChoiceDialog(false);
|
|
2174
|
-
setShowNewPaneDialog(true);
|
|
2175
|
-
setNewPanePrompt(pendingPrompt);
|
|
2176
|
-
setPendingPrompt('');
|
|
2177
|
-
}
|
|
2178
|
-
else if (key.leftArrow || input === '1' || (input && input.toLowerCase() === 'c')) {
|
|
2179
|
-
setAgentChoice('claude');
|
|
2180
|
-
}
|
|
2181
|
-
else if (key.rightArrow || input === '2' || (input && input.toLowerCase() === 'o')) {
|
|
2182
|
-
setAgentChoice('opencode');
|
|
2183
|
-
}
|
|
2184
|
-
else if (key.return) {
|
|
2185
|
-
const chosen = agentChoice || (availableAgents[0] || 'claude');
|
|
2186
|
-
const promptValue = pendingPrompt;
|
|
2187
|
-
setShowAgentChoiceDialog(false);
|
|
2188
|
-
setPendingPrompt('');
|
|
2189
|
-
await createNewPaneHook(promptValue, chosen);
|
|
2190
|
-
setNewPanePrompt('');
|
|
2191
|
-
}
|
|
2192
|
-
return;
|
|
2193
|
-
}
|
|
2194
2892
|
if (showCommandPrompt) {
|
|
2195
2893
|
if (key.escape) {
|
|
2196
2894
|
setShowCommandPrompt(null);
|
|
2197
|
-
setCommandInput(
|
|
2895
|
+
setCommandInput("");
|
|
2198
2896
|
}
|
|
2199
2897
|
else if (key.return) {
|
|
2200
|
-
if (commandInput.trim() ===
|
|
2898
|
+
if (commandInput.trim() === "") {
|
|
2201
2899
|
// If empty, suggest a default command based on package manager
|
|
2202
2900
|
const suggested = await suggestCommand(showCommandPrompt);
|
|
2203
2901
|
if (suggested) {
|
|
@@ -2208,13 +2906,15 @@ const DmuxApp = ({ panesFile, projectName, sessionName, settingsFile, projectRoo
|
|
|
2208
2906
|
// User provided manual command
|
|
2209
2907
|
const newSettings = {
|
|
2210
2908
|
...projectSettings,
|
|
2211
|
-
[showCommandPrompt ===
|
|
2909
|
+
[showCommandPrompt === "test" ? "testCommand" : "devCommand"]: commandInput.trim(),
|
|
2212
2910
|
};
|
|
2213
2911
|
await saveSettings(newSettings);
|
|
2214
2912
|
const selectedPane = panes[selectedIndex];
|
|
2215
2913
|
if (selectedPane) {
|
|
2216
2914
|
// Check if first run
|
|
2217
|
-
const isFirstRun = showCommandPrompt ===
|
|
2915
|
+
const isFirstRun = showCommandPrompt === "test"
|
|
2916
|
+
? !projectSettings.firstTestRun
|
|
2917
|
+
: !projectSettings.firstDevRun;
|
|
2218
2918
|
if (isFirstRun) {
|
|
2219
2919
|
setCurrentCommandType(showCommandPrompt);
|
|
2220
2920
|
setShowCommandPrompt(null);
|
|
@@ -2223,171 +2923,210 @@ const DmuxApp = ({ panesFile, projectName, sessionName, settingsFile, projectRoo
|
|
|
2223
2923
|
else {
|
|
2224
2924
|
await runCommandInternal(showCommandPrompt, selectedPane);
|
|
2225
2925
|
setShowCommandPrompt(null);
|
|
2226
|
-
setCommandInput(
|
|
2926
|
+
setCommandInput("");
|
|
2227
2927
|
}
|
|
2228
2928
|
}
|
|
2229
2929
|
else {
|
|
2230
2930
|
setShowCommandPrompt(null);
|
|
2231
|
-
setCommandInput(
|
|
2931
|
+
setCommandInput("");
|
|
2232
2932
|
}
|
|
2233
2933
|
}
|
|
2234
2934
|
}
|
|
2235
2935
|
return;
|
|
2236
2936
|
}
|
|
2237
|
-
if (showNewPaneDialog) {
|
|
2238
|
-
if (key.escape) {
|
|
2239
|
-
setShowNewPaneDialog(false);
|
|
2240
|
-
setNewPanePrompt('');
|
|
2241
|
-
}
|
|
2242
|
-
// TextInput handles other input events
|
|
2243
|
-
return;
|
|
2244
|
-
}
|
|
2245
2937
|
// Handle directional navigation with spatial awareness based on card grid layout
|
|
2246
2938
|
if (key.upArrow || key.downArrow || key.leftArrow || key.rightArrow) {
|
|
2247
2939
|
let targetIndex = null;
|
|
2248
2940
|
if (key.upArrow) {
|
|
2249
|
-
targetIndex = findCardInDirection(selectedIndex,
|
|
2941
|
+
targetIndex = findCardInDirection(selectedIndex, "up");
|
|
2250
2942
|
}
|
|
2251
2943
|
else if (key.downArrow) {
|
|
2252
|
-
targetIndex = findCardInDirection(selectedIndex,
|
|
2944
|
+
targetIndex = findCardInDirection(selectedIndex, "down");
|
|
2253
2945
|
}
|
|
2254
2946
|
else if (key.leftArrow) {
|
|
2255
|
-
targetIndex = findCardInDirection(selectedIndex,
|
|
2947
|
+
targetIndex = findCardInDirection(selectedIndex, "left");
|
|
2256
2948
|
}
|
|
2257
2949
|
else if (key.rightArrow) {
|
|
2258
|
-
targetIndex = findCardInDirection(selectedIndex,
|
|
2950
|
+
targetIndex = findCardInDirection(selectedIndex, "right");
|
|
2259
2951
|
}
|
|
2260
2952
|
if (targetIndex !== null) {
|
|
2261
2953
|
setSelectedIndex(targetIndex);
|
|
2262
2954
|
}
|
|
2263
2955
|
return;
|
|
2264
2956
|
}
|
|
2265
|
-
if (
|
|
2266
|
-
// Open kebab menu for selected pane
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
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") {
|
|
2281
2980
|
cleanExit();
|
|
2282
2981
|
}
|
|
2283
|
-
else if (input ===
|
|
2284
|
-
//
|
|
2285
|
-
if (
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
try {
|
|
2289
|
-
const url = await server.startTunnel();
|
|
2290
|
-
setTunnelUrl(url);
|
|
2291
|
-
setStatusMessage('');
|
|
2292
|
-
setShowQRCode(true);
|
|
2293
|
-
}
|
|
2294
|
-
catch (error) {
|
|
2295
|
-
setStatusMessage('Failed to create tunnel');
|
|
2296
|
-
setTimeout(() => setStatusMessage(''), 3000);
|
|
2297
|
-
}
|
|
2298
|
-
finally {
|
|
2299
|
-
setIsCreatingTunnel(false);
|
|
2300
|
-
}
|
|
2982
|
+
else if (input === "r" && server) {
|
|
2983
|
+
// Handle remote tunnel
|
|
2984
|
+
if (tunnelUrl) {
|
|
2985
|
+
// Tunnel exists - open popup with QR code
|
|
2986
|
+
await launchRemotePopup();
|
|
2301
2987
|
}
|
|
2302
|
-
else {
|
|
2303
|
-
//
|
|
2304
|
-
|
|
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
|
+
})();
|
|
2305
3004
|
}
|
|
3005
|
+
// If tunnelCreating is true, do nothing (already creating)
|
|
3006
|
+
return;
|
|
2306
3007
|
}
|
|
2307
|
-
else if (!isLoading &&
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
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;
|
|
2312
3040
|
}
|
|
2313
|
-
else if (input ===
|
|
3041
|
+
else if (input === "j" && selectedIndex < panes.length) {
|
|
2314
3042
|
// Jump to pane (NEW: using action system)
|
|
2315
3043
|
StateManager.getInstance().setDebugMessage(\`Jumping to pane: \${panes[selectedIndex].slug}\`);
|
|
2316
|
-
setTimeout(() => StateManager.getInstance().setDebugMessage(
|
|
3044
|
+
setTimeout(() => StateManager.getInstance().setDebugMessage(""), 2000);
|
|
2317
3045
|
actionSystem.executeAction(PaneAction.VIEW, panes[selectedIndex]);
|
|
2318
3046
|
}
|
|
2319
|
-
else if (input ===
|
|
3047
|
+
else if (input === "x" && selectedIndex < panes.length) {
|
|
2320
3048
|
// Close pane (NEW: using action system)
|
|
2321
3049
|
StateManager.getInstance().setDebugMessage(\`Closing pane: \${panes[selectedIndex].slug}\`);
|
|
2322
|
-
setTimeout(() => StateManager.getInstance().setDebugMessage(
|
|
3050
|
+
setTimeout(() => StateManager.getInstance().setDebugMessage(""), 2000);
|
|
2323
3051
|
actionSystem.executeAction(PaneAction.CLOSE, panes[selectedIndex]);
|
|
2324
3052
|
}
|
|
2325
3053
|
else if (key.return && selectedIndex < panes.length) {
|
|
2326
3054
|
// Jump to pane (NEW: using action system)
|
|
2327
3055
|
StateManager.getInstance().setDebugMessage(\`Jumping to pane: \${panes[selectedIndex].slug}\`);
|
|
2328
|
-
setTimeout(() => StateManager.getInstance().setDebugMessage(
|
|
3056
|
+
setTimeout(() => StateManager.getInstance().setDebugMessage(""), 2000);
|
|
2329
3057
|
actionSystem.executeAction(PaneAction.VIEW, panes[selectedIndex]);
|
|
2330
3058
|
}
|
|
2331
3059
|
});
|
|
2332
|
-
//
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
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;
|
|
2340
3072
|
}
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
} })),
|
|
2377
|
-
actionSystem.actionState.showProgressDialog && (React.createElement(ActionProgressDialog, { key: "progress-dialog", message: actionSystem.actionState.progressMessage, percent: actionSystem.actionState.progressPercent })),
|
|
2378
|
-
runningCommand && (React.createElement(RunningIndicator, null)),
|
|
2379
|
-
isUpdating && (React.createElement(UpdatingIndicator, null)),
|
|
2380
|
-
isCreatingTunnel && (React.createElement(Box, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", padding: 1, marginTop: 1 },
|
|
2381
|
-
React.createElement(Text, { bold: true, color: "cyan" }, "Creating tunnel..."),
|
|
2382
|
-
React.createElement(Box, { marginTop: 1 },
|
|
2383
|
-
React.createElement(Text, { dimColor: true }, "This may take a few moments...")))),
|
|
2384
|
-
statusMessage && (React.createElement(Box, { marginTop: 1 },
|
|
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 },
|
|
3098
|
+
showRepaintSpinner && (React.createElement(Box, { marginTop: -10, marginLeft: -100 },
|
|
3099
|
+
React.createElement(Text, null, "\\u27F3"))),
|
|
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,
|
|
2385
3108
|
React.createElement(Text, { color: "green" }, statusMessage))),
|
|
2386
|
-
actionSystem.actionState.statusMessage && (React.createElement(Box,
|
|
2387
|
-
React.createElement(Text, { color: actionSystem.actionState.statusType ===
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
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: (() => {
|
|
2391
3130
|
if (!process.env.DEBUG_DMUX)
|
|
2392
3131
|
return undefined;
|
|
2393
3132
|
const cols = Math.max(1, Math.floor(terminalWidth / 37));
|
|
@@ -2395,23 +3134,21 @@ const DmuxApp = ({ panesFile, projectName, sessionName, settingsFile, projectRoo
|
|
|
2395
3134
|
const pos = getCardGridPosition(selectedIndex);
|
|
2396
3135
|
return \`Grid: \${cols} cols × \${rows} rows | Selected: row \${pos.row}, col \${pos.col} | Terminal: \${terminalWidth}w\`;
|
|
2397
3136
|
})() }),
|
|
2398
|
-
React.createElement(Text, { dimColor: true },
|
|
2399
|
-
updateAvailable && updateInfo && (React.createElement(Text, { color: "red", bold: true },
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
"
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
serverPort))),
|
|
2407
|
-
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 },
|
|
2408
3145
|
" \\u2022 ",
|
|
2409
3146
|
debugMessage)))));
|
|
2410
3147
|
};
|
|
2411
3148
|
export default DmuxApp;
|
|
2412
3149
|
//# sourceMappingURL=DmuxApp.js.map`,
|
|
2413
3150
|
mimeType: 'application/javascript',
|
|
2414
|
-
size:
|
|
3151
|
+
size: 94288
|
|
2415
3152
|
},
|
|
2416
3153
|
'EnhancedTextInput.js': {
|
|
2417
3154
|
content: `import React, { useState, useEffect } from 'react';
|
|
@@ -4776,26 +5513,174 @@ Expected function or array of functions, received type \${typeof e}.\`),ne)}func
|
|
|
4776
5513
|
},
|
|
4777
5514
|
'dashboard.js': {
|
|
4778
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(\`
|
|
4779
|
-
\`).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");
|
|
4780
5517
|
`,
|
|
4781
5518
|
mimeType: 'application/javascript',
|
|
4782
|
-
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
|
|
4783
5660
|
},
|
|
4784
5661
|
'index.js': {
|
|
4785
5662
|
content: `#!/usr/bin/env node
|
|
4786
5663
|
import { execSync } from 'child_process';
|
|
4787
5664
|
import fs from 'fs/promises';
|
|
5665
|
+
import * as fsSync from 'fs';
|
|
4788
5666
|
import path from 'path';
|
|
4789
5667
|
import { fileURLToPath } from 'url';
|
|
4790
5668
|
import { render } from 'ink';
|
|
4791
5669
|
import React from 'react';
|
|
4792
5670
|
import { createHash } from 'crypto';
|
|
5671
|
+
import { createRequire } from 'module';
|
|
4793
5672
|
import DmuxApp from './DmuxApp.js';
|
|
4794
5673
|
import { AutoUpdater } from './AutoUpdater.js';
|
|
4795
5674
|
import readline from 'readline';
|
|
4796
5675
|
import { DmuxServer } from './server/index.js';
|
|
4797
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';
|
|
4798
5681
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
5682
|
+
const require = createRequire(import.meta.url);
|
|
5683
|
+
const packageJson = require('../package.json');
|
|
4799
5684
|
class Dmux {
|
|
4800
5685
|
panesFile;
|
|
4801
5686
|
settingsFile;
|
|
@@ -4834,6 +5719,11 @@ class Dmux {
|
|
|
4834
5719
|
async init() {
|
|
4835
5720
|
// Set up global signal handlers for clean exit
|
|
4836
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
|
+
}
|
|
4837
5727
|
// Ensure .dmux directory exists and is in .gitignore
|
|
4838
5728
|
await this.ensureDmuxDirectory();
|
|
4839
5729
|
// Check for migration from old config location
|
|
@@ -4845,13 +5735,16 @@ class Dmux {
|
|
|
4845
5735
|
projectRoot: this.projectRoot,
|
|
4846
5736
|
panes: [],
|
|
4847
5737
|
settings: {},
|
|
4848
|
-
lastUpdated: new Date().toISOString()
|
|
5738
|
+
lastUpdated: new Date().toISOString(),
|
|
5739
|
+
controlPaneId: undefined,
|
|
5740
|
+
controlPaneSize: 40 // Sidebar width
|
|
4849
5741
|
};
|
|
4850
5742
|
await fs.writeFile(this.panesFile, JSON.stringify(initialConfig, null, 2));
|
|
4851
5743
|
}
|
|
4852
5744
|
// Check for updates in background if needed
|
|
4853
5745
|
this.checkForUpdatesBackground();
|
|
4854
5746
|
const inTmux = process.env.TMUX !== undefined;
|
|
5747
|
+
const isDev = process.env.DMUX_DEV === 'true';
|
|
4855
5748
|
if (!inTmux) {
|
|
4856
5749
|
// Check if project-specific session already exists
|
|
4857
5750
|
try {
|
|
@@ -4864,27 +5757,116 @@ class Dmux {
|
|
|
4864
5757
|
execSync(\`tmux new-session -d -s \${this.sessionName}\`, { stdio: 'inherit' });
|
|
4865
5758
|
// Enable pane borders to show titles
|
|
4866
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' });
|
|
4867
5765
|
// Set pane title for the main dmux pane
|
|
4868
|
-
execSync(\`tmux select-pane -t \${this.sessionName} -T "dmux
|
|
4869
|
-
// Send dmux command to the new session
|
|
4870
|
-
|
|
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' });
|
|
4871
5792
|
}
|
|
4872
5793
|
execSync(\`tmux attach-session -t \${this.sessionName}\`, { stdio: 'inherit' });
|
|
4873
5794
|
return;
|
|
4874
5795
|
}
|
|
4875
5796
|
// Enable pane borders to show titles
|
|
4876
|
-
|
|
4877
|
-
|
|
4878
|
-
}
|
|
4879
|
-
catch {
|
|
4880
|
-
|
|
4881
|
-
}
|
|
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
|
+
// }
|
|
4882
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;
|
|
4883
5812
|
try {
|
|
4884
|
-
|
|
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
|
+
}
|
|
4885
5866
|
}
|
|
4886
|
-
catch {
|
|
4887
|
-
// 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);
|
|
4888
5870
|
}
|
|
4889
5871
|
// Update state manager with project info
|
|
4890
5872
|
this.stateManager.updateProjectInfo(this.projectName, this.sessionName, this.projectRoot, this.panesFile);
|
|
@@ -4897,24 +5879,29 @@ class Dmux {
|
|
|
4897
5879
|
// Don't log the local URL - tunnel will be created on demand when "r" is pressed
|
|
4898
5880
|
}
|
|
4899
5881
|
catch (err) {
|
|
4900
|
-
|
|
5882
|
+
LogService.getInstance().error('Failed to start HTTP server', 'Setup', undefined, err instanceof Error ? err : undefined);
|
|
4901
5883
|
// Continue without server - not critical for main functionality
|
|
4902
5884
|
}
|
|
4903
|
-
//
|
|
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
|
|
4904
5900
|
process.stdout.write('\\x1b[2J\\x1b[H'); // Clear screen and move cursor to home
|
|
4905
|
-
|
|
4906
|
-
//
|
|
4907
|
-
|
|
4908
|
-
|
|
4909
|
-
}
|
|
4910
|
-
catch { }
|
|
4911
|
-
// Force tmux to refresh before Ink renders
|
|
4912
|
-
try {
|
|
4913
|
-
execSync('tmux refresh-client', { stdio: 'pipe' });
|
|
4914
|
-
}
|
|
4915
|
-
catch { }
|
|
4916
|
-
// Small delay to let the clear take effect before Ink renders
|
|
4917
|
-
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));
|
|
4918
5905
|
// Launch the Ink app
|
|
4919
5906
|
const app = render(React.createElement(DmuxApp, {
|
|
4920
5907
|
panesFile: this.panesFile,
|
|
@@ -4924,7 +5911,8 @@ class Dmux {
|
|
|
4924
5911
|
projectRoot: this.projectRoot,
|
|
4925
5912
|
autoUpdater: this.autoUpdater,
|
|
4926
5913
|
serverPort: serverInfo.port,
|
|
4927
|
-
server: this.server
|
|
5914
|
+
server: this.server,
|
|
5915
|
+
controlPaneId
|
|
4928
5916
|
}), {
|
|
4929
5917
|
exitOnCtrlC: false // Disable automatic exit on Ctrl+C
|
|
4930
5918
|
});
|
|
@@ -4943,6 +5931,26 @@ class Dmux {
|
|
|
4943
5931
|
return false;
|
|
4944
5932
|
}
|
|
4945
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
|
+
}
|
|
4946
5954
|
getProjectRoot() {
|
|
4947
5955
|
// Return cached value if available
|
|
4948
5956
|
if (Dmux.cachedProjectRoot) {
|
|
@@ -5144,8 +6152,57 @@ class Dmux {
|
|
|
5144
6152
|
getAutoUpdater() {
|
|
5145
6153
|
return this.autoUpdater;
|
|
5146
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
|
+
}
|
|
5147
6199
|
setupGlobalSignalHandlers() {
|
|
5148
6200
|
const cleanTerminalExit = () => {
|
|
6201
|
+
// Clean up hooks
|
|
6202
|
+
if (process.env.TMUX) {
|
|
6203
|
+
this.cleanupResizeHook();
|
|
6204
|
+
this.cleanupPaneSplitHook();
|
|
6205
|
+
}
|
|
5149
6206
|
// Clear screen multiple times to ensure no artifacts
|
|
5150
6207
|
process.stdout.write('\\x1b[2J\\x1b[H'); // Clear screen and move to home
|
|
5151
6208
|
process.stdout.write('\\x1b[3J'); // Clear scrollback buffer
|
|
@@ -5168,6 +6225,14 @@ class Dmux {
|
|
|
5168
6225
|
// Handle Ctrl+C and SIGTERM
|
|
5169
6226
|
process.on('SIGINT', cleanTerminalExit);
|
|
5170
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
|
+
});
|
|
5171
6236
|
// Handle uncaught exceptions and unhandled rejections
|
|
5172
6237
|
process.on('uncaughtException', (error) => {
|
|
5173
6238
|
console.error('Uncaught exception:', error);
|
|
@@ -5183,7 +6248,51 @@ const dmux = new Dmux();
|
|
|
5183
6248
|
dmux.init().catch(() => process.exit(1));
|
|
5184
6249
|
//# sourceMappingURL=index.js.map`,
|
|
5185
6250
|
mimeType: 'application/javascript',
|
|
5186
|
-
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
|
|
5187
6296
|
},
|
|
5188
6297
|
'terminal.html': {
|
|
5189
6298
|
content: `<!DOCTYPE html>
|