dmux 3.1.0 → 3.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (233) hide show
  1. package/dist/CleanTextInput.d.ts +9 -0
  2. package/dist/CleanTextInput.d.ts.map +1 -1
  3. package/dist/CleanTextInput.js +127 -47
  4. package/dist/CleanTextInput.js.map +1 -1
  5. package/dist/DmuxApp.d.ts +2 -2
  6. package/dist/DmuxApp.d.ts.map +1 -1
  7. package/dist/DmuxApp.js +1408 -751
  8. package/dist/DmuxApp.js.map +1 -1
  9. package/dist/actions/paneActions.d.ts.map +1 -1
  10. package/dist/actions/paneActions.js +68 -11
  11. package/dist/actions/paneActions.js.map +1 -1
  12. package/dist/actions/types.d.ts +1 -0
  13. package/dist/actions/types.d.ts.map +1 -1
  14. package/dist/actions/types.js +1 -0
  15. package/dist/actions/types.js.map +1 -1
  16. package/dist/components/ActionInputDialog.d.ts.map +1 -1
  17. package/dist/components/ActionInputDialog.js +3 -2
  18. package/dist/components/ActionInputDialog.js.map +1 -1
  19. package/dist/components/DialogBox.d.ts +16 -0
  20. package/dist/components/DialogBox.d.ts.map +1 -0
  21. package/dist/components/DialogBox.js +12 -0
  22. package/dist/components/DialogBox.js.map +1 -0
  23. package/dist/components/FooterHelp.d.ts +9 -0
  24. package/dist/components/FooterHelp.d.ts.map +1 -1
  25. package/dist/components/FooterHelp.js +41 -5
  26. package/dist/components/FooterHelp.js.map +1 -1
  27. package/dist/components/PaneCard.d.ts +3 -1
  28. package/dist/components/PaneCard.d.ts.map +1 -1
  29. package/dist/components/PaneCard.js +50 -33
  30. package/dist/components/PaneCard.js.map +1 -1
  31. package/dist/components/PanesGrid.d.ts +3 -5
  32. package/dist/components/PanesGrid.d.ts.map +1 -1
  33. package/dist/components/PanesGrid.js +40 -10
  34. package/dist/components/PanesGrid.js.map +1 -1
  35. package/dist/dashboard.js +1 -1
  36. package/dist/decorative-pane.d.ts +3 -0
  37. package/dist/decorative-pane.d.ts.map +1 -0
  38. package/dist/decorative-pane.js +136 -0
  39. package/dist/decorative-pane.js.map +1 -0
  40. package/dist/hooks/useActionSystem.d.ts +14 -1
  41. package/dist/hooks/useActionSystem.d.ts.map +1 -1
  42. package/dist/hooks/useActionSystem.js +93 -4
  43. package/dist/hooks/useActionSystem.js.map +1 -1
  44. package/dist/hooks/useNavigation.js +1 -1
  45. package/dist/hooks/useNavigation.js.map +1 -1
  46. package/dist/hooks/usePaneCreation.d.ts +1 -2
  47. package/dist/hooks/usePaneCreation.d.ts.map +1 -1
  48. package/dist/hooks/usePaneCreation.js +30 -29
  49. package/dist/hooks/usePaneCreation.js.map +1 -1
  50. package/dist/hooks/usePaneRunner.d.ts.map +1 -1
  51. package/dist/hooks/usePaneRunner.js +8 -3
  52. package/dist/hooks/usePaneRunner.js.map +1 -1
  53. package/dist/hooks/usePanes.d.ts.map +1 -1
  54. package/dist/hooks/usePanes.js +210 -37
  55. package/dist/hooks/usePanes.js.map +1 -1
  56. package/dist/hooks/useWorktreeActions.d.ts.map +1 -1
  57. package/dist/hooks/useWorktreeActions.js +7 -13
  58. package/dist/hooks/useWorktreeActions.js.map +1 -1
  59. package/dist/index.js +217 -29
  60. package/dist/index.js.map +1 -1
  61. package/dist/popups/agentChoicePopup.d.ts +7 -0
  62. package/dist/popups/agentChoicePopup.d.ts.map +1 -0
  63. package/dist/popups/agentChoicePopup.js +74 -0
  64. package/dist/popups/agentChoicePopup.js.map +1 -0
  65. package/dist/popups/choicePopup.d.ts +7 -0
  66. package/dist/popups/choicePopup.d.ts.map +1 -0
  67. package/dist/popups/choicePopup.js +64 -0
  68. package/dist/popups/choicePopup.js.map +1 -0
  69. package/dist/popups/components/FileList.d.ts +13 -0
  70. package/dist/popups/components/FileList.d.ts.map +1 -0
  71. package/dist/popups/components/FileList.js +61 -0
  72. package/dist/popups/components/FileList.js.map +1 -0
  73. package/dist/popups/components/PopupContainer.d.ts +14 -0
  74. package/dist/popups/components/PopupContainer.d.ts.map +1 -0
  75. package/dist/popups/components/PopupContainer.js +15 -0
  76. package/dist/popups/components/PopupContainer.js.map +1 -0
  77. package/dist/popups/components/PopupInputBox.d.ts +11 -0
  78. package/dist/popups/components/PopupInputBox.d.ts.map +1 -0
  79. package/dist/popups/components/PopupInputBox.js +10 -0
  80. package/dist/popups/components/PopupInputBox.js.map +1 -0
  81. package/dist/popups/components/PopupWrapper.d.ts +37 -0
  82. package/dist/popups/components/PopupWrapper.d.ts.map +1 -0
  83. package/dist/popups/components/PopupWrapper.js +88 -0
  84. package/dist/popups/components/PopupWrapper.js.map +1 -0
  85. package/dist/popups/components/index.d.ts +8 -0
  86. package/dist/popups/components/index.d.ts.map +1 -0
  87. package/dist/popups/components/index.js +8 -0
  88. package/dist/popups/components/index.js.map +1 -0
  89. package/dist/popups/config.d.ts +40 -0
  90. package/dist/popups/config.d.ts.map +1 -0
  91. package/dist/popups/config.js +40 -0
  92. package/dist/popups/config.js.map +1 -0
  93. package/dist/popups/confirmPopup.d.ts +7 -0
  94. package/dist/popups/confirmPopup.d.ts.map +1 -0
  95. package/dist/popups/confirmPopup.js +72 -0
  96. package/dist/popups/confirmPopup.js.map +1 -0
  97. package/dist/popups/hooksPopup.d.ts +7 -0
  98. package/dist/popups/hooksPopup.d.ts.map +1 -0
  99. package/dist/popups/hooksPopup.js +71 -0
  100. package/dist/popups/hooksPopup.js.map +1 -0
  101. package/dist/popups/inputPopup.d.ts +7 -0
  102. package/dist/popups/inputPopup.d.ts.map +1 -0
  103. package/dist/popups/inputPopup.js +48 -0
  104. package/dist/popups/inputPopup.js.map +1 -0
  105. package/dist/popups/kebabMenuPopup.d.ts +7 -0
  106. package/dist/popups/kebabMenuPopup.d.ts.map +1 -0
  107. package/dist/popups/kebabMenuPopup.js +52 -0
  108. package/dist/popups/kebabMenuPopup.js.map +1 -0
  109. package/dist/popups/logsPopup.d.ts +12 -0
  110. package/dist/popups/logsPopup.d.ts.map +1 -0
  111. package/dist/popups/logsPopup.js +364 -0
  112. package/dist/popups/logsPopup.js.map +1 -0
  113. package/dist/popups/mergePopup.d.ts +7 -0
  114. package/dist/popups/mergePopup.d.ts.map +1 -0
  115. package/dist/popups/mergePopup.js +310 -0
  116. package/dist/popups/mergePopup.js.map +1 -0
  117. package/dist/popups/newPanePopup.d.ts +7 -0
  118. package/dist/popups/newPanePopup.d.ts.map +1 -0
  119. package/dist/popups/newPanePopup.js +234 -0
  120. package/dist/popups/newPanePopup.js.map +1 -0
  121. package/dist/popups/progressPopup.d.ts +8 -0
  122. package/dist/popups/progressPopup.d.ts.map +1 -0
  123. package/dist/popups/progressPopup.js +54 -0
  124. package/dist/popups/progressPopup.js.map +1 -0
  125. package/dist/popups/remotePopup.d.ts +6 -0
  126. package/dist/popups/remotePopup.d.ts.map +1 -0
  127. package/dist/popups/remotePopup.js +166 -0
  128. package/dist/popups/remotePopup.js.map +1 -0
  129. package/dist/popups/settingsPopup.d.ts +7 -0
  130. package/dist/popups/settingsPopup.d.ts.map +1 -0
  131. package/dist/popups/settingsPopup.js +212 -0
  132. package/dist/popups/settingsPopup.js.map +1 -0
  133. package/dist/popups/shortcutsPopup.d.ts +6 -0
  134. package/dist/popups/shortcutsPopup.d.ts.map +1 -0
  135. package/dist/popups/shortcutsPopup.js +74 -0
  136. package/dist/popups/shortcutsPopup.js.map +1 -0
  137. package/dist/popups/templates/SimpleInputPopup.d.ts +15 -0
  138. package/dist/popups/templates/SimpleInputPopup.d.ts.map +1 -0
  139. package/dist/popups/templates/SimpleInputPopup.js +28 -0
  140. package/dist/popups/templates/SimpleInputPopup.js.map +1 -0
  141. package/dist/server/embedded-assets.d.ts.map +1 -1
  142. package/dist/server/embedded-assets.js +2077 -968
  143. package/dist/server/embedded-assets.js.map +1 -1
  144. package/dist/server/index.js +1 -1
  145. package/dist/server/index.js.map +1 -1
  146. package/dist/server/routes.d.ts +1 -1
  147. package/dist/server/routes.d.ts.map +1 -1
  148. package/dist/server/routes.js +73 -6
  149. package/dist/server/routes.js.map +1 -1
  150. package/dist/services/ConfigWatcher.d.ts.map +1 -1
  151. package/dist/services/ConfigWatcher.js +10 -3
  152. package/dist/services/ConfigWatcher.js.map +1 -1
  153. package/dist/services/LogService.d.ts +112 -0
  154. package/dist/services/LogService.d.ts.map +1 -0
  155. package/dist/services/LogService.js +252 -0
  156. package/dist/services/LogService.js.map +1 -0
  157. package/dist/services/PaneWorkerManager.d.ts.map +1 -1
  158. package/dist/services/PaneWorkerManager.js +35 -9
  159. package/dist/services/PaneWorkerManager.js.map +1 -1
  160. package/dist/services/TunnelService.d.ts +1 -0
  161. package/dist/services/TunnelService.d.ts.map +1 -1
  162. package/dist/services/TunnelService.js +56 -15
  163. package/dist/services/TunnelService.js.map +1 -1
  164. package/dist/shared/StateManager.d.ts +49 -1
  165. package/dist/shared/StateManager.d.ts.map +1 -1
  166. package/dist/shared/StateManager.js +97 -2
  167. package/dist/shared/StateManager.js.map +1 -1
  168. package/dist/spacer-pane.d.ts +8 -0
  169. package/dist/spacer-pane.d.ts.map +1 -0
  170. package/dist/spacer-pane.js +40 -0
  171. package/dist/spacer-pane.js.map +1 -0
  172. package/dist/theme/colors.d.ts +25 -0
  173. package/dist/theme/colors.d.ts.map +1 -0
  174. package/dist/theme/colors.js +33 -0
  175. package/dist/theme/colors.js.map +1 -0
  176. package/dist/types.d.ts +14 -0
  177. package/dist/types.d.ts.map +1 -1
  178. package/dist/utils/asciiArt.d.ts +26 -0
  179. package/dist/utils/asciiArt.d.ts.map +1 -0
  180. package/dist/utils/asciiArt.js +88 -0
  181. package/dist/utils/asciiArt.js.map +1 -0
  182. package/dist/utils/conflictResolutionPane.d.ts.map +1 -1
  183. package/dist/utils/conflictResolutionPane.js +8 -4
  184. package/dist/utils/conflictResolutionPane.js.map +1 -1
  185. package/dist/utils/fileScanner.d.ts +23 -0
  186. package/dist/utils/fileScanner.d.ts.map +1 -0
  187. package/dist/utils/fileScanner.js +123 -0
  188. package/dist/utils/fileScanner.js.map +1 -0
  189. package/dist/utils/generated-agents-doc.d.ts +1 -1
  190. package/dist/utils/generated-agents-doc.js +1 -1
  191. package/dist/utils/hooks.d.ts +3 -3
  192. package/dist/utils/hooks.d.ts.map +1 -1
  193. package/dist/utils/hooks.js +70 -41
  194. package/dist/utils/hooks.js.map +1 -1
  195. package/dist/utils/hooksDocs.d.ts +1 -1
  196. package/dist/utils/layoutManager.d.ts +45 -0
  197. package/dist/utils/layoutManager.d.ts.map +1 -0
  198. package/dist/utils/layoutManager.js +500 -0
  199. package/dist/utils/layoutManager.js.map +1 -0
  200. package/dist/utils/paneCreation.d.ts.map +1 -1
  201. package/dist/utils/paneCreation.js +125 -11
  202. package/dist/utils/paneCreation.js.map +1 -1
  203. package/dist/utils/popup.d.ts +97 -0
  204. package/dist/utils/popup.d.ts.map +1 -0
  205. package/dist/utils/popup.js +509 -0
  206. package/dist/utils/popup.js.map +1 -0
  207. package/dist/utils/postPaneCleanup.d.ts +12 -0
  208. package/dist/utils/postPaneCleanup.d.ts.map +1 -0
  209. package/dist/utils/postPaneCleanup.js +53 -0
  210. package/dist/utils/postPaneCleanup.js.map +1 -0
  211. package/dist/utils/shellPaneDetection.d.ts +44 -0
  212. package/dist/utils/shellPaneDetection.d.ts.map +1 -0
  213. package/dist/utils/shellPaneDetection.js +175 -0
  214. package/dist/utils/shellPaneDetection.js.map +1 -0
  215. package/dist/utils/tmux.d.ts +53 -1
  216. package/dist/utils/tmux.d.ts.map +1 -1
  217. package/dist/utils/tmux.js +352 -84
  218. package/dist/utils/tmux.js.map +1 -1
  219. package/dist/utils/welcomePane.d.ts +22 -0
  220. package/dist/utils/welcomePane.d.ts.map +1 -0
  221. package/dist/utils/welcomePane.js +119 -0
  222. package/dist/utils/welcomePane.js.map +1 -0
  223. package/dist/utils/welcomePaneManager.d.ts +36 -0
  224. package/dist/utils/welcomePaneManager.d.ts.map +1 -0
  225. package/dist/utils/welcomePaneManager.js +160 -0
  226. package/dist/utils/welcomePaneManager.js.map +1 -0
  227. package/dist/workers/PaneWorker.js +2 -0
  228. package/dist/workers/PaneWorker.js.map +1 -1
  229. package/package.json +5 -2
  230. package/dist/components/NewPaneDialog.d.ts +0 -9
  231. package/dist/components/NewPaneDialog.d.ts.map +0 -1
  232. package/dist/components/NewPaneDialog.js +0 -11
  233. package/dist/components/NewPaneDialog.js.map +0 -1
@@ -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
- const CleanTextInput = ({ value, onChange, onSubmit, placeholder = '' }) => {
431
- const { isFocused } = useFocus({ autoFocus: true });
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
- if (!isFocused)
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
- // Escape clears
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
- onChange('');
604
- setCursor(0);
605
- return;
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" }, wrappedLines.map((wrappedLine, idx) => {
1038
- const isFirst = idx === 0;
1039
- const hasCursor = idx === cursorPos.line;
1040
- const line = wrappedLine.line;
1041
- if (hasCursor) {
1042
- // Ensure cursor position is valid
1043
- const actualCol = Math.min(cursorPos.col, line.length);
1044
- const before = line.slice(0, actualCol);
1045
- const at = line[actualCol] || ' ';
1046
- const after = line.slice(actualCol + 1);
1047
- // Check if cursor is within a paste tag
1048
- const tagPattern = /\\[#\\d+ Pasted, \\d+ lines?\\]/g;
1049
- let match;
1050
- let cursorInTag = false;
1051
- while ((match = tagPattern.exec(line)) !== null) {
1052
- if (actualCol >= match.index && actualCol < match.index + match[0].length) {
1053
- cursorInTag = true;
1054
- break;
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, cursorInTag ? (
1061
- // Cursor is within a paste tag - render specially
1062
- React.createElement(React.Fragment, null,
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, null, isFirst ? '> ' : ' ')),
1075
- React.createElement(Box, null, line ? renderTextWithTags(line) : React.createElement(Text, null, ' '))));
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: 29918
1161
+ size: 34293
1082
1162
  },
1083
1163
  'DmuxApp.js': {
1084
- content: `import React, { useState, useEffect } from 'react';
1085
- import { Box, Text, useInput, useApp } from 'ink';
1086
- import { execSync } from 'child_process';
1087
- import path from 'path';
1088
- import { createRequire } from 'module';
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 './hooks/usePanes.js';
1091
- import useProjectSettings from './hooks/useProjectSettings.js';
1092
- import useTerminalWidth from './hooks/useTerminalWidth.js';
1093
- import useNavigation from './hooks/useNavigation.js';
1094
- import useAutoUpdater from './hooks/useAutoUpdater.js';
1095
- import useAgentDetection from './hooks/useAgentDetection.js';
1096
- import useAgentStatus from './hooks/useAgentStatus.js';
1097
- import usePaneRunner from './hooks/usePaneRunner.js';
1098
- import usePaneCreation from './hooks/usePaneCreation.js';
1099
- import useActionSystem from './hooks/useActionSystem.js';
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 { applySmartLayout } from './utils/tmux.js';
1102
- import { suggestCommand } from './utils/commands.js';
1103
- import { generateSlug } from './utils/slug.js';
1104
- import { getMainBranch } from './utils/git.js';
1105
- import { capturePaneContent } from './utils/paneCapture.js';
1106
- import { StateManager } from './shared/StateManager.js';
1107
- import { getStatusDetector } from './services/StatusDetector.js';
1108
- import { PaneAction, getAvailableActions } from './actions/index.js';
1109
- import { SettingsManager, SETTING_DEFINITIONS } from './utils/settingsManager.js';
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('../package.json');
1112
- import PanesGrid from './components/PanesGrid.js';
1113
- import NewPaneDialog from './components/NewPaneDialog.js';
1114
- import AgentChoiceDialog from './components/AgentChoiceDialog.js';
1115
- import CommandPromptDialog from './components/CommandPromptDialog.js';
1116
- import FileCopyPrompt from './components/FileCopyPrompt.js';
1117
- import LoadingIndicator from './components/LoadingIndicator.js';
1118
- import RunningIndicator from './components/RunningIndicator.js';
1119
- import UpdatingIndicator from './components/UpdatingIndicator.js';
1120
- import CreatingIndicator from './components/CreatingIndicator.js';
1121
- import FooterHelp from './components/FooterHelp.js';
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 [showNewPaneDialog, setShowNewPaneDialog] = useState(false);
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
- const [pendingPrompt, setPendingPrompt] = useState('');
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 = () => setForceRepaintTrigger(prev => prev + 1);
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('tmux refresh-client', { stdio: 'pipe' });
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 !== 'waiting' && pane.agentStatus === 'waiting') {
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 !== 'idle' && pane.agentStatus === 'idle') {
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 === 'working') {
1397
+ if (event.status === "working") {
1277
1398
  updated.analyzerError = undefined;
1278
1399
  }
1279
- else if (event.status === 'waiting' || event.status === 'idle') {
1280
- if (event.analyzerError === undefined && (event.optionsQuestion || event.summary)) {
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('Failed to save panes after status update:', err);
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('status-updated', handleStatusUpdate);
1417
+ statusDetector.on("status-updated", handleStatusUpdate);
1296
1418
  return () => {
1297
- statusDetector.off('status-updated', handleStatusUpdate);
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('SIGTERM', handleTermination);
1323
- // Test debug message on mount
1324
- StateManager.getInstance().setDebugMessage('Debug logging initialized - watching for AI activity...');
1325
- setTimeout(() => {
1326
- StateManager.getInstance().setDebugMessage('');
1327
- }, 3000);
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('SIGTERM', handleTermination);
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] || 'claude');
1459
+ setAgentChoice(availableAgents[0] || "claude");
1360
1460
  }
1361
1461
  }, [availableAgents]);
1362
- // Monitor agent status across panes (returns a map of pane ID to status)
1363
- const agentStatuses = useAgentStatus({
1364
- panes,
1365
- suspend: showNewPaneDialog || actionSystem.actionState.showConfirmDialog || actionSystem.actionState.showChoiceDialog || actionSystem.actionState.showInputDialog || actionSystem.actionState.showProgressDialog || !!showCommandPrompt || showFileCopyPrompt,
1366
- onPaneRemoved: (paneId) => {
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 createNewPane = async (prompt, agent) => {
1386
- setIsCreatingPane(true);
1387
- setStatusMessage('Generating slug...');
1388
- const slug = await generateSlug(prompt);
1389
- setStatusMessage(\`Creating worktree: \${slug}...\`);
1390
- // Get git root directory for consistent worktree placement
1391
- let projectRoot;
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
- projectRoot = execSync('git rev-parse --show-toplevel', {
1394
- encoding: 'utf-8',
1395
- stdio: 'pipe'
1396
- }).trim();
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
- // Fallback to current directory if not in a git repo
1400
- projectRoot = process.cwd();
1540
+ catch (error) {
1541
+ setStatusMessage(\`Failed to launch popup: \${error.message}\`);
1542
+ setTimeout(() => setStatusMessage(""), 3000);
1401
1543
  }
1402
- // Create worktree path inside .dmux/worktrees directory
1403
- const worktreePath = path.join(projectRoot, '.dmux', 'worktrees', slug);
1404
- // Get the original pane ID (where dmux is running) before clearing
1405
- const originalPaneId = execSync('tmux display-message -p "#{pane_id}"', { encoding: 'utf-8' }).trim();
1406
- // Multiple clearing strategies to prevent artifacts
1407
- // 1. Clear screen with ANSI codes
1408
- process.stdout.write('\\x1b[2J\\x1b[H');
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
- catch { }
1417
- // Wait a bit for clearing to settle
1418
- await new Promise(resolve => setTimeout(resolve, 100));
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
- execSync(\`tmux set-option -g pane-border-status top\`, { stdio: 'pipe' });
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
- // Ignore if already set or fails
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
- execSync(\`tmux select-pane -t '\${paneInfo}' -T "\${slug}"\`, { stdio: 'pipe' });
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
- // Ignore if setting title fails
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
- // First, create the worktree and cd into it as a single command
1451
- // Use ; instead of && to ensure cd runs even if worktree already exists
1452
- const worktreeCmd = \`git worktree add "\${worktreePath}" -b \${slug} 2>/dev/null ; cd "\${worktreePath}"\`;
1453
- execSync(\`tmux send-keys -t '\${paneInfo}' '\${worktreeCmd}' Enter\`, { stdio: 'pipe' });
1454
- // Wait longer for worktree creation and cd to complete
1455
- // This is critical - if we don't wait long enough, Claude will start in the wrong directory
1456
- await new Promise(resolve => setTimeout(resolve, 2500));
1457
- // Verify we're in the worktree directory by sending pwd command
1458
- execSync(\`tmux send-keys -t '\${paneInfo}' 'echo "Worktree created at:" && pwd' Enter\`, { stdio: 'pipe' });
1459
- await new Promise(resolve => setTimeout(resolve, 500));
1460
- setStatusMessage(agent ? \`Worktree created, launching \${agent === 'opencode' ? 'opencode' : 'Claude'}...\` : 'Worktree created.');
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
- // Log error but continue - worktree creation is essential
1464
- setStatusMessage(\`Warning: Worktree issue: \${error}\`);
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
- // Prepare and send the agent command
1470
- let escapedCmd = '';
1471
- if (agent === 'claude') {
1472
- // Claude should always be launched AFTER we're in the worktree directory
1473
- let claudeCmd;
1474
- if (prompt && prompt.trim()) {
1475
- const escapedPrompt = prompt
1476
- .replace(/\\\\/g, '\\\\\\\\')
1477
- .replace(/"/g, '\\\\"')
1478
- .replace(/\`/g, '\\\\\`')
1479
- .replace(/\\$/g, '\\\\$');
1480
- claudeCmd = \`claude "\${escapedPrompt}" --permission-mode=acceptEdits\`;
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
- claudeCmd = \`claude --permission-mode=acceptEdits\`;
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
- else if (agent === 'opencode') {
1491
- // opencode: start the TUI, then paste the prompt and submit
1492
- const openCoderCmd = \`opencode\`;
1493
- const escapedOpenCmd = openCoderCmd.replace(/'/g, "'\\\\''");
1494
- execSync(\`tmux send-keys -t '\${paneInfo}' '\${escapedOpenCmd}'\`, { stdio: 'pipe' });
1495
- execSync(\`tmux send-keys -t '\${paneInfo}' Enter\`, { stdio: 'pipe' });
1496
- if (prompt && prompt.trim()) {
1497
- await new Promise(resolve => setTimeout(resolve, 1500));
1498
- const bufName = \`dmux_prompt_\${Date.now()}\`;
1499
- const promptEsc = prompt.replace(/\\\\/g, '\\\\\\\\').replace(/'/g, "'\\\\''");
1500
- execSync(\`tmux set-buffer -b '\${bufName}' -- '\${promptEsc}'\`, { stdio: 'pipe' });
1501
- execSync(\`tmux paste-buffer -b '\${bufName}' -t '\${paneInfo}'\`, { stdio: 'pipe' });
1502
- await new Promise(resolve => setTimeout(resolve, 200));
1503
- execSync(\`tmux delete-buffer -b '\${bufName}'\`, { stdio: 'pipe' });
1504
- execSync(\`tmux send-keys -t '\${paneInfo}' Enter\`, { stdio: 'pipe' });
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
- if (agent === 'claude') {
1508
- // Monitor for Claude Code trust prompt and auto-respond
1509
- const autoApproveTrust = async () => {
1510
- // Wait for Claude to start up before checking for prompts
1511
- await new Promise(resolve => setTimeout(resolve, 800));
1512
- const maxChecks = 100; // 100 checks * 100ms = 10 seconds total
1513
- const checkInterval = 100; // Check every 100ms
1514
- let lastContent = '';
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) { // Log every 10 checks (every second)
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('Do you trust') ||
1564
- paneContent.includes('trust the files') ||
1565
- paneContent.includes('permission') ||
1566
- paneContent.includes('allow') ||
1567
- (paneContent.includes('folder') && paneContent.includes('?'));
1568
- if ((hasTrustPrompt || hasClaudePermissionPrompt) && !promptHandled) {
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\`, { stdio: 'pipe' });
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'\`, { stdio: 'pipe' });
1582
- await new Promise(resolve => setTimeout(resolve, 50));
1583
- execSync(\`tmux send-keys -t '\${paneInfo}' Enter\`, { stdio: 'pipe' });
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\`, { stdio: 'pipe' });
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('Claude') ||
1599
- updatedContent.includes('claude') ||
1600
- updatedContent.includes('Assistant') ||
1601
- (prompt && updatedContent.includes(prompt.substring(0, Math.min(20, prompt.length))));
1602
- if (!claudeRunning && !updatedContent.includes('$')) {
1603
- await new Promise(resolve => setTimeout(resolve, 300));
1604
- execSync(\`tmux send-keys -t '\${paneInfo}' '\${escapedCmd}'\`, { stdio: 'pipe' });
1605
- execSync(\`tmux send-keys -t '\${paneInfo}' Enter\`, { stdio: 'pipe' });
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 && !hasClaudePermissionPrompt &&
1613
- (paneContent.includes('Claude') || paneContent.includes('Assistant'))) {
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: 'pipe' });
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 || 'No initial 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: 'pipe' });
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-\${projectName}"\`, { stdio: 'pipe' });
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('\\x1b[2J\\x1b[H');
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: 'pipe' });
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: 'pipe' });
1667
- setStatusMessage('Jumped to pane');
1668
- setTimeout(() => setStatusMessage(''), 2000);
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('Failed to jump - pane may be closed');
1672
- setTimeout(() => setStatusMessage(''), 2000);
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('No worktree path for this pane');
1678
- setTimeout(() => setStatusMessage(''), 2000);
2701
+ setStatusMessage("No worktree path for this pane");
2702
+ setTimeout(() => setStatusMessage(""), 2000);
1679
2703
  return;
1680
2704
  }
1681
- const command = type === 'test' ? projectSettings.testCommand : projectSettings.devCommand;
1682
- const isFirstRun = type === 'test' ? !projectSettings.firstTestRun : !projectSettings.firstDevRun;
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 === 'test' ? pane.testWindowId : pane.devWindowId;
2727
+ const existingWindowId = type === "test" ? pane.testWindowId : pane.devWindowId;
1702
2728
  if (existingWindowId) {
1703
2729
  try {
1704
- execSync(\`tmux kill-window -t '\${existingWindowId}'\`, { stdio: 'pipe' });
1705
- }
1706
- catch { }
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
- return;
2734
+ catch { }
2079
2735
  }
2080
- else if (input === 'n' || input === 'N') {
2081
- // Shortcut: no
2082
- if (actionSystem.actionState.onConfirmNo) {
2083
- actionSystem.executeCallback(actionSystem.actionState.onConfirmNo);
2084
- }
2085
- else {
2086
- actionSystem.setActionState(prev => ({ ...prev, showConfirmDialog: false }));
2087
- }
2088
- return;
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 action system input dialog
2093
- if (actionSystem.actionState.showInputDialog) {
2094
- if (key.escape) {
2095
- actionSystem.setActionState(prev => ({ ...prev, showInputDialog: false }));
2096
- return;
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 if (key.return) {
2099
- if (actionSystem.actionState.onInputSubmit) {
2100
- actionSystem.executeCallback(async () => actionSystem.actionState.onInputSubmit(actionSystem.actionState.inputValue));
2101
- }
2102
- return;
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
- // Handle action system choice dialog
2108
- if (actionSystem.actionState.showChoiceDialog) {
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
- actionSystem.setActionState(prev => ({ ...prev, showChoiceDialog: false }));
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 === 'y' || input === 'Y') {
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 === 'test' ? 'firstTestRun' : 'firstDevRun']: true
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 === 'n' || input === 'N' || key.escape) {
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 === 'test' ? 'firstTestRun' : 'firstDevRun']: true
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 === 'test' ? 'testCommand' : 'devCommand']: commandInput.trim()
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 === 'test' ? !projectSettings.firstTestRun : !projectSettings.firstDevRun;
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, 'up');
2941
+ targetIndex = findCardInDirection(selectedIndex, "up");
2250
2942
  }
2251
2943
  else if (key.downArrow) {
2252
- targetIndex = findCardInDirection(selectedIndex, 'down');
2944
+ targetIndex = findCardInDirection(selectedIndex, "down");
2253
2945
  }
2254
2946
  else if (key.leftArrow) {
2255
- targetIndex = findCardInDirection(selectedIndex, 'left');
2947
+ targetIndex = findCardInDirection(selectedIndex, "left");
2256
2948
  }
2257
2949
  else if (key.rightArrow) {
2258
- targetIndex = findCardInDirection(selectedIndex, 'right');
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 ((input === 'm' || key.return) && selectedIndex < panes.length) {
2266
- // Open kebab menu for selected pane
2267
- const selectedPane = panes[selectedIndex];
2268
- const actions = getAvailableActions(selectedPane, projectSettings);
2269
- setKebabMenuActions(actions);
2270
- setShowKebabMenu(true);
2271
- setKebabMenuPaneIndex(selectedIndex);
2272
- setKebabMenuOption(0);
2273
- }
2274
- else if (input === 's') {
2275
- // Open settings dialog
2276
- setShowSettingsDialog(true);
2277
- setSettingsMode('list');
2278
- setSettingsSelectedIndex(0);
2279
- }
2280
- else if (input === 'q') {
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 === 'r' && server) {
2284
- // Create tunnel if not already created, then show QR code
2285
- if (!tunnelUrl) {
2286
- setIsCreatingTunnel(true);
2287
- setStatusMessage('Creating tunnel...');
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
- // Tunnel already exists, just show the QR code
2304
- setShowQRCode(true);
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 && (input === 'n' || (key.return && selectedIndex === panes.length))) {
2308
- // Clear the prompt and show dialog in next tick to prevent 'n' bleeding through
2309
- setNewPanePrompt('');
2310
- setShowNewPaneDialog(true);
2311
- return; // Consume the 'n' keystroke so it doesn't propagate
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 === 'j' && selectedIndex < panes.length) {
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(''), 2000);
3044
+ setTimeout(() => StateManager.getInstance().setDebugMessage(""), 2000);
2317
3045
  actionSystem.executeAction(PaneAction.VIEW, panes[selectedIndex]);
2318
3046
  }
2319
- else if (input === 'x' && selectedIndex < panes.length) {
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(''), 2000);
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(''), 2000);
3056
+ setTimeout(() => StateManager.getInstance().setDebugMessage(""), 2000);
2329
3057
  actionSystem.executeAction(PaneAction.VIEW, panes[selectedIndex]);
2330
3058
  }
2331
3059
  });
2332
- // If showing QR code, render only that
2333
- if (showQRCode && tunnelUrl) {
2334
- return (React.createElement(Box, { flexDirection: "column" },
2335
- React.createElement(Box, { marginBottom: 1 },
2336
- React.createElement(Text, { bold: true, color: "cyan" }, "dmux - Remote Access")),
2337
- React.createElement(QRCode, { url: tunnelUrl }),
2338
- React.createElement(Box, { marginTop: 1 },
2339
- React.createElement(Text, { dimColor: true }, "Press ESC to return to pane list"))));
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
- return (React.createElement(Box, { flexDirection: "column" },
2342
- React.createElement(PanesGrid, { panes: panes, selectedIndex: selectedIndex, isLoading: isLoading, showNewPaneDialog: showNewPaneDialog, agentStatuses: agentStatuses, kebabMenuPaneIndex: kebabMenuPaneIndex ?? undefined }),
2343
- isLoading && (React.createElement(LoadingIndicator, null)),
2344
- showNewPaneDialog && !showAgentChoiceDialog && (React.createElement(NewPaneDialog, { value: newPanePrompt, onChange: setNewPanePrompt, onSubmit: (value) => {
2345
- const promptValue = value;
2346
- const agents = availableAgents;
2347
- if (agents.length === 0) {
2348
- setShowNewPaneDialog(false);
2349
- setNewPanePrompt('');
2350
- createNewPaneHook(promptValue);
2351
- }
2352
- else if (agents.length === 1) {
2353
- setShowNewPaneDialog(false);
2354
- setNewPanePrompt('');
2355
- createNewPaneHook(promptValue, agents[0]);
2356
- }
2357
- else {
2358
- setPendingPrompt(promptValue);
2359
- setShowNewPaneDialog(false);
2360
- setNewPanePrompt('');
2361
- setShowAgentChoiceDialog(true);
2362
- setAgentChoice(agentChoice || 'claude');
2363
- }
2364
- } })),
2365
- showAgentChoiceDialog && (React.createElement(AgentChoiceDialog, { agentChoice: agentChoice })),
2366
- isCreatingPane && (React.createElement(CreatingIndicator, { message: statusMessage })),
2367
- showCommandPrompt && (React.createElement(CommandPromptDialog, { type: showCommandPrompt, value: commandInput, onChange: setCommandInput })),
2368
- showFileCopyPrompt && (React.createElement(FileCopyPrompt, null)),
2369
- showKebabMenu && kebabMenuPaneIndex !== null && panes[kebabMenuPaneIndex] && (React.createElement(KebabMenu, { selectedOption: kebabMenuOption, actions: kebabMenuActions, paneName: panes[kebabMenuPaneIndex].slug })),
2370
- showSettingsDialog && (React.createElement(SettingsDialog, { settings: settingsManager.getSettings(), globalSettings: settingsManager.getGlobalSettings(), projectSettings: settingsManager.getProjectSettings(), settingDefinitions: SETTING_DEFINITIONS, selectedIndex: settingsSelectedIndex, mode: settingsMode, editingKey: settingsEditingKey, editingValueIndex: settingsEditingValueIndex, scopeIndex: settingsScopeIndex })),
2371
- showHooksDialog && (React.createElement(HooksDialog, { hooks: hooksData, selectedIndex: hooksSelectedIndex })),
2372
- actionSystem.actionState.showConfirmDialog && (React.createElement(ActionConfirmDialog, { key: "confirm-dialog", title: actionSystem.actionState.confirmTitle, message: actionSystem.actionState.confirmMessage, yesLabel: actionSystem.actionState.confirmYesLabel, noLabel: actionSystem.actionState.confirmNoLabel, selectedIndex: actionSystem.actionState.confirmSelectedIndex })),
2373
- actionSystem.actionState.showChoiceDialog && (React.createElement(ActionChoiceDialog, { key: "choice-dialog", title: actionSystem.actionState.choiceTitle, message: actionSystem.actionState.choiceMessage, options: actionSystem.actionState.choiceOptions, selectedIndex: actionSystem.actionState.choiceSelectedIndex })),
2374
- actionSystem.actionState.showInputDialog && (React.createElement(ActionInputDialog, { key: "input-dialog", title: actionSystem.actionState.inputTitle, message: actionSystem.actionState.inputMessage, placeholder: actionSystem.actionState.inputPlaceholder, value: actionSystem.actionState.inputValue, onValueChange: (value) => {
2375
- actionSystem.setActionState(prev => ({ ...prev, inputValue: value }));
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, { marginTop: 1 },
2387
- React.createElement(Text, { color: actionSystem.actionState.statusType === 'error' ? 'red' :
2388
- actionSystem.actionState.statusType === 'success' ? 'green' :
2389
- 'cyan' }, actionSystem.actionState.statusMessage))),
2390
- React.createElement(FooterHelp, { show: !showNewPaneDialog && !showCommandPrompt, showRemoteKey: !!server, quitConfirmMode: quitConfirmMode, gridInfo: (() => {
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 }, "Update available: npm i -g dmux@latest ")),
2400
- "v",
2401
- packageJson.version,
2402
- serverPort && serverPort > 0 && (React.createElement(Text, { dimColor: true },
2403
- " \\u2022 ",
2404
- React.createElement(Text, { color: "cyan" },
2405
- "http://127.0.0.1:",
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: 67765
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: 27228
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-\${this.projectName}"\`, { stdio: 'inherit' });
4869
- // Send dmux command to the new session
4870
- execSync(\`tmux send-keys -t \${this.sessionName} "dmux" Enter\`, { stdio: 'inherit' });
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
- try {
4877
- execSync(\`tmux set-option pane-border-status top\`, { stdio: 'pipe' });
4878
- }
4879
- catch {
4880
- // Ignore if it fails
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
- execSync(\`tmux select-pane -T "dmux-\${this.projectName}"\`, { stdio: 'pipe' });
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 if it fails (might not have permission or tmux version doesn't support it)
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
- console.error('Failed to start HTTP server:', err);
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
- // Clear screen before launching Ink to prevent artifacts
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
- process.stdout.write('\\x1b[3J'); // Clear scrollback buffer
4906
- // Clear tmux history
4907
- try {
4908
- execSync('tmux clear-history', { stdio: 'pipe' });
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: 17447
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>