dmux 3.1.1 → 3.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (232) 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 +1396 -750
  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 +13 -27
  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 +2066 -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.map +1 -1
  192. package/dist/utils/hooks.js +65 -36
  193. package/dist/utils/hooks.js.map +1 -1
  194. package/dist/utils/hooksDocs.d.ts +1 -1
  195. package/dist/utils/layoutManager.d.ts +45 -0
  196. package/dist/utils/layoutManager.d.ts.map +1 -0
  197. package/dist/utils/layoutManager.js +500 -0
  198. package/dist/utils/layoutManager.js.map +1 -0
  199. package/dist/utils/paneCreation.d.ts.map +1 -1
  200. package/dist/utils/paneCreation.js +125 -11
  201. package/dist/utils/paneCreation.js.map +1 -1
  202. package/dist/utils/popup.d.ts +97 -0
  203. package/dist/utils/popup.d.ts.map +1 -0
  204. package/dist/utils/popup.js +509 -0
  205. package/dist/utils/popup.js.map +1 -0
  206. package/dist/utils/postPaneCleanup.d.ts +12 -0
  207. package/dist/utils/postPaneCleanup.d.ts.map +1 -0
  208. package/dist/utils/postPaneCleanup.js +53 -0
  209. package/dist/utils/postPaneCleanup.js.map +1 -0
  210. package/dist/utils/shellPaneDetection.d.ts +44 -0
  211. package/dist/utils/shellPaneDetection.d.ts.map +1 -0
  212. package/dist/utils/shellPaneDetection.js +175 -0
  213. package/dist/utils/shellPaneDetection.js.map +1 -0
  214. package/dist/utils/tmux.d.ts +53 -1
  215. package/dist/utils/tmux.d.ts.map +1 -1
  216. package/dist/utils/tmux.js +352 -84
  217. package/dist/utils/tmux.js.map +1 -1
  218. package/dist/utils/welcomePane.d.ts +22 -0
  219. package/dist/utils/welcomePane.d.ts.map +1 -0
  220. package/dist/utils/welcomePane.js +119 -0
  221. package/dist/utils/welcomePane.js.map +1 -0
  222. package/dist/utils/welcomePaneManager.d.ts +36 -0
  223. package/dist/utils/welcomePaneManager.d.ts.map +1 -0
  224. package/dist/utils/welcomePaneManager.js +160 -0
  225. package/dist/utils/welcomePaneManager.js.map +1 -0
  226. package/dist/workers/PaneWorker.js +2 -0
  227. package/dist/workers/PaneWorker.js.map +1 -1
  228. package/package.json +5 -2
  229. package/dist/components/NewPaneDialog.d.ts +0 -9
  230. package/dist/components/NewPaneDialog.d.ts.map +0 -1
  231. package/dist/components/NewPaneDialog.js +0 -11
  232. 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,176 +1103,175 @@ const CleanTextInput = ({ value, onChange, onSubmit, placeholder = '' }) => {
1031
1103
  React.createElement(Box, null,
1032
1104
  React.createElement(Text, { inverse: true }, ' '))));
1033
1105
  }
1034
- // Find cursor position in wrapped lines
1035
- const cursorPos = findCursorInWrappedLines(wrappedLines, cursor);
1036
1106
  // Render wrapped lines
1037
- return (React.createElement(Box, { flexDirection: "column" }, 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);
1150
1218
  // Spinner state - shows for a few frames to force render
1151
1219
  const [showRepaintSpinner, setShowRepaintSpinner] = useState(false);
1152
1220
  const { projectSettings, saveSettings } = useProjectSettings(settingsFile);
1153
- // Hooks management state
1154
- const [showHooksDialog, setShowHooksDialog] = useState(false);
1155
- const [hooksSelectedIndex, setHooksSelectedIndex] = useState(0);
1156
- const [hooksData, setHooksData] = useState([]);
1157
1221
  const [showCommandPrompt, setShowCommandPrompt] = useState(null);
1158
- const [commandInput, setCommandInput] = useState('');
1222
+ const [commandInput, setCommandInput] = useState("");
1159
1223
  const [showFileCopyPrompt, setShowFileCopyPrompt] = useState(false);
1160
1224
  const [currentCommandType, setCurrentCommandType] = useState(null);
1161
1225
  const [runningCommand, setRunningCommand] = useState(false);
1162
1226
  const [quitConfirmMode, setQuitConfirmMode] = useState(false);
1163
- const [showKebabMenu, setShowKebabMenu] = useState(false);
1164
- const [kebabMenuPaneIndex, setKebabMenuPaneIndex] = useState(null);
1165
- const [kebabMenuOption, setKebabMenuOption] = useState(0);
1166
- const [kebabMenuActions, setKebabMenuActions] = useState([]);
1167
1227
  // Debug message state - for temporary logging messages
1168
- const [debugMessage, setDebugMessage] = useState('');
1228
+ const [debugMessage, setDebugMessage] = useState("");
1229
+ // Current git branch state (for dev builds)
1230
+ const [currentBranch, setCurrentBranch] = useState(null);
1169
1231
  // Update state handled by hook
1170
- const { updateInfo, showUpdateDialog, isUpdating, performUpdate, skipUpdate, dismissUpdate, updateAvailable } = useAutoUpdater(autoUpdater, setStatusMessage);
1232
+ const { updateInfo, showUpdateDialog, isUpdating, performUpdate, skipUpdate, dismissUpdate, updateAvailable, } = useAutoUpdater(autoUpdater, setStatusMessage);
1171
1233
  const { exit } = useApp();
1234
+ // Flag to ignore input temporarily after popup closes (prevents buffered keys)
1235
+ const [ignoreInput, setIgnoreInput] = useState(false);
1172
1236
  // Agent selection state
1173
1237
  const { availableAgents } = useAgentDetection();
1174
- const [showAgentChoiceDialog, setShowAgentChoiceDialog] = useState(false);
1175
1238
  const [agentChoice, setAgentChoice] = useState(null);
1176
- const [pendingPrompt, setPendingPrompt] = useState('');
1239
+ // Popup support detection
1240
+ const [popupsSupported, setPopupsSupported] = useState(false);
1177
1241
  // Track terminal dimensions for responsive layout
1178
1242
  const terminalWidth = useTerminalWidth();
1243
+ // Track unread error and warning counts for logs badge
1244
+ const [unreadErrorCount, setUnreadErrorCount] = useState(0);
1245
+ const [unreadWarningCount, setUnreadWarningCount] = useState(0);
1246
+ // Tunnel state
1247
+ const [tunnelUrl, setTunnelUrl] = useState(null);
1248
+ const [tunnelCreating, setTunnelCreating] = useState(false);
1249
+ const [tunnelSpinnerFrame, setTunnelSpinnerFrame] = useState(0);
1250
+ const [localIp, setLocalIp] = useState("127.0.0.1");
1251
+ const [tunnelCopied, setTunnelCopied] = useState(false);
1252
+ // Subscribe to StateManager for unread error/warning count updates
1253
+ useEffect(() => {
1254
+ const stateManager = StateManager.getInstance();
1255
+ const updateCounts = () => {
1256
+ setUnreadErrorCount(stateManager.getUnreadErrorCount());
1257
+ setUnreadWarningCount(stateManager.getUnreadWarningCount());
1258
+ };
1259
+ // Initial count
1260
+ updateCounts();
1261
+ // Subscribe to changes
1262
+ const unsubscribe = stateManager.subscribe(updateCounts);
1263
+ return () => {
1264
+ unsubscribe();
1265
+ };
1266
+ }, []);
1179
1267
  // Panes state and persistence (skipLoading will be updated after actionSystem is initialized)
1180
1268
  const { panes, setPanes, isLoading, loadPanes, savePanes } = usePanes(panesFile, false);
1181
1269
  // Track intentionally closed panes to prevent race condition
1182
1270
  // When a user closes a pane, we add it to this set. If the worker detects
1183
1271
  // the pane is gone (which it will), we check this set first before re-saving.
1184
1272
  const intentionallyClosedPanes = React.useRef(new Set());
1185
- // Action system
1186
- const actionSystem = useActionSystem({
1187
- panes,
1188
- savePanes,
1189
- sessionName,
1190
- projectName,
1191
- onPaneRemove: (paneId) => {
1192
- // Mark this pane as intentionally closed
1193
- intentionallyClosedPanes.current.add(paneId);
1194
- const updated = panes.filter(p => p.id !== paneId);
1195
- setPanes(updated);
1196
- // Clean up the tracking after a delay (in case of race conditions)
1197
- setTimeout(() => {
1198
- intentionallyClosedPanes.current.delete(paneId);
1199
- }, 5000);
1200
- },
1201
- });
1202
1273
  // Pane runner
1203
- const { copyNonGitFiles, runCommandInternal, monitorTestOutput, monitorDevOutput, attachBackgroundWindow } = usePaneRunner({
1274
+ const { copyNonGitFiles, runCommandInternal, monitorTestOutput, monitorDevOutput, attachBackgroundWindow, } = usePaneRunner({
1204
1275
  panes,
1205
1276
  savePanes,
1206
1277
  projectSettings,
@@ -1209,7 +1280,7 @@ const DmuxApp = ({ panesFile, projectName, sessionName, settingsFile, projectRoo
1209
1280
  });
1210
1281
  // Force repaint helper - shows spinner for a few frames to force full re-render
1211
1282
  const forceRepaint = () => {
1212
- setForceRepaintTrigger(prev => prev + 1);
1283
+ setForceRepaintTrigger((prev) => prev + 1);
1213
1284
  setShowRepaintSpinner(true);
1214
1285
  // Hide spinner after a few frames (enough to trigger multiple renders)
1215
1286
  setTimeout(() => setShowRepaintSpinner(false), 100);
@@ -1220,13 +1291,57 @@ const DmuxApp = ({ panesFile, projectName, sessionName, settingsFile, projectRoo
1220
1291
  // Small delay to ensure terminal is ready
1221
1292
  const timer = setTimeout(() => {
1222
1293
  try {
1223
- execSync('tmux refresh-client', { stdio: 'pipe' });
1294
+ execSync("tmux refresh-client", { stdio: "pipe" });
1224
1295
  }
1225
1296
  catch { }
1226
1297
  }, 50);
1227
1298
  return () => clearTimeout(timer);
1228
1299
  }
1229
1300
  }, [forceRepaintTrigger]);
1301
+ // Get local network IP on mount
1302
+ useEffect(() => {
1303
+ try {
1304
+ // Get local IP address (not 127.0.0.1)
1305
+ const result = execSync(\`hostname -I 2>/dev/null || ifconfig | grep "inet " | grep -v 127.0.0.1 | awk '{print $2}' | head -1\`, {
1306
+ encoding: "utf-8",
1307
+ stdio: "pipe",
1308
+ }).trim();
1309
+ if (result) {
1310
+ setLocalIp(result.split(" ")[0]); // Take first IP if multiple
1311
+ }
1312
+ }
1313
+ catch {
1314
+ // Fallback to 127.0.0.1
1315
+ setLocalIp("127.0.0.1");
1316
+ }
1317
+ }, []);
1318
+ // Spinner animation for tunnel creation
1319
+ useEffect(() => {
1320
+ if (!tunnelCreating)
1321
+ return;
1322
+ const spinnerInterval = setInterval(() => {
1323
+ setTunnelSpinnerFrame((prev) => (prev + 1) % 10);
1324
+ }, 80); // Update every 80ms
1325
+ return () => clearInterval(spinnerInterval);
1326
+ }, [tunnelCreating]);
1327
+ // Get current git branch on mount (only for dev builds)
1328
+ useEffect(() => {
1329
+ const isDev = process.env.DMUX_DEV === "true" || __dirname.includes("dist") === false;
1330
+ if (isDev) {
1331
+ try {
1332
+ const branch = execSync("git rev-parse --abbrev-ref HEAD", {
1333
+ encoding: "utf-8",
1334
+ stdio: "pipe",
1335
+ cwd: projectRoot,
1336
+ }).trim();
1337
+ setCurrentBranch(branch);
1338
+ }
1339
+ catch {
1340
+ // Not in a git repo or git not available
1341
+ setCurrentBranch(null);
1342
+ }
1343
+ }
1344
+ }, [projectRoot]);
1230
1345
  // Pane creation
1231
1346
  const { createNewPane: createNewPaneHook } = usePaneCreation({
1232
1347
  panes,
@@ -1234,7 +1349,6 @@ const DmuxApp = ({ panesFile, projectName, sessionName, settingsFile, projectRoo
1234
1349
  projectName,
1235
1350
  setIsCreatingPane,
1236
1351
  setStatusMessage,
1237
- setNewPanePrompt,
1238
1352
  loadPanes,
1239
1353
  panesFile,
1240
1354
  availableAgents,
@@ -1244,8 +1358,8 @@ const DmuxApp = ({ panesFile, projectName, sessionName, settingsFile, projectRoo
1244
1358
  useEffect(() => {
1245
1359
  const statusDetector = getStatusDetector();
1246
1360
  const handleStatusUpdate = (event) => {
1247
- setPanes(prevPanes => {
1248
- const updatedPanes = prevPanes.map(pane => {
1361
+ setPanes((prevPanes) => {
1362
+ const updatedPanes = prevPanes.map((pane) => {
1249
1363
  if (pane.id === event.paneId) {
1250
1364
  const updated = {
1251
1365
  ...pane,
@@ -1269,22 +1383,23 @@ const DmuxApp = ({ panesFile, projectName, sessionName, settingsFile, projectRoo
1269
1383
  updated.analyzerError = event.analyzerError;
1270
1384
  }
1271
1385
  // Clear option dialog data when transitioning away from 'waiting' state
1272
- if (event.status !== 'waiting' && pane.agentStatus === 'waiting') {
1386
+ if (event.status !== "waiting" && pane.agentStatus === "waiting") {
1273
1387
  updated.optionsQuestion = undefined;
1274
1388
  updated.options = undefined;
1275
1389
  updated.potentialHarm = undefined;
1276
1390
  }
1277
1391
  // Clear summary when transitioning away from 'idle' state
1278
- if (event.status !== 'idle' && pane.agentStatus === 'idle') {
1392
+ if (event.status !== "idle" && pane.agentStatus === "idle") {
1279
1393
  updated.agentSummary = undefined;
1280
1394
  }
1281
1395
  // Clear analyzer error when successfully getting a new analysis
1282
1396
  // or when transitioning to 'working' status
1283
- if (event.status === 'working') {
1397
+ if (event.status === "working") {
1284
1398
  updated.analyzerError = undefined;
1285
1399
  }
1286
- else if (event.status === 'waiting' || event.status === 'idle') {
1287
- 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)) {
1288
1403
  updated.analyzerError = undefined;
1289
1404
  }
1290
1405
  }
@@ -1293,15 +1408,15 @@ const DmuxApp = ({ panesFile, projectName, sessionName, settingsFile, projectRoo
1293
1408
  return pane;
1294
1409
  });
1295
1410
  // Persist to disk - ConfigWatcher will handle syncing to StateManager
1296
- savePanes(updatedPanes).catch(err => {
1297
- console.error('Failed to save panes after status update:', err);
1411
+ savePanes(updatedPanes).catch((err) => {
1412
+ console.error("Failed to save panes after status update:", err);
1298
1413
  });
1299
1414
  return updatedPanes;
1300
1415
  });
1301
1416
  };
1302
- statusDetector.on('status-updated', handleStatusUpdate);
1417
+ statusDetector.on("status-updated", handleStatusUpdate);
1303
1418
  return () => {
1304
- statusDetector.off('status-updated', handleStatusUpdate);
1419
+ statusDetector.off("status-updated", handleStatusUpdate);
1305
1420
  };
1306
1421
  }, [setPanes, savePanes]);
1307
1422
  // Note: No need to sync panes with StateManager here.
@@ -1326,62 +1441,29 @@ const DmuxApp = ({ panesFile, projectName, sessionName, settingsFile, projectRoo
1326
1441
  const handleTermination = () => {
1327
1442
  cleanExit();
1328
1443
  };
1329
- process.on('SIGTERM', handleTermination);
1330
- // Test debug message on mount
1331
- StateManager.getInstance().setDebugMessage('Debug logging initialized - watching for AI activity...');
1332
- setTimeout(() => {
1333
- StateManager.getInstance().setDebugMessage('');
1334
- }, 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
+ }
1335
1451
  return () => {
1336
- process.removeListener('SIGTERM', handleTermination);
1452
+ process.removeListener("SIGTERM", handleTermination);
1337
1453
  };
1338
1454
  }, []);
1339
- // Auto-show new pane dialog when starting with no panes
1340
- useEffect(() => {
1341
- // Only show the dialog if:
1342
- // 1. Initial load is complete (!isLoading)
1343
- // 2. We have no panes
1344
- // 3. We're not already showing the dialog
1345
- // 4. We're not showing any other dialogs or prompts
1346
- if (!isLoading &&
1347
- panes.length === 0 &&
1348
- !showNewPaneDialog &&
1349
- !actionSystem.actionState.showConfirmDialog &&
1350
- !actionSystem.actionState.showChoiceDialog &&
1351
- !actionSystem.actionState.showInputDialog &&
1352
- !actionSystem.actionState.showProgressDialog &&
1353
- !showCommandPrompt &&
1354
- !showFileCopyPrompt &&
1355
- !showAgentChoiceDialog &&
1356
- !isCreatingPane &&
1357
- !runningCommand &&
1358
- !isUpdating) {
1359
- setShowNewPaneDialog(true);
1360
- }
1361
- }, [isLoading, panes.length, showNewPaneDialog, actionSystem.actionState.showConfirmDialog, actionSystem.actionState.showChoiceDialog, actionSystem.actionState.showInputDialog, actionSystem.actionState.showProgressDialog, showCommandPrompt, showFileCopyPrompt, showAgentChoiceDialog, isCreatingPane, runningCommand, isUpdating]);
1362
1455
  // Update checking moved to useAutoUpdater
1363
1456
  // Set default agent choice when detection completes
1364
1457
  useEffect(() => {
1365
1458
  if (agentChoice == null && availableAgents.length > 0) {
1366
- setAgentChoice(availableAgents[0] || 'claude');
1459
+ setAgentChoice(availableAgents[0] || "claude");
1367
1460
  }
1368
1461
  }, [availableAgents]);
1369
- // Monitor agent status across panes (returns a map of pane ID to status)
1370
- const agentStatuses = useAgentStatus({
1371
- panes,
1372
- suspend: showNewPaneDialog || actionSystem.actionState.showConfirmDialog || actionSystem.actionState.showChoiceDialog || actionSystem.actionState.showInputDialog || actionSystem.actionState.showProgressDialog || !!showCommandPrompt || showFileCopyPrompt,
1373
- onPaneRemoved: (paneId) => {
1374
- // Check if this pane was intentionally closed
1375
- // If so, don't re-save - the close action already handled it
1376
- if (intentionallyClosedPanes.current.has(paneId)) {
1377
- return;
1378
- }
1379
- // Pane was removed unexpectedly (e.g., user killed tmux pane manually)
1380
- // Remove it from our tracking
1381
- const updatedPanes = panes.filter(p => p.id !== paneId);
1382
- savePanes(updatedPanes);
1383
- },
1384
- });
1462
+ // Welcome pane is now fully event-based:
1463
+ // - Created at startup (in src/index.ts)
1464
+ // - Destroyed when first pane is created (in paneCreation.ts)
1465
+ // - Recreated when last pane is closed (in paneActions.ts)
1466
+ // No polling needed!
1385
1467
  // loadPanes moved to usePanes
1386
1468
  // getPanePositions moved to utils/tmux
1387
1469
  // Navigation logic moved to hook
@@ -1389,137 +1471,1057 @@ const DmuxApp = ({ panesFile, projectName, sessionName, settingsFile, projectRoo
1389
1471
  // findCardInDirection provided by useNavigation
1390
1472
  // savePanes moved to usePanes
1391
1473
  // applySmartLayout moved to utils/tmux
1392
- const createNewPane = async (prompt, agent) => {
1393
- setIsCreatingPane(true);
1394
- setStatusMessage('Generating slug...');
1395
- const slug = await generateSlug(prompt);
1396
- setStatusMessage(\`Creating worktree: \${slug}...\`);
1397
- // Get git root directory for consistent worktree placement
1398
- 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
+ }
1399
1481
  try {
1400
- projectRoot = execSync('git rev-parse --show-toplevel', {
1401
- encoding: 'utf-8',
1402
- stdio: 'pipe'
1403
- }).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
+ }
1404
1539
  }
1405
- catch {
1406
- // Fallback to current directory if not in a git repo
1407
- projectRoot = process.cwd();
1540
+ catch (error) {
1541
+ setStatusMessage(\`Failed to launch popup: \${error.message}\`);
1542
+ setTimeout(() => setStatusMessage(""), 3000);
1408
1543
  }
1409
- // Create worktree path inside .dmux/worktrees directory
1410
- const worktreePath = path.join(projectRoot, '.dmux', 'worktrees', slug);
1411
- // Get the original pane ID (where dmux is running) before clearing
1412
- const originalPaneId = execSync('tmux display-message -p "#{pane_id}"', { encoding: 'utf-8' }).trim();
1413
- // Multiple clearing strategies to prevent artifacts
1414
- // 1. Clear screen with ANSI codes
1415
- process.stdout.write('\\x1b[2J\\x1b[H');
1416
- // 2. Fill with blank lines to push content off screen
1417
- process.stdout.write('\\n'.repeat(100));
1418
- // 3. Clear tmux history and send clear command
1419
- try {
1420
- execSync('tmux clear-history', { stdio: 'pipe' });
1421
- execSync('tmux send-keys C-l', { stdio: 'pipe' });
1544
+ };
1545
+ const launchKebabMenuPopup = async (paneIndex) => {
1546
+ // Only launch popup if tmux supports it
1547
+ if (!popupsSupported) {
1548
+ setStatusMessage("Popups require tmux 3.2+");
1549
+ setTimeout(() => setStatusMessage(""), 3000);
1550
+ return;
1422
1551
  }
1423
- catch { }
1424
- // Wait a bit for clearing to settle
1425
- await new Promise(resolve => setTimeout(resolve, 100));
1426
- // 4. Force tmux to refresh the display
1427
- try {
1428
- execSync('tmux refresh-client', { stdio: 'pipe' });
1552
+ const selectedPane = panes[paneIndex];
1553
+ if (!selectedPane) {
1554
+ return;
1429
1555
  }
1430
- catch { }
1431
- // Get current pane count to determine layout
1432
- const paneCount = parseInt(execSync('tmux list-panes | wc -l', { encoding: 'utf-8' }).trim());
1433
- // Enable pane borders to show titles
1434
1556
  try {
1435
- 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
+ }
1436
1614
  }
1437
- catch {
1438
- // 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;
1439
1626
  }
1440
- // Create new pane
1441
- const paneInfo = execSync(\`tmux split-window -h -P -F '#{pane_id}'\`, { encoding: 'utf-8' }).trim();
1442
- // Wait for pane creation to settle
1443
- await new Promise(resolve => setTimeout(resolve, 500));
1444
- // Set pane title to match the slug
1445
1627
  try {
1446
- 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
+ }
1447
1663
  }
1448
- catch {
1449
- // 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;
1450
1676
  }
1451
- // Apply smart layout based on pane count
1452
- const newPaneCount = paneCount + 1;
1453
- applySmartLayout(newPaneCount);
1454
- // Create git worktree and cd into it
1455
- // This MUST happen before launching Claude to ensure we're in the right directory
1456
1677
  try {
1457
- // First, create the worktree and cd into it as a single command
1458
- // Use ; instead of && to ensure cd runs even if worktree already exists
1459
- const worktreeCmd = \`git worktree add "\${worktreePath}" -b \${slug} 2>/dev/null ; cd "\${worktreePath}"\`;
1460
- execSync(\`tmux send-keys -t '\${paneInfo}' '\${worktreeCmd}' Enter\`, { stdio: 'pipe' });
1461
- // Wait longer for worktree creation and cd to complete
1462
- // This is critical - if we don't wait long enough, Claude will start in the wrong directory
1463
- await new Promise(resolve => setTimeout(resolve, 2500));
1464
- // Verify we're in the worktree directory by sending pwd command
1465
- execSync(\`tmux send-keys -t '\${paneInfo}' 'echo "Worktree created at:" && pwd' Enter\`, { stdio: 'pipe' });
1466
- await new Promise(resolve => setTimeout(resolve, 500));
1467
- 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
+ }
1468
1706
  }
1469
1707
  catch (error) {
1470
- // Log error but continue - worktree creation is essential
1471
- setStatusMessage(\`Warning: Worktree issue: \${error}\`);
1472
- // Even if worktree creation failed, try to cd to the directory in case it exists
1473
- execSync(\`tmux send-keys -t '\${paneInfo}' 'cd "\${worktreePath}" 2>/dev/null || (echo "ERROR: Failed to create/enter worktree \${slug}" && pwd)' Enter\`, { stdio: 'pipe' });
1474
- await new Promise(resolve => setTimeout(resolve, 1000));
1708
+ setStatusMessage(\`Failed to launch popup: \${error.message}\`);
1709
+ setTimeout(() => setStatusMessage(""), 3000);
1475
1710
  }
1476
- // Prepare and send the agent command
1477
- let escapedCmd = '';
1478
- if (agent === 'claude') {
1479
- // Claude should always be launched AFTER we're in the worktree directory
1480
- let claudeCmd;
1481
- if (prompt && prompt.trim()) {
1482
- const escapedPrompt = prompt
1483
- .replace(/\\\\/g, '\\\\\\\\')
1484
- .replace(/"/g, '\\\\"')
1485
- .replace(/\`/g, '\\\\\`')
1486
- .replace(/\\$/g, '\\\\$');
1487
- 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
+ }
1488
1782
  }
1489
- else {
1490
- 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);
1491
1795
  }
1492
- // Send Claude command to new pane
1493
- escapedCmd = claudeCmd.replace(/'/g, "'\\\\''");
1494
- execSync(\`tmux send-keys -t '\${paneInfo}' '\${escapedCmd}'\`, { stdio: 'pipe' });
1495
- execSync(\`tmux send-keys -t '\${paneInfo}' Enter\`, { stdio: 'pipe' });
1496
1796
  }
1497
- else if (agent === 'opencode') {
1498
- // opencode: start the TUI, then paste the prompt and submit
1499
- const openCoderCmd = \`opencode\`;
1500
- const escapedOpenCmd = openCoderCmd.replace(/'/g, "'\\\\''");
1501
- execSync(\`tmux send-keys -t '\${paneInfo}' '\${escapedOpenCmd}'\`, { stdio: 'pipe' });
1502
- execSync(\`tmux send-keys -t '\${paneInfo}' Enter\`, { stdio: 'pipe' });
1503
- if (prompt && prompt.trim()) {
1504
- await new Promise(resolve => setTimeout(resolve, 1500));
1505
- const bufName = \`dmux_prompt_\${Date.now()}\`;
1506
- const promptEsc = prompt.replace(/\\\\/g, '\\\\\\\\').replace(/'/g, "'\\\\''");
1507
- execSync(\`tmux set-buffer -b '\${bufName}' -- '\${promptEsc}'\`, { stdio: 'pipe' });
1508
- execSync(\`tmux paste-buffer -b '\${bufName}' -t '\${paneInfo}'\`, { stdio: 'pipe' });
1509
- await new Promise(resolve => setTimeout(resolve, 200));
1510
- execSync(\`tmux delete-buffer -b '\${bufName}'\`, { stdio: 'pipe' });
1511
- 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();
1512
1846
  }
1513
1847
  }
1514
- if (agent === 'claude') {
1515
- // Monitor for Claude Code trust prompt and auto-respond
1516
- const autoApproveTrust = async () => {
1517
- // Wait for Claude to start up before checking for prompts
1518
- await new Promise(resolve => setTimeout(resolve, 800));
1519
- const maxChecks = 100; // 100 checks * 100ms = 10 seconds total
1520
- const checkInterval = 100; // Check every 100ms
1521
- let lastContent = '';
1522
- let stableContentCount = 0;
1848
+ catch (error) {
1849
+ setStatusMessage(\`Failed to launch popup: \${error.message}\`);
1850
+ setTimeout(() => setStatusMessage(""), 3000);
1851
+ }
1852
+ };
1853
+ const launchShortcutsPopup = async () => {
1854
+ // Only launch popup if tmux supports it
1855
+ if (!popupsSupported) {
1856
+ setStatusMessage("Popups require tmux 3.2+");
1857
+ setTimeout(() => setStatusMessage(""), 3000);
1858
+ return;
1859
+ }
1860
+ try {
1861
+ // Resolve the popup script path
1862
+ const projectRootForPopup = __dirname.includes("/dist")
1863
+ ? path.resolve(__dirname, "..") // If in dist/, go up one level
1864
+ : path.resolve(__dirname, ".."); // If in src/, go up one level
1865
+ const popupScriptPath = path.join(projectRootForPopup, "dist", "popups", "shortcutsPopup.js");
1866
+ // Prepare data for shortcuts popup
1867
+ const shortcutsData = {
1868
+ hasSidebarLayout: !!controlPaneId,
1869
+ showRemoteKey: !!server,
1870
+ };
1871
+ // Write data to temp file
1872
+ const dataFile = \`/tmp/dmux-shortcuts-\${Date.now()}.json\`;
1873
+ const dataJson = JSON.stringify(shortcutsData);
1874
+ await fs.writeFile(dataFile, dataJson);
1875
+ // Launch the popup non-blocking and track it
1876
+ const popupHandle = launchNodePopupNonBlocking(popupScriptPath, [dataFile], {
1877
+ ...POPUP_POSITIONING.standard(SIDEBAR_WIDTH),
1878
+ width: 50,
1879
+ height: 20,
1880
+ title: "⌨️ Keyboard Shortcuts",
1881
+ });
1882
+ // Wait for the popup to close
1883
+ const result = await popupHandle.resultPromise;
1884
+ // Clear active popup tracking
1885
+ // Clean up temp file
1886
+ try {
1887
+ await fs.unlink(dataFile);
1888
+ }
1889
+ catch (err) {
1890
+ // Ignore cleanup errors
1891
+ }
1892
+ }
1893
+ catch (error) {
1894
+ setStatusMessage(\`Failed to launch popup: \${error.message}\`);
1895
+ setTimeout(() => setStatusMessage(""), 3000);
1896
+ }
1897
+ };
1898
+ const launchRemotePopup = async () => {
1899
+ // Only launch popup if tmux supports it
1900
+ if (!popupsSupported) {
1901
+ setStatusMessage("Popups require tmux 3.2+");
1902
+ setTimeout(() => setStatusMessage(""), 3000);
1903
+ return;
1904
+ }
1905
+ // Check if server is available and tunnel URL exists
1906
+ if (!server || !serverPort || !tunnelUrl) {
1907
+ setStatusMessage("Tunnel not ready");
1908
+ setTimeout(() => setStatusMessage(""), 3000);
1909
+ return;
1910
+ }
1911
+ try {
1912
+ // Resolve the popup script path
1913
+ const projectRootForPopup = __dirname.includes("/dist")
1914
+ ? path.resolve(__dirname, "..") // If in dist/, go up one level
1915
+ : path.resolve(__dirname, ".."); // If in src/, go up one level
1916
+ const popupScriptPath = path.join(projectRootForPopup, "dist", "popups", "remotePopup.js");
1917
+ // Prepare status file with existing tunnel URL
1918
+ const tunnelStatusFile = \`/tmp/dmux-tunnel-status-\${Date.now()}.json\`;
1919
+ await fs.writeFile(tunnelStatusFile, JSON.stringify({ url: tunnelUrl }));
1920
+ // Prepare data for remote popup
1921
+ const remoteData = {
1922
+ loading: false,
1923
+ serverPort: serverPort,
1924
+ statusFile: tunnelStatusFile,
1925
+ };
1926
+ // Write data to temp file
1927
+ const dataFile = \`/tmp/dmux-remote-\${Date.now()}.json\`;
1928
+ await fs.writeFile(dataFile, JSON.stringify(remoteData));
1929
+ // Launch the popup non-blocking and track it
1930
+ const popupHandle = launchNodePopupNonBlocking(popupScriptPath, [dataFile], {
1931
+ ...POPUP_POSITIONING.centeredWithSidebar(SIDEBAR_WIDTH),
1932
+ width: 60,
1933
+ height: 30,
1934
+ title: "🌐 Remote Access",
1935
+ });
1936
+ // Wait for the popup to close
1937
+ const result = await popupHandle.resultPromise;
1938
+ // Clear active popup tracking
1939
+ // Show "Copied!" message if URL was copied
1940
+ // Note: result is the parsed JSON directly, not wrapped in PopupResult.data
1941
+ if (result && result.copied) {
1942
+ setTunnelCopied(true);
1943
+ setTimeout(() => setTunnelCopied(false), 2000);
1944
+ }
1945
+ // Clean up temp files
1946
+ try {
1947
+ await fs.unlink(dataFile);
1948
+ }
1949
+ catch (err) {
1950
+ // Ignore cleanup errors
1951
+ }
1952
+ try {
1953
+ await fs.unlink(tunnelStatusFile);
1954
+ }
1955
+ catch (err) {
1956
+ // Ignore cleanup errors (file might not exist yet)
1957
+ }
1958
+ }
1959
+ catch (error) {
1960
+ setStatusMessage(\`Failed to launch popup: \${error.message}\`);
1961
+ setTimeout(() => setStatusMessage(""), 3000);
1962
+ }
1963
+ };
1964
+ const launchSettingsPopup = async () => {
1965
+ // Only launch popup if tmux supports it
1966
+ if (!popupsSupported) {
1967
+ setStatusMessage("Popups require tmux 3.2+");
1968
+ setTimeout(() => setStatusMessage(""), 3000);
1969
+ return;
1970
+ }
1971
+ try {
1972
+ // Resolve the popup script path
1973
+ const projectRootForPopup = __dirname.includes("/dist")
1974
+ ? path.resolve(__dirname, "..") // If in dist/, go up one level
1975
+ : path.resolve(__dirname, ".."); // If in src/, go up one level
1976
+ const popupScriptPath = path.join(projectRootForPopup, "dist", "popups", "settingsPopup.js");
1977
+ // Prepare settings data for popup
1978
+ const settingsData = {
1979
+ settingDefinitions: SETTING_DEFINITIONS,
1980
+ settings: settingsManager.getSettings(),
1981
+ globalSettings: settingsManager.getGlobalSettings(),
1982
+ projectSettings: settingsManager.getProjectSettings(),
1983
+ };
1984
+ const settingsJson = JSON.stringify(settingsData);
1985
+ // Launch the popup non-blocking and track it
1986
+ const popupHandle = launchNodePopupNonBlocking(popupScriptPath, [settingsJson], {
1987
+ ...POPUP_POSITIONING.standard(SIDEBAR_WIDTH),
1988
+ width: 70,
1989
+ height: Math.min(25, SETTING_DEFINITIONS.length + 8),
1990
+ title: "⚙️ Settings",
1991
+ });
1992
+ // Wait for the popup to close
1993
+ const result = await popupHandle.resultPromise;
1994
+ // Clear active popup tracking
1995
+ if (result.success) {
1996
+ // Check if this is an action result (action field at top level)
1997
+ if (result.action) {
1998
+ // Action type setting (like 'hooks')
1999
+ if (result.action === "hooks") {
2000
+ // Launch hooks popup
2001
+ await launchHooksPopup();
2002
+ }
2003
+ }
2004
+ else if (result.data) {
2005
+ // Regular setting change (result.data contains the setting)
2006
+ const { key, value, scope } = result.data;
2007
+ settingsManager.updateSetting(key, value, scope);
2008
+ setStatusMessage(\`Setting saved (\${scope})\`);
2009
+ setTimeout(() => setStatusMessage(""), 2000);
2010
+ }
2011
+ }
2012
+ else if (result.cancelled) {
2013
+ // User pressed ESC - do nothing
2014
+ return;
2015
+ }
2016
+ else if (result.error) {
2017
+ setStatusMessage(\`Popup error: \${result.error}\`);
2018
+ setTimeout(() => setStatusMessage(""), 3000);
2019
+ }
2020
+ }
2021
+ catch (error) {
2022
+ setStatusMessage(\`Failed to launch popup: \${error.message}\`);
2023
+ setTimeout(() => setStatusMessage(""), 3000);
2024
+ }
2025
+ };
2026
+ const launchMergePopup = async (pane) => {
2027
+ LogService.getInstance().debug(\`launchMergePopup called for pane: \${pane.slug}\`, "MergePopup");
2028
+ // Only launch popup if tmux supports it
2029
+ if (!popupsSupported) {
2030
+ LogService.getInstance().warn("Popups not supported", "MergePopup");
2031
+ setStatusMessage("Popups require tmux 3.2+");
2032
+ setTimeout(() => setStatusMessage(""), 3000);
2033
+ return;
2034
+ }
2035
+ if (!pane.worktreePath) {
2036
+ LogService.getInstance().warn("No worktree path for pane", "MergePopup", pane.id);
2037
+ setStatusMessage("This pane has no worktree to merge");
2038
+ setTimeout(() => setStatusMessage(""), 3000);
2039
+ return;
2040
+ }
2041
+ try {
2042
+ // Resolve the popup script path
2043
+ const projectRootForPopup = __dirname.includes("/dist")
2044
+ ? path.resolve(__dirname, "..") // If in dist/, go up one level
2045
+ : path.resolve(__dirname, ".."); // If in src/, go up one level
2046
+ const popupScriptPath = path.join(projectRootForPopup, "dist", "popups", "mergePopup.js");
2047
+ LogService.getInstance().debug(\`Popup script path: \${popupScriptPath}\`, "MergePopup");
2048
+ // Check if popup script exists
2049
+ try {
2050
+ await fs.access(popupScriptPath);
2051
+ LogService.getInstance().debug("Popup script exists", "MergePopup");
2052
+ }
2053
+ catch {
2054
+ LogService.getInstance().error(\`Popup script NOT found at: \${popupScriptPath}\`, "MergePopup");
2055
+ setStatusMessage(\`Merge popup script not found: \${popupScriptPath}\`);
2056
+ setTimeout(() => setStatusMessage(""), 5000);
2057
+ return;
2058
+ }
2059
+ // Prepare merge data
2060
+ const mainRepoPath = pane.worktreePath.replace(/\\/\\.dmux\\/worktrees\\/[^/]+$/, "");
2061
+ const mergeData = {
2062
+ paneSlug: pane.slug,
2063
+ worktreePath: pane.worktreePath,
2064
+ mainRepoPath,
2065
+ mainBranch: getMainBranch(),
2066
+ };
2067
+ // Write data to temp file
2068
+ const dataFile = \`/tmp/dmux-merge-\${Date.now()}.json\`;
2069
+ await fs.writeFile(dataFile, JSON.stringify(mergeData));
2070
+ LogService.getInstance().debug(\`Merge data written to: \${dataFile}\`, "MergePopup");
2071
+ LogService.getInstance().debug(\`Merge data: \${JSON.stringify(mergeData)}\`, "MergePopup");
2072
+ // Launch the popup non-blocking and track it
2073
+ LogService.getInstance().debug("Launching merge popup...", "MergePopup");
2074
+ const popupHandle = launchNodePopupNonBlocking(popupScriptPath, [dataFile], {
2075
+ ...POPUP_POSITIONING.large(SIDEBAR_WIDTH, terminalWidth, terminalHeight),
2076
+ width: 80,
2077
+ height: 30,
2078
+ title: \`🔀 Merge: \${pane.slug}\`,
2079
+ });
2080
+ LogService.getInstance().debug(\`Popup launched, PID: \${popupHandle.pid}\`, "MergePopup");
2081
+ // Wait for the popup to close
2082
+ const result = await popupHandle.resultPromise;
2083
+ // Clear active popup tracking
2084
+ // Clean up temp file
2085
+ try {
2086
+ await fs.unlink(dataFile);
2087
+ }
2088
+ catch { }
2089
+ if (result.success && result.data?.merged) {
2090
+ if (result.data.closedPane) {
2091
+ // Pane was closed during merge, refresh pane list
2092
+ await loadPanes();
2093
+ setStatusMessage("Merge complete, pane closed");
2094
+ }
2095
+ else {
2096
+ setStatusMessage("Merge complete");
2097
+ }
2098
+ setTimeout(() => setStatusMessage(""), 3000);
2099
+ }
2100
+ else if (result.cancelled) {
2101
+ // User cancelled
2102
+ return;
2103
+ }
2104
+ else if (result.error) {
2105
+ setStatusMessage(\`Merge error: \${result.error}\`);
2106
+ setTimeout(() => setStatusMessage(""), 3000);
2107
+ }
2108
+ }
2109
+ catch (error) {
2110
+ setStatusMessage(\`Failed to launch merge popup: \${error.message}\`);
2111
+ setTimeout(() => setStatusMessage(""), 3000);
2112
+ }
2113
+ };
2114
+ const launchChoicePopup = async (title, message, options) => {
2115
+ // Only launch popup if tmux supports it
2116
+ if (!popupsSupported) {
2117
+ setStatusMessage("Popups require tmux 3.2+");
2118
+ setTimeout(() => setStatusMessage(""), 3000);
2119
+ return null;
2120
+ }
2121
+ try {
2122
+ // Resolve the popup script path
2123
+ const projectRootForPopup = __dirname.includes("/dist")
2124
+ ? path.resolve(__dirname, "..") // If in dist/, go up one level
2125
+ : path.resolve(__dirname, ".."); // If in src/, go up one level
2126
+ const popupScriptPath = path.join(projectRootForPopup, "dist", "popups", "choicePopup.js");
2127
+ // Write data to temp file to avoid shell escaping issues
2128
+ const dataFile = \`/tmp/dmux-choice-\${Date.now()}.json\`;
2129
+ const dataJson = JSON.stringify({ title, message, options });
2130
+ await fs.writeFile(dataFile, dataJson);
2131
+ // Launch the popup non-blocking and track it
2132
+ const popupHandle = launchNodePopupNonBlocking(popupScriptPath, [dataFile], {
2133
+ ...POPUP_POSITIONING.standard(SIDEBAR_WIDTH),
2134
+ width: 70,
2135
+ height: Math.min(25, options.length * 3 + 8),
2136
+ title: title || "Choose Option",
2137
+ });
2138
+ // Wait for the popup to close
2139
+ const result = await popupHandle.resultPromise;
2140
+ // Clear active popup tracking
2141
+ // Clean up temp file
2142
+ try {
2143
+ await fs.unlink(dataFile);
2144
+ }
2145
+ catch { }
2146
+ if (result.success && result.data) {
2147
+ return result.data;
2148
+ }
2149
+ else if (result.cancelled) {
2150
+ return null;
2151
+ }
2152
+ else if (result.error) {
2153
+ setStatusMessage(\`Popup error: \${result.error}\`);
2154
+ setTimeout(() => setStatusMessage(""), 3000);
2155
+ return null;
2156
+ }
2157
+ }
2158
+ catch (error) {
2159
+ setStatusMessage(\`Failed to launch popup: \${error.message}\`);
2160
+ setTimeout(() => setStatusMessage(""), 3000);
2161
+ }
2162
+ return null;
2163
+ };
2164
+ const launchInputPopup = async (title, message, placeholder, defaultValue) => {
2165
+ // Only launch popup if tmux supports it
2166
+ if (!popupsSupported) {
2167
+ setStatusMessage("Popups require tmux 3.2+");
2168
+ setTimeout(() => setStatusMessage(""), 3000);
2169
+ return null;
2170
+ }
2171
+ try {
2172
+ // Resolve the popup script path
2173
+ const projectRootForPopup = __dirname.includes("/dist")
2174
+ ? path.resolve(__dirname, "..") // If in dist/, go up one level
2175
+ : path.resolve(__dirname, ".."); // If in src/, go up one level
2176
+ const popupScriptPath = path.join(projectRootForPopup, "dist", "popups", "inputPopup.js");
2177
+ // Write data to temp file to avoid shell escaping issues
2178
+ const dataFile = \`/tmp/dmux-input-\${Date.now()}.json\`;
2179
+ const dataJson = JSON.stringify({
2180
+ title,
2181
+ message,
2182
+ placeholder,
2183
+ defaultValue,
2184
+ });
2185
+ await fs.writeFile(dataFile, dataJson);
2186
+ // Launch the popup non-blocking and track it
2187
+ const popupHandle = launchNodePopupNonBlocking(popupScriptPath, [dataFile], {
2188
+ ...POPUP_POSITIONING.standard(SIDEBAR_WIDTH),
2189
+ width: 70,
2190
+ height: 15,
2191
+ title: title || "Input",
2192
+ });
2193
+ // Wait for the popup to close
2194
+ const result = await popupHandle.resultPromise;
2195
+ // Clear active popup tracking
2196
+ // Clean up temp file
2197
+ try {
2198
+ await fs.unlink(dataFile);
2199
+ }
2200
+ catch { }
2201
+ if (result.success && result.data !== undefined) {
2202
+ return result.data;
2203
+ }
2204
+ else if (result.cancelled) {
2205
+ return null;
2206
+ }
2207
+ else if (result.error) {
2208
+ setStatusMessage(\`Popup error: \${result.error}\`);
2209
+ setTimeout(() => setStatusMessage(""), 3000);
2210
+ return null;
2211
+ }
2212
+ }
2213
+ catch (error) {
2214
+ setStatusMessage(\`Failed to launch popup: \${error.message}\`);
2215
+ setTimeout(() => setStatusMessage(""), 3000);
2216
+ }
2217
+ return null;
2218
+ };
2219
+ const launchProgressPopup = async (message, type = "info", timeout = 2000) => {
2220
+ // Only launch popup if tmux supports it
2221
+ if (!popupsSupported) {
2222
+ setStatusMessage(message);
2223
+ setTimeout(() => setStatusMessage(""), timeout);
2224
+ return;
2225
+ }
2226
+ try {
2227
+ // Resolve the popup script path
2228
+ const projectRootForPopup = __dirname.includes("/dist")
2229
+ ? path.resolve(__dirname, "..") // If in dist/, go up one level
2230
+ : path.resolve(__dirname, ".."); // If in src/, go up one level
2231
+ const popupScriptPath = path.join(projectRootForPopup, "dist", "popups", "progressPopup.js");
2232
+ // Write data to temp file to avoid shell escaping issues
2233
+ const dataFile = \`/tmp/dmux-progress-\${Date.now()}.json\`;
2234
+ const dataJson = JSON.stringify({ message, type, timeout });
2235
+ await fs.writeFile(dataFile, dataJson);
2236
+ // Launch the popup - position at top, 1 char right of sidebar
2237
+ // Height depends on message length
2238
+ const lines = Math.ceil(message.length / 60) + 3; // Estimate lines needed
2239
+ const titleText = type === "success"
2240
+ ? "✓ Success"
2241
+ : type === "error"
2242
+ ? "✗ Error"
2243
+ : type === "info"
2244
+ ? "ℹ Info"
2245
+ : "Progress";
2246
+ // Launch the popup non-blocking and track it
2247
+ const popupHandle = launchNodePopupNonBlocking(popupScriptPath, [dataFile], {
2248
+ ...POPUP_POSITIONING.standard(SIDEBAR_WIDTH),
2249
+ width: 70,
2250
+ height: Math.min(15, lines + 4),
2251
+ title: titleText,
2252
+ });
2253
+ // Wait for the popup to close
2254
+ await popupHandle.resultPromise;
2255
+ // Clear active popup tracking
2256
+ // Clean up temp file
2257
+ try {
2258
+ await fs.unlink(dataFile);
2259
+ }
2260
+ catch { }
2261
+ }
2262
+ catch (error) {
2263
+ // Fallback to inline message
2264
+ setStatusMessage(message);
2265
+ setTimeout(() => setStatusMessage(""), timeout);
2266
+ }
2267
+ };
2268
+ // Action system - initialized after popup launchers are defined
2269
+ const actionSystem = useActionSystem({
2270
+ panes,
2271
+ savePanes,
2272
+ sessionName,
2273
+ projectName,
2274
+ onPaneRemove: (paneId) => {
2275
+ // Mark the pane as intentionally closed to prevent race condition with worker
2276
+ intentionallyClosedPanes.current.add(paneId);
2277
+ // Remove from panes list
2278
+ const updatedPanes = panes.filter((p) => p.paneId !== paneId);
2279
+ savePanes(updatedPanes);
2280
+ // Clean up after a delay
2281
+ setTimeout(() => {
2282
+ intentionallyClosedPanes.current.delete(paneId);
2283
+ }, 5000);
2284
+ },
2285
+ forceRepaint,
2286
+ popupLaunchers: popupsSupported
2287
+ ? {
2288
+ launchConfirmPopup,
2289
+ launchChoicePopup,
2290
+ launchInputPopup,
2291
+ launchProgressPopup,
2292
+ }
2293
+ : undefined,
2294
+ });
2295
+ // Auto-show new pane dialog removed - users can press 'n' to create panes via popup
2296
+ // Periodic enforcement of control pane size and content pane rebalancing (left sidebar at 40 chars)
2297
+ useEffect(() => {
2298
+ if (!controlPaneId) {
2299
+ return; // No sidebar layout configured
2300
+ }
2301
+ // Enforce sidebar width immediately on mount
2302
+ enforceControlPaneSize(controlPaneId, SIDEBAR_WIDTH);
2303
+ // Debounce resize handler to prevent infinite loops
2304
+ let resizeTimeout = null;
2305
+ let isApplyingLayout = false;
2306
+ const handleResize = () => {
2307
+ // Skip if we're already applying a layout (prevents loops)
2308
+ if (isApplyingLayout) {
2309
+ return;
2310
+ }
2311
+ // Clear any pending resize
2312
+ if (resizeTimeout) {
2313
+ clearTimeout(resizeTimeout);
2314
+ }
2315
+ // Debounce: wait 500ms after last resize event (prevents excessive recalculations)
2316
+ resizeTimeout = setTimeout(() => {
2317
+ // Only enforce if not showing dialogs (to avoid interference)
2318
+ const hasActiveDialog = actionSystem.actionState.showConfirmDialog ||
2319
+ actionSystem.actionState.showChoiceDialog ||
2320
+ actionSystem.actionState.showInputDialog ||
2321
+ actionSystem.actionState.showProgressDialog ||
2322
+ !!showCommandPrompt ||
2323
+ showFileCopyPrompt ||
2324
+ isCreatingPane ||
2325
+ runningCommand ||
2326
+ isUpdating;
2327
+ if (!hasActiveDialog) {
2328
+ isApplyingLayout = true;
2329
+ // Only enforce sidebar width when terminal resizes
2330
+ enforceControlPaneSize(controlPaneId, SIDEBAR_WIDTH);
2331
+ // Force Ink to repaint after resize to prevent blank dmux pane
2332
+ forceRepaint();
2333
+ // Reset flag after a brief delay
2334
+ setTimeout(() => {
2335
+ isApplyingLayout = false;
2336
+ }, 100);
2337
+ }
2338
+ }, 500);
2339
+ };
2340
+ // Listen to stdout resize events
2341
+ process.stdout.on("resize", handleResize);
2342
+ // Also listen for SIGWINCH and SIGUSR1 (tmux hook sends USR1)
2343
+ process.on("SIGWINCH", handleResize);
2344
+ process.on("SIGUSR1", handleResize);
2345
+ return () => {
2346
+ process.stdout.off("resize", handleResize);
2347
+ process.off("SIGWINCH", handleResize);
2348
+ process.off("SIGUSR1", handleResize);
2349
+ if (resizeTimeout) {
2350
+ clearTimeout(resizeTimeout);
2351
+ }
2352
+ };
2353
+ }, [
2354
+ controlPaneId,
2355
+ actionSystem.actionState.showConfirmDialog,
2356
+ actionSystem.actionState.showChoiceDialog,
2357
+ actionSystem.actionState.showInputDialog,
2358
+ actionSystem.actionState.showProgressDialog,
2359
+ showCommandPrompt,
2360
+ showFileCopyPrompt,
2361
+ isCreatingPane,
2362
+ runningCommand,
2363
+ isUpdating,
2364
+ ]);
2365
+ // Monitor agent status across panes (returns a map of pane ID to status)
2366
+ const agentStatuses = useAgentStatus({
2367
+ panes,
2368
+ suspend: actionSystem.actionState.showConfirmDialog ||
2369
+ actionSystem.actionState.showChoiceDialog ||
2370
+ actionSystem.actionState.showInputDialog ||
2371
+ actionSystem.actionState.showProgressDialog ||
2372
+ !!showCommandPrompt ||
2373
+ showFileCopyPrompt,
2374
+ onPaneRemoved: (paneId) => {
2375
+ // Check if this pane was intentionally closed
2376
+ // If so, don't re-save - the close action already handled it
2377
+ if (intentionallyClosedPanes.current.has(paneId)) {
2378
+ return;
2379
+ }
2380
+ // Pane was removed unexpectedly (e.g., user killed tmux pane manually)
2381
+ // Remove it from our tracking
2382
+ const updatedPanes = panes.filter((p) => p.id !== paneId);
2383
+ savePanes(updatedPanes);
2384
+ },
2385
+ });
2386
+ const createNewPane = async (prompt, agent) => {
2387
+ setIsCreatingPane(true);
2388
+ setStatusMessage("Generating slug...");
2389
+ const slug = await generateSlug(prompt);
2390
+ setStatusMessage(\`Creating worktree: \${slug}...\`);
2391
+ // Get git root directory for consistent worktree placement
2392
+ let projectRoot;
2393
+ try {
2394
+ projectRoot = execSync("git rev-parse --show-toplevel", {
2395
+ encoding: "utf-8",
2396
+ stdio: "pipe",
2397
+ }).trim();
2398
+ }
2399
+ catch {
2400
+ // Fallback to current directory if not in a git repo
2401
+ projectRoot = process.cwd();
2402
+ }
2403
+ // Create worktree path inside .dmux/worktrees directory
2404
+ const worktreePath = path.join(projectRoot, ".dmux", "worktrees", slug);
2405
+ // Get the original pane ID (where dmux is running) before clearing
2406
+ const originalPaneId = execSync('tmux display-message -p "#{pane_id}"', {
2407
+ encoding: "utf-8",
2408
+ }).trim();
2409
+ // Minimal clearing to avoid layout shifts
2410
+ process.stdout.write("\\x1b[2J\\x1b[H");
2411
+ // Get current pane count to determine layout
2412
+ const paneCount = parseInt(execSync("tmux list-panes | wc -l", { encoding: "utf-8" }).trim());
2413
+ // Enable pane borders to show titles
2414
+ try {
2415
+ execSync(\`tmux set-option -g pane-border-status top\`, { stdio: "pipe" });
2416
+ }
2417
+ catch {
2418
+ // Ignore if already set or fails
2419
+ }
2420
+ // Create new pane
2421
+ const paneInfo = execSync(\`tmux split-window -h -P -F '#{pane_id}'\`, {
2422
+ encoding: "utf-8",
2423
+ }).trim();
2424
+ // Wait for pane creation to settle
2425
+ await new Promise((resolve) => setTimeout(resolve, 500));
2426
+ // Set pane title to match the slug
2427
+ try {
2428
+ execSync(\`tmux select-pane -t '\${paneInfo}' -T "\${slug}"\`, {
2429
+ stdio: "pipe",
2430
+ });
2431
+ }
2432
+ catch {
2433
+ // Ignore if setting title fails
2434
+ }
2435
+ // Don't apply global layouts - let content panes arrange naturally
2436
+ // Only enforce sidebar width
2437
+ try {
2438
+ const controlPaneId = execSync('tmux display-message -p "#{pane_id}"', {
2439
+ encoding: "utf-8",
2440
+ }).trim();
2441
+ enforceControlPaneSize(controlPaneId, SIDEBAR_WIDTH);
2442
+ }
2443
+ catch { }
2444
+ // Create git worktree and cd into it
2445
+ // This MUST happen before launching Claude to ensure we're in the right directory
2446
+ try {
2447
+ // First, create the worktree and cd into it as a single command
2448
+ // Use ; instead of && to ensure cd runs even if worktree already exists
2449
+ const worktreeCmd = \`git worktree add "\${worktreePath}" -b \${slug} 2>/dev/null ; cd "\${worktreePath}"\`;
2450
+ execSync(\`tmux send-keys -t '\${paneInfo}' '\${worktreeCmd}' Enter\`, {
2451
+ stdio: "pipe",
2452
+ });
2453
+ // Wait longer for worktree creation and cd to complete
2454
+ // This is critical - if we don't wait long enough, Claude will start in the wrong directory
2455
+ await new Promise((resolve) => setTimeout(resolve, 2500));
2456
+ // Verify we're in the worktree directory by sending pwd command
2457
+ execSync(\`tmux send-keys -t '\${paneInfo}' 'echo "Worktree created at:" && pwd' Enter\`, { stdio: "pipe" });
2458
+ await new Promise((resolve) => setTimeout(resolve, 500));
2459
+ setStatusMessage(agent
2460
+ ? \`Worktree created, launching \${agent === "opencode" ? "opencode" : "Claude"}...\`
2461
+ : "Worktree created.");
2462
+ }
2463
+ catch (error) {
2464
+ // Log error but continue - worktree creation is essential
2465
+ setStatusMessage(\`Warning: Worktree issue: \${error}\`);
2466
+ // Even if worktree creation failed, try to cd to the directory in case it exists
2467
+ execSync(\`tmux send-keys -t '\${paneInfo}' 'cd "\${worktreePath}" 2>/dev/null || (echo "ERROR: Failed to create/enter worktree \${slug}" && pwd)' Enter\`, { stdio: "pipe" });
2468
+ await new Promise((resolve) => setTimeout(resolve, 1000));
2469
+ }
2470
+ // Prepare and send the agent command
2471
+ let escapedCmd = "";
2472
+ if (agent === "claude") {
2473
+ // Claude should always be launched AFTER we're in the worktree directory
2474
+ let claudeCmd;
2475
+ if (prompt && prompt.trim()) {
2476
+ const escapedPrompt = prompt
2477
+ .replace(/\\\\/g, "\\\\\\\\")
2478
+ .replace(/"/g, '\\\\"')
2479
+ .replace(/\`/g, "\\\\\`")
2480
+ .replace(/\\$/g, "\\\\$");
2481
+ claudeCmd = \`claude "\${escapedPrompt}" --permission-mode=acceptEdits\`;
2482
+ }
2483
+ else {
2484
+ claudeCmd = \`claude --permission-mode=acceptEdits\`;
2485
+ }
2486
+ // Send Claude command to new pane
2487
+ escapedCmd = claudeCmd.replace(/'/g, "'\\\\''");
2488
+ execSync(\`tmux send-keys -t '\${paneInfo}' '\${escapedCmd}'\`, {
2489
+ stdio: "pipe",
2490
+ });
2491
+ execSync(\`tmux send-keys -t '\${paneInfo}' Enter\`, { stdio: "pipe" });
2492
+ }
2493
+ else if (agent === "opencode") {
2494
+ // opencode: start the TUI, then paste the prompt and submit
2495
+ const openCoderCmd = \`opencode\`;
2496
+ const escapedOpenCmd = openCoderCmd.replace(/'/g, "'\\\\''");
2497
+ execSync(\`tmux send-keys -t '\${paneInfo}' '\${escapedOpenCmd}'\`, {
2498
+ stdio: "pipe",
2499
+ });
2500
+ execSync(\`tmux send-keys -t '\${paneInfo}' Enter\`, { stdio: "pipe" });
2501
+ if (prompt && prompt.trim()) {
2502
+ await new Promise((resolve) => setTimeout(resolve, 1500));
2503
+ const bufName = \`dmux_prompt_\${Date.now()}\`;
2504
+ const promptEsc = prompt.replace(/\\\\/g, "\\\\\\\\").replace(/'/g, "'\\\\''");
2505
+ execSync(\`tmux set-buffer -b '\${bufName}' -- '\${promptEsc}'\`, {
2506
+ stdio: "pipe",
2507
+ });
2508
+ execSync(\`tmux paste-buffer -b '\${bufName}' -t '\${paneInfo}'\`, {
2509
+ stdio: "pipe",
2510
+ });
2511
+ await new Promise((resolve) => setTimeout(resolve, 200));
2512
+ execSync(\`tmux delete-buffer -b '\${bufName}'\`, { stdio: "pipe" });
2513
+ execSync(\`tmux send-keys -t '\${paneInfo}' Enter\`, { stdio: "pipe" });
2514
+ }
2515
+ }
2516
+ if (agent === "claude") {
2517
+ // Monitor for Claude Code trust prompt and auto-respond
2518
+ const autoApproveTrust = async () => {
2519
+ // Wait for Claude to start up before checking for prompts
2520
+ await new Promise((resolve) => setTimeout(resolve, 800));
2521
+ const maxChecks = 100; // 100 checks * 100ms = 10 seconds total
2522
+ const checkInterval = 100; // Check every 100ms
2523
+ let lastContent = "";
2524
+ let stableContentCount = 0;
1523
2525
  let promptHandled = false;
1524
2526
  // More comprehensive trust prompt patterns
1525
2527
  const trustPromptPatterns = [
@@ -1547,14 +2549,15 @@ const DmuxApp = ({ panesFile, projectName, sessionName, settingsFile, projectRoo
1547
2549
  /❯\\s*1\\.\\s*Yes,\\s*proceed/i, // New Claude numbered menu format
1548
2550
  /Enter to confirm.*Esc to exit/i, // New Claude confirmation format
1549
2551
  /1\\.\\s*Yes,\\s*proceed/i, // Yes proceed option
1550
- /2\\.\\s*No,\\s*exit/i // No exit option
2552
+ /2\\.\\s*No,\\s*exit/i, // No exit option
1551
2553
  ];
1552
2554
  for (let i = 0; i < maxChecks; i++) {
1553
- await new Promise(resolve => setTimeout(resolve, checkInterval));
2555
+ await new Promise((resolve) => setTimeout(resolve, checkInterval));
1554
2556
  try {
1555
2557
  // Capture the pane content
1556
2558
  const paneContent = capturePaneContent(paneInfo, 30);
1557
- if (i % 10 === 0) { // Log every 10 checks (every second)
2559
+ if (i % 10 === 0) {
2560
+ // Log every 10 checks (every second)
1558
2561
  }
1559
2562
  // Check if content has stabilized (same for 3 checks = prompt is waiting)
1560
2563
  if (paneContent === lastContent) {
@@ -1565,14 +2568,15 @@ const DmuxApp = ({ panesFile, projectName, sessionName, settingsFile, projectRoo
1565
2568
  lastContent = paneContent;
1566
2569
  }
1567
2570
  // Look for trust prompt in the current content
1568
- const hasTrustPrompt = trustPromptPatterns.some(pattern => pattern.test(paneContent));
2571
+ const hasTrustPrompt = trustPromptPatterns.some((pattern) => pattern.test(paneContent));
1569
2572
  // Also check if we see specific Claude permission text
1570
- const hasClaudePermissionPrompt = paneContent.includes('Do you trust') ||
1571
- paneContent.includes('trust the files') ||
1572
- paneContent.includes('permission') ||
1573
- paneContent.includes('allow') ||
1574
- (paneContent.includes('folder') && paneContent.includes('?'));
1575
- 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) {
1576
2580
  // Content is stable and we found a prompt
1577
2581
  if (stableContentCount >= 2) {
1578
2582
  // Check if this is the new Claude numbered menu format
@@ -1580,44 +2584,57 @@ const DmuxApp = ({ panesFile, projectName, sessionName, settingsFile, projectRoo
1580
2584
  /Enter to confirm.*Esc to exit/i.test(paneContent);
1581
2585
  if (isNewClaudeFormat) {
1582
2586
  // For new Claude format, just press Enter to confirm default "Yes, proceed"
1583
- execSync(\`tmux send-keys -t '\${paneInfo}' Enter\`, { stdio: 'pipe' });
2587
+ execSync(\`tmux send-keys -t '\${paneInfo}' Enter\`, {
2588
+ stdio: "pipe",
2589
+ });
1584
2590
  }
1585
2591
  else {
1586
2592
  // Try multiple response methods for older formats
1587
2593
  // Method 1: Send 'y' followed by Enter (most explicit)
1588
- execSync(\`tmux send-keys -t '\${paneInfo}' 'y'\`, { stdio: 'pipe' });
1589
- await new Promise(resolve => setTimeout(resolve, 50));
1590
- 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
+ });
1591
2601
  // Method 2: Just Enter (if it's a yes/no with default yes)
1592
- await new Promise(resolve => setTimeout(resolve, 100));
1593
- execSync(\`tmux send-keys -t '\${paneInfo}' Enter\`, { stdio: 'pipe' });
2602
+ await new Promise((resolve) => setTimeout(resolve, 100));
2603
+ execSync(\`tmux send-keys -t '\${paneInfo}' Enter\`, {
2604
+ stdio: "pipe",
2605
+ });
1594
2606
  }
1595
2607
  // Mark as handled to avoid duplicate responses
1596
2608
  promptHandled = true;
1597
2609
  // Wait and check if prompt is gone
1598
- await new Promise(resolve => setTimeout(resolve, 500));
2610
+ await new Promise((resolve) => setTimeout(resolve, 500));
1599
2611
  // Verify the prompt is gone
1600
2612
  const updatedContent = capturePaneContent(paneInfo, 10);
1601
2613
  // If trust prompt is gone, check if we need to resend the Claude command
1602
- const promptGone = !trustPromptPatterns.some(p => p.test(updatedContent));
2614
+ const promptGone = !trustPromptPatterns.some((p) => p.test(updatedContent));
1603
2615
  if (promptGone) {
1604
2616
  // Check if Claude is running or if we need to restart it
1605
- const claudeRunning = updatedContent.includes('Claude') ||
1606
- updatedContent.includes('claude') ||
1607
- updatedContent.includes('Assistant') ||
1608
- (prompt && updatedContent.includes(prompt.substring(0, Math.min(20, prompt.length))));
1609
- if (!claudeRunning && !updatedContent.includes('$')) {
1610
- await new Promise(resolve => setTimeout(resolve, 300));
1611
- execSync(\`tmux send-keys -t '\${paneInfo}' '\${escapedCmd}'\`, { stdio: 'pipe' });
1612
- 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
+ });
1613
2628
  }
1614
2629
  break;
1615
2630
  }
1616
2631
  }
1617
2632
  }
1618
2633
  // If we see Claude is already running without prompts, we're done
1619
- if (!hasTrustPrompt && !hasClaudePermissionPrompt &&
1620
- (paneContent.includes('Claude') || paneContent.includes('Assistant'))) {
2634
+ if (!hasTrustPrompt &&
2635
+ !hasClaudePermissionPrompt &&
2636
+ (paneContent.includes("Claude") ||
2637
+ paneContent.includes("Assistant"))) {
1621
2638
  break;
1622
2639
  }
1623
2640
  }
@@ -1627,37 +2644,35 @@ const DmuxApp = ({ panesFile, projectName, sessionName, settingsFile, projectRoo
1627
2644
  }
1628
2645
  };
1629
2646
  // Start monitoring for trust prompt in background
1630
- autoApproveTrust().catch(err => {
1631
- });
2647
+ autoApproveTrust().catch((err) => { });
1632
2648
  }
1633
2649
  // Keep focus on the new pane
1634
- execSync(\`tmux select-pane -t '\${paneInfo}'\`, { stdio: 'pipe' });
2650
+ execSync(\`tmux select-pane -t '\${paneInfo}'\`, { stdio: "pipe" });
1635
2651
  // Save pane info
1636
2652
  const newPane = {
1637
2653
  id: \`dmux-\${Date.now()}\`,
1638
2654
  slug,
1639
- prompt: prompt || 'No initial prompt',
2655
+ prompt: prompt || "No initial prompt",
1640
2656
  paneId: paneInfo,
1641
2657
  worktreePath,
1642
- agent
2658
+ agent,
1643
2659
  };
1644
2660
  const updatedPanes = [...panes, newPane];
1645
2661
  await savePanes(updatedPanes);
1646
2662
  // Switch back to the original pane (where dmux is running)
1647
- execSync(\`tmux select-pane -t '\${originalPaneId}'\`, { stdio: 'pipe' });
2663
+ execSync(\`tmux select-pane -t '\${originalPaneId}'\`, { stdio: "pipe" });
1648
2664
  // Re-set the title for the dmux pane
1649
2665
  try {
1650
- execSync(\`tmux select-pane -t '\${originalPaneId}' -T "dmux-\${projectName}"\`, { stdio: 'pipe' });
2666
+ execSync(\`tmux select-pane -t '\${originalPaneId}' -T "dmux v\${packageJson.version} - \${projectName}"\`, { stdio: "pipe" });
1651
2667
  }
1652
2668
  catch {
1653
2669
  // Ignore if setting title fails
1654
2670
  }
1655
2671
  // Clear the screen and redraw the UI
1656
- process.stdout.write('\\x1b[2J\\x1b[H');
2672
+ process.stdout.write("\\x1b[2J\\x1b[H");
1657
2673
  // Reset the creating pane flag and refresh
1658
2674
  setIsCreatingPane(false);
1659
- setStatusMessage('');
1660
- setNewPanePrompt('');
2675
+ setStatusMessage("");
1661
2676
  // Force a reload of panes to ensure UI is up to date
1662
2677
  await loadPanes();
1663
2678
  };
@@ -1665,30 +2680,32 @@ const DmuxApp = ({ panesFile, projectName, sessionName, settingsFile, projectRoo
1665
2680
  try {
1666
2681
  // Enable pane borders to show titles (if not already enabled)
1667
2682
  try {
1668
- execSync(\`tmux set-option -g pane-border-status top\`, { stdio: 'pipe' });
2683
+ execSync(\`tmux set-option -g pane-border-status top\`, { stdio: "pipe" });
1669
2684
  }
1670
2685
  catch {
1671
2686
  // Ignore if already set or fails
1672
2687
  }
1673
- execSync(\`tmux select-pane -t '\${paneId}'\`, { stdio: 'pipe' });
2688
+ execSync(\`tmux select-pane -t '\${paneId}'\`, { stdio: "pipe" });
1674
2689
  // Clear screen after jump to remove artifacts
1675
2690
  clearScreen();
1676
- setStatusMessage('Jumped to pane');
1677
- setTimeout(() => setStatusMessage(''), 2000);
2691
+ setStatusMessage("Jumped to pane");
2692
+ setTimeout(() => setStatusMessage(""), 2000);
1678
2693
  }
1679
2694
  catch {
1680
- setStatusMessage('Failed to jump - pane may be closed');
1681
- setTimeout(() => setStatusMessage(''), 2000);
2695
+ setStatusMessage("Failed to jump - pane may be closed");
2696
+ setTimeout(() => setStatusMessage(""), 2000);
1682
2697
  }
1683
2698
  };
1684
2699
  const runCommand = async (type, pane) => {
1685
2700
  if (!pane.worktreePath) {
1686
- setStatusMessage('No worktree path for this pane');
1687
- setTimeout(() => setStatusMessage(''), 2000);
2701
+ setStatusMessage("No worktree path for this pane");
2702
+ setTimeout(() => setStatusMessage(""), 2000);
1688
2703
  return;
1689
2704
  }
1690
- const command = type === 'test' ? projectSettings.testCommand : projectSettings.devCommand;
1691
- 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;
1692
2709
  if (!command) {
1693
2710
  // No command configured, prompt user
1694
2711
  setShowCommandPrompt(type);
@@ -1707,444 +2724,139 @@ const DmuxApp = ({ panesFile, projectName, sessionName, settingsFile, projectRoo
1707
2724
  setRunningCommand(true);
1708
2725
  setStatusMessage(\`Starting \${type} in background window...\`);
1709
2726
  // Kill existing window if present
1710
- const existingWindowId = type === 'test' ? pane.testWindowId : pane.devWindowId;
2727
+ const existingWindowId = type === "test" ? pane.testWindowId : pane.devWindowId;
1711
2728
  if (existingWindowId) {
1712
2729
  try {
1713
- execSync(\`tmux kill-window -t '\${existingWindowId}'\`, { stdio: 'pipe' });
1714
- }
1715
- catch { }
1716
- }
1717
- // Create a new background window for the command
1718
- const windowName = \`\${pane.slug}-\${type}\`;
1719
- const windowId = execSync(\`tmux new-window -d -n '\${windowName}' -P -F '#{window_id}'\`, { encoding: 'utf-8', stdio: 'pipe' }).trim();
1720
- // Create a log file to capture output
1721
- const logFile = \`/tmp/dmux-\${pane.id}-\${type}.log\`;
1722
- // Build the command with output capture
1723
- const fullCommand = type === 'test'
1724
- ? \`cd "\${pane.worktreePath}" && \${command} 2>&1 | tee \${logFile}\`
1725
- : \`cd "\${pane.worktreePath}" && \${command} 2>&1 | tee \${logFile}\`;
1726
- // Send the command to the new window
1727
- execSync(\`tmux send-keys -t '\${windowId}' '\${fullCommand.replace(/'/g, "'\\\\''")}'\`, { stdio: 'pipe' });
1728
- execSync(\`tmux send-keys -t '\${windowId}' Enter\`, { stdio: 'pipe' });
1729
- // Update pane with window info
1730
- const updatedPane = {
1731
- ...pane,
1732
- [type === 'test' ? 'testWindowId' : 'devWindowId']: windowId,
1733
- [type === 'test' ? 'testStatus' : 'devStatus']: 'running'
1734
- };
1735
- const updatedPanes = panes.map(p => p.id === pane.id ? updatedPane : p);
1736
- await savePanes(updatedPanes);
1737
- // Start monitoring the output
1738
- if (type === 'test') {
1739
- // For tests, monitor for completion
1740
- setTimeout(() => monitorTestOutput(pane.id, logFile), 2000);
1741
- }
1742
- else {
1743
- // For dev, monitor for server URL
1744
- setTimeout(() => monitorDevOutput(pane.id, logFile), 2000);
1745
- }
1746
- setRunningCommand(false);
1747
- setStatusMessage(\`\${type === 'test' ? 'Test' : 'Dev server'} started in background\`);
1748
- setTimeout(() => setStatusMessage(''), 3000);
1749
- }
1750
- catch (error) {
1751
- setRunningCommand(false);
1752
- setStatusMessage(\`Failed to run \${type} command\`);
1753
- setTimeout(() => setStatusMessage(''), 3000);
1754
- }
1755
- };
1756
- // Update handling moved to useAutoUpdater
1757
- // Helper function to clear screen artifacts
1758
- const clearScreen = () => {
1759
- // CRITICAL: Force Ink to re-render FIRST, before clearing
1760
- // This prevents blank screen by ensuring React starts rendering immediately
1761
- setForceRepaintTrigger(prev => prev + 1);
1762
- // Multiple clearing strategies to prevent artifacts
1763
- // 1. Clear screen with ANSI codes
1764
- process.stdout.write('\\x1b[2J\\x1b[H');
1765
- // 2. Clear tmux history
1766
- try {
1767
- execSync('tmux clear-history', { stdio: 'pipe' });
1768
- }
1769
- catch { }
1770
- // 3. Force tmux to refresh the display
1771
- try {
1772
- execSync('tmux refresh-client', { stdio: 'pipe' });
1773
- }
1774
- catch { }
1775
- };
1776
- // Cleanup function for exit
1777
- const cleanExit = () => {
1778
- // Clear screen before exiting Ink
1779
- process.stdout.write('\\x1b[2J\\x1b[3J\\x1b[H');
1780
- // Exit the Ink app (this cleans up the React tree)
1781
- exit();
1782
- // Give Ink a moment to clean up its rendering, then do final cleanup
1783
- setTimeout(() => {
1784
- // Multiple aggressive clearing strategies
1785
- process.stdout.write('\\x1b[2J\\x1b[H'); // Clear screen and move cursor to home
1786
- process.stdout.write('\\x1b[3J'); // Clear scrollback buffer
1787
- process.stdout.write('\\x1b[0m'); // Reset all attributes
1788
- // Clear tmux history and pane
1789
- try {
1790
- execSync('tmux clear-history', { stdio: 'pipe' });
1791
- execSync('tmux send-keys C-l', { stdio: 'pipe' });
1792
- }
1793
- catch { }
1794
- // One more final clear
1795
- process.stdout.write('\\x1b[2J\\x1b[H');
1796
- // Show clean goodbye message
1797
- process.stdout.write('\\n Run dmux again to resume. Goodbye 👋\\n\\n');
1798
- // Exit process
1799
- process.exit(0);
1800
- }, 100);
1801
- };
1802
- useInput(async (input, key) => {
1803
- // Handle Ctrl+C for quit confirmation (must be first, before any other checks)
1804
- if (key.ctrl && input === 'c') {
1805
- if (quitConfirmMode) {
1806
- // Second Ctrl+C - actually quit
1807
- cleanExit();
1808
- }
1809
- else {
1810
- // First Ctrl+C - show confirmation
1811
- setQuitConfirmMode(true);
1812
- // Reset after 3 seconds if user doesn't press Ctrl+C again
1813
- setTimeout(() => {
1814
- setQuitConfirmMode(false);
1815
- }, 3000);
1816
- }
1817
- return;
1818
- }
1819
- if (isCreatingPane || runningCommand || isUpdating || isLoading || isCreatingTunnel) {
1820
- // Disable input while performing operations or loading
1821
- return;
1822
- }
1823
- // Handle kebab menu navigation
1824
- if (showKebabMenu && kebabMenuPaneIndex !== null) {
1825
- const currentPane = panes[kebabMenuPaneIndex];
1826
- const availableActions = kebabMenuActions;
1827
- if (key.escape) {
1828
- setShowKebabMenu(false);
1829
- setKebabMenuPaneIndex(null);
1830
- setKebabMenuOption(0);
1831
- setKebabMenuActions([]);
1832
- return;
1833
- }
1834
- else if (key.upArrow) {
1835
- setKebabMenuOption(Math.max(0, kebabMenuOption - 1));
1836
- return;
1837
- }
1838
- else if (key.downArrow) {
1839
- setKebabMenuOption(Math.min(availableActions.length - 1, kebabMenuOption + 1));
1840
- return;
1841
- }
1842
- else if (key.return) {
1843
- // Execute the selected menu action
1844
- setShowKebabMenu(false);
1845
- const selectedAction = availableActions[kebabMenuOption];
1846
- if (selectedAction) {
1847
- // Use the action system to execute
1848
- actionSystem.executeAction(selectedAction.id, currentPane, { mainBranch: getMainBranch() });
1849
- }
1850
- setKebabMenuPaneIndex(null);
1851
- setKebabMenuOption(0);
1852
- setKebabMenuActions([]);
1853
- return;
1854
- }
1855
- // Don't process other inputs while menu is open
1856
- return;
1857
- }
1858
- // Handle quit confirm mode - ESC cancels it
1859
- if (quitConfirmMode) {
1860
- if (key.escape) {
1861
- setQuitConfirmMode(false);
1862
- return;
1863
- }
1864
- // Allow other inputs to continue (don't return early)
1865
- }
1866
- // Handle QR code view
1867
- if (showQRCode) {
1868
- if (key.escape) {
1869
- setShowQRCode(false);
1870
- }
1871
- return;
1872
- }
1873
- // Handle hooks dialog
1874
- if (showHooksDialog) {
1875
- if (key.escape) {
1876
- setShowHooksDialog(false);
1877
- setHooksSelectedIndex(0);
1878
- // Go back to settings dialog
1879
- setShowSettingsDialog(true);
1880
- setSettingsMode('list');
1881
- return;
1882
- }
1883
- else if (key.upArrow) {
1884
- setHooksSelectedIndex(Math.max(0, hooksSelectedIndex - 1));
1885
- return;
1886
- }
1887
- else if (key.downArrow) {
1888
- setHooksSelectedIndex(Math.min(hooksData.length - 1, hooksSelectedIndex + 1));
1889
- return;
1890
- }
1891
- else if (input === 'e') {
1892
- // Edit hooks using an agent
1893
- setShowHooksDialog(false);
1894
- setHooksSelectedIndex(0);
1895
- const prompt = "I would like to edit my dmux hooks in .dmux-hooks, please read the instructions in there and ask me what I want to edit";
1896
- setPendingPrompt(prompt);
1897
- setNewPanePrompt(prompt);
1898
- // Choose agent
1899
- const agents = availableAgents;
1900
- if (agents.length === 0) {
1901
- createNewPaneHook(prompt);
1902
- }
1903
- else if (agents.length === 1) {
1904
- createNewPaneHook(prompt, agents[0]);
1905
- }
1906
- else {
1907
- setShowAgentChoiceDialog(true);
1908
- setAgentChoice(agentChoice || 'claude');
1909
- }
1910
- return;
1911
- }
1912
- return;
1913
- }
1914
- // Handle settings dialog
1915
- if (showSettingsDialog) {
1916
- if (key.escape) {
1917
- if (settingsMode === 'list') {
1918
- // Close settings dialog
1919
- setShowSettingsDialog(false);
1920
- setSettingsMode('list');
1921
- setSettingsSelectedIndex(0);
1922
- setSettingsEditingKey(undefined);
1923
- }
1924
- else {
1925
- // Go back to list
1926
- setSettingsMode('list');
1927
- setSettingsEditingKey(undefined);
1928
- setSettingsEditingValueIndex(0);
1929
- setSettingsScopeIndex(0);
1930
- }
1931
- return;
1932
- }
1933
- else if (key.upArrow) {
1934
- if (settingsMode === 'list') {
1935
- setSettingsSelectedIndex(Math.max(0, settingsSelectedIndex - 1));
1936
- }
1937
- else if (settingsMode === 'edit') {
1938
- const currentDef = SETTING_DEFINITIONS[settingsSelectedIndex];
1939
- const maxIndex = currentDef.type === 'boolean' ? 1 : (currentDef.options?.length || 1) - 1;
1940
- setSettingsEditingValueIndex(Math.max(0, settingsEditingValueIndex - 1));
1941
- }
1942
- else if (settingsMode === 'scope') {
1943
- setSettingsScopeIndex(Math.max(0, settingsScopeIndex - 1));
1944
- }
1945
- return;
1946
- }
1947
- else if (key.downArrow) {
1948
- if (settingsMode === 'list') {
1949
- setSettingsSelectedIndex(Math.min(SETTING_DEFINITIONS.length - 1, settingsSelectedIndex + 1));
1950
- }
1951
- else if (settingsMode === 'edit') {
1952
- const currentDef = SETTING_DEFINITIONS[settingsSelectedIndex];
1953
- const maxIndex = currentDef.type === 'boolean' ? 1 : (currentDef.options?.length || 1) - 1;
1954
- setSettingsEditingValueIndex(Math.min(maxIndex, settingsEditingValueIndex + 1));
1955
- }
1956
- else if (settingsMode === 'scope') {
1957
- setSettingsScopeIndex(Math.min(1, settingsScopeIndex + 1));
1958
- }
1959
- return;
1960
- }
1961
- else if (key.return) {
1962
- if (settingsMode === 'list') {
1963
- const currentDef = SETTING_DEFINITIONS[settingsSelectedIndex];
1964
- // Handle action type - trigger the action instead of editing
1965
- if (currentDef.type === 'action') {
1966
- if (currentDef.key === 'hooks') {
1967
- // Show hooks dialog
1968
- setShowSettingsDialog(false);
1969
- const { hasHook } = await import('./utils/hooks.js');
1970
- const allHookTypes = [
1971
- 'before_pane_create',
1972
- 'pane_created',
1973
- 'worktree_created',
1974
- 'before_pane_close',
1975
- 'pane_closed',
1976
- 'before_worktree_remove',
1977
- 'worktree_removed',
1978
- 'pre_merge',
1979
- 'post_merge',
1980
- 'run_test',
1981
- 'run_dev',
1982
- ];
1983
- const hooks = allHookTypes.map(hookName => ({
1984
- name: hookName,
1985
- active: hasHook(projectRoot || process.cwd(), hookName)
1986
- }));
1987
- setHooksData(hooks);
1988
- setShowHooksDialog(true);
1989
- setHooksSelectedIndex(0);
1990
- }
1991
- return;
1992
- }
1993
- // Enter edit mode for regular settings
1994
- setSettingsEditingKey(currentDef.key);
1995
- setSettingsMode('edit');
1996
- // Set initial value index based on current setting
1997
- const currentValue = settingsManager.getSetting(currentDef.key);
1998
- if (currentDef.type === 'boolean') {
1999
- setSettingsEditingValueIndex(currentValue ? 0 : 1);
2000
- }
2001
- else if (currentDef.type === 'select' && currentDef.options) {
2002
- const optIndex = currentDef.options.findIndex(o => o.value === currentValue);
2003
- setSettingsEditingValueIndex(Math.max(0, optIndex));
2004
- }
2005
- }
2006
- else if (settingsMode === 'edit') {
2007
- // Go to scope selection
2008
- setSettingsMode('scope');
2009
- setSettingsScopeIndex(0);
2010
- }
2011
- else if (settingsMode === 'scope') {
2012
- // Save the setting
2013
- const currentDef = SETTING_DEFINITIONS[settingsSelectedIndex];
2014
- const scope = settingsScopeIndex === 0 ? 'global' : 'project';
2015
- // Only save if this is not an action type (actions don't have values)
2016
- if (currentDef.type !== 'action') {
2017
- // Calculate the new value
2018
- let newValue;
2019
- if (currentDef.type === 'boolean') {
2020
- newValue = settingsEditingValueIndex === 0;
2021
- }
2022
- else if (currentDef.type === 'select' && currentDef.options) {
2023
- newValue = currentDef.options[settingsEditingValueIndex]?.value || '';
2024
- }
2025
- // Update the setting - cast key to proper type since we know it's not an action
2026
- settingsManager.updateSetting(currentDef.key, newValue, scope);
2027
- }
2028
- // Reset to list view
2029
- setSettingsMode('list');
2030
- setSettingsEditingKey(undefined);
2031
- setSettingsEditingValueIndex(0);
2032
- setSettingsScopeIndex(0);
2033
- setStatusMessage(\`Setting saved (\${scope})\`);
2034
- setTimeout(() => setStatusMessage(''), 2000);
2035
- }
2036
- return;
2037
- }
2038
- return;
2039
- }
2040
- // Handle action system confirm dialog
2041
- if (actionSystem.actionState.showConfirmDialog) {
2042
- if (key.escape) {
2043
- if (actionSystem.actionState.onConfirmNo) {
2044
- actionSystem.executeCallback(actionSystem.actionState.onConfirmNo);
2045
- }
2046
- else {
2047
- actionSystem.setActionState(prev => ({ ...prev, showConfirmDialog: false }));
2048
- }
2049
- return;
2050
- }
2051
- else if (key.upArrow) {
2052
- actionSystem.setActionState(prev => ({
2053
- ...prev,
2054
- confirmSelectedIndex: Math.max(0, prev.confirmSelectedIndex - 1)
2055
- }));
2056
- return;
2057
- }
2058
- else if (key.downArrow) {
2059
- actionSystem.setActionState(prev => ({
2060
- ...prev,
2061
- confirmSelectedIndex: Math.min(1, prev.confirmSelectedIndex + 1)
2062
- }));
2063
- return;
2064
- }
2065
- else if (key.return) {
2066
- // Execute based on selected index
2067
- if (actionSystem.actionState.confirmSelectedIndex === 0) {
2068
- if (actionSystem.actionState.onConfirmYes) {
2069
- actionSystem.executeCallback(actionSystem.actionState.onConfirmYes);
2070
- }
2071
- }
2072
- else {
2073
- if (actionSystem.actionState.onConfirmNo) {
2074
- actionSystem.executeCallback(actionSystem.actionState.onConfirmNo);
2075
- }
2076
- else {
2077
- actionSystem.setActionState(prev => ({ ...prev, showConfirmDialog: false }));
2078
- }
2079
- }
2080
- return;
2081
- }
2082
- else if (input === 'y' || input === 'Y') {
2083
- // Shortcut: yes
2084
- if (actionSystem.actionState.onConfirmYes) {
2085
- actionSystem.executeCallback(actionSystem.actionState.onConfirmYes);
2730
+ execSync(\`tmux kill-window -t '\${existingWindowId}'\`, {
2731
+ stdio: "pipe",
2732
+ });
2086
2733
  }
2087
- return;
2734
+ catch { }
2088
2735
  }
2089
- else if (input === 'n' || input === 'N') {
2090
- // Shortcut: no
2091
- if (actionSystem.actionState.onConfirmNo) {
2092
- actionSystem.executeCallback(actionSystem.actionState.onConfirmNo);
2093
- }
2094
- else {
2095
- actionSystem.setActionState(prev => ({ ...prev, showConfirmDialog: false }));
2096
- }
2097
- 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" });
2098
2811
  }
2812
+ catch { }
2813
+ // One more final clear
2814
+ process.stdout.write("\\x1b[2J\\x1b[H");
2815
+ // Show clean goodbye message
2816
+ process.stdout.write("\\n Run dmux again to resume. Goodbye 👋\\n\\n");
2817
+ // Exit process
2818
+ process.exit(0);
2819
+ }, 100);
2820
+ };
2821
+ useInput(async (input, key) => {
2822
+ const logService = LogService.getInstance();
2823
+ // Log all input for debugging (only first 50 chars to avoid spam)
2824
+ const inputPreview = input.length > 50 ? input.substring(0, 50) + "..." : input;
2825
+ logService.debug(\`Input: "\${inputPreview}"\`, "InputDebug");
2826
+ // Ignore input temporarily after popup operations (prevents buffered keys from being processed)
2827
+ if (ignoreInput) {
2099
2828
  return;
2100
2829
  }
2101
- // Handle action system input dialog
2102
- if (actionSystem.actionState.showInputDialog) {
2103
- if (key.escape) {
2104
- actionSystem.setActionState(prev => ({ ...prev, showInputDialog: false }));
2105
- 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();
2106
2835
  }
2107
- else if (key.return) {
2108
- if (actionSystem.actionState.onInputSubmit) {
2109
- actionSystem.executeCallback(async () => actionSystem.actionState.onInputSubmit(actionSystem.actionState.inputValue));
2110
- }
2111
- 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);
2112
2843
  }
2113
- // Let CleanTextInput handle all other key events
2114
2844
  return;
2115
2845
  }
2116
- // Handle action system choice dialog
2117
- 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) {
2118
2852
  if (key.escape) {
2119
- actionSystem.setActionState(prev => ({ ...prev, showChoiceDialog: false }));
2120
- return;
2121
- }
2122
- else if (key.upArrow) {
2123
- actionSystem.setActionState(prev => ({
2124
- ...prev,
2125
- choiceSelectedIndex: Math.max(0, prev.choiceSelectedIndex - 1)
2126
- }));
2127
- return;
2128
- }
2129
- else if (key.downArrow) {
2130
- const maxIndex = actionSystem.actionState.choiceOptions.length - 1;
2131
- actionSystem.setActionState(prev => ({
2132
- ...prev,
2133
- choiceSelectedIndex: Math.min(maxIndex, prev.choiceSelectedIndex + 1)
2134
- }));
2135
- return;
2136
- }
2137
- else if (key.return) {
2138
- const selectedOption = actionSystem.actionState.choiceOptions[actionSystem.actionState.choiceSelectedIndex];
2139
- if (selectedOption && actionSystem.actionState.onChoiceSelect) {
2140
- actionSystem.executeCallback(async () => actionSystem.actionState.onChoiceSelect(selectedOption.id));
2141
- }
2853
+ setQuitConfirmMode(false);
2142
2854
  return;
2143
2855
  }
2144
- return;
2856
+ // Allow other inputs to continue (don't return early)
2145
2857
  }
2146
2858
  if (showFileCopyPrompt) {
2147
- if (input === 'y' || input === 'Y') {
2859
+ if (input === "y" || input === "Y") {
2148
2860
  setShowFileCopyPrompt(false);
2149
2861
  const selectedPane = panes[selectedIndex];
2150
2862
  if (selectedPane && selectedPane.worktreePath && currentCommandType) {
@@ -2152,7 +2864,7 @@ const DmuxApp = ({ panesFile, projectName, sessionName, settingsFile, projectRoo
2152
2864
  // Mark as not first run and continue with command
2153
2865
  const newSettings = {
2154
2866
  ...projectSettings,
2155
- [currentCommandType === 'test' ? 'firstTestRun' : 'firstDevRun']: true
2867
+ [currentCommandType === "test" ? "firstTestRun" : "firstDevRun"]: true,
2156
2868
  };
2157
2869
  await saveSettings(newSettings);
2158
2870
  // Now run the actual command
@@ -2160,14 +2872,14 @@ const DmuxApp = ({ panesFile, projectName, sessionName, settingsFile, projectRoo
2160
2872
  }
2161
2873
  setCurrentCommandType(null);
2162
2874
  }
2163
- else if (input === 'n' || input === 'N' || key.escape) {
2875
+ else if (input === "n" || input === "N" || key.escape) {
2164
2876
  setShowFileCopyPrompt(false);
2165
2877
  const selectedPane = panes[selectedIndex];
2166
2878
  if (selectedPane && currentCommandType) {
2167
2879
  // Mark as not first run and continue without copying
2168
2880
  const newSettings = {
2169
2881
  ...projectSettings,
2170
- [currentCommandType === 'test' ? 'firstTestRun' : 'firstDevRun']: true
2882
+ [currentCommandType === "test" ? "firstTestRun" : "firstDevRun"]: true,
2171
2883
  };
2172
2884
  await saveSettings(newSettings);
2173
2885
  // Now run the actual command
@@ -2177,36 +2889,13 @@ const DmuxApp = ({ panesFile, projectName, sessionName, settingsFile, projectRoo
2177
2889
  }
2178
2890
  return;
2179
2891
  }
2180
- if (showAgentChoiceDialog) {
2181
- if (key.escape) {
2182
- setShowAgentChoiceDialog(false);
2183
- setShowNewPaneDialog(true);
2184
- setNewPanePrompt(pendingPrompt);
2185
- setPendingPrompt('');
2186
- }
2187
- else if (key.leftArrow || input === '1' || (input && input.toLowerCase() === 'c')) {
2188
- setAgentChoice('claude');
2189
- }
2190
- else if (key.rightArrow || input === '2' || (input && input.toLowerCase() === 'o')) {
2191
- setAgentChoice('opencode');
2192
- }
2193
- else if (key.return) {
2194
- const chosen = agentChoice || (availableAgents[0] || 'claude');
2195
- const promptValue = pendingPrompt;
2196
- setShowAgentChoiceDialog(false);
2197
- setPendingPrompt('');
2198
- await createNewPaneHook(promptValue, chosen);
2199
- setNewPanePrompt('');
2200
- }
2201
- return;
2202
- }
2203
2892
  if (showCommandPrompt) {
2204
2893
  if (key.escape) {
2205
2894
  setShowCommandPrompt(null);
2206
- setCommandInput('');
2895
+ setCommandInput("");
2207
2896
  }
2208
2897
  else if (key.return) {
2209
- if (commandInput.trim() === '') {
2898
+ if (commandInput.trim() === "") {
2210
2899
  // If empty, suggest a default command based on package manager
2211
2900
  const suggested = await suggestCommand(showCommandPrompt);
2212
2901
  if (suggested) {
@@ -2217,13 +2906,15 @@ const DmuxApp = ({ panesFile, projectName, sessionName, settingsFile, projectRoo
2217
2906
  // User provided manual command
2218
2907
  const newSettings = {
2219
2908
  ...projectSettings,
2220
- [showCommandPrompt === 'test' ? 'testCommand' : 'devCommand']: commandInput.trim()
2909
+ [showCommandPrompt === "test" ? "testCommand" : "devCommand"]: commandInput.trim(),
2221
2910
  };
2222
2911
  await saveSettings(newSettings);
2223
2912
  const selectedPane = panes[selectedIndex];
2224
2913
  if (selectedPane) {
2225
2914
  // Check if first run
2226
- const isFirstRun = showCommandPrompt === 'test' ? !projectSettings.firstTestRun : !projectSettings.firstDevRun;
2915
+ const isFirstRun = showCommandPrompt === "test"
2916
+ ? !projectSettings.firstTestRun
2917
+ : !projectSettings.firstDevRun;
2227
2918
  if (isFirstRun) {
2228
2919
  setCurrentCommandType(showCommandPrompt);
2229
2920
  setShowCommandPrompt(null);
@@ -2232,173 +2923,210 @@ const DmuxApp = ({ panesFile, projectName, sessionName, settingsFile, projectRoo
2232
2923
  else {
2233
2924
  await runCommandInternal(showCommandPrompt, selectedPane);
2234
2925
  setShowCommandPrompt(null);
2235
- setCommandInput('');
2926
+ setCommandInput("");
2236
2927
  }
2237
2928
  }
2238
2929
  else {
2239
2930
  setShowCommandPrompt(null);
2240
- setCommandInput('');
2931
+ setCommandInput("");
2241
2932
  }
2242
2933
  }
2243
2934
  }
2244
2935
  return;
2245
2936
  }
2246
- if (showNewPaneDialog) {
2247
- if (key.escape) {
2248
- setShowNewPaneDialog(false);
2249
- setNewPanePrompt('');
2250
- }
2251
- // TextInput handles other input events
2252
- return;
2253
- }
2254
2937
  // Handle directional navigation with spatial awareness based on card grid layout
2255
2938
  if (key.upArrow || key.downArrow || key.leftArrow || key.rightArrow) {
2256
2939
  let targetIndex = null;
2257
2940
  if (key.upArrow) {
2258
- targetIndex = findCardInDirection(selectedIndex, 'up');
2941
+ targetIndex = findCardInDirection(selectedIndex, "up");
2259
2942
  }
2260
2943
  else if (key.downArrow) {
2261
- targetIndex = findCardInDirection(selectedIndex, 'down');
2944
+ targetIndex = findCardInDirection(selectedIndex, "down");
2262
2945
  }
2263
2946
  else if (key.leftArrow) {
2264
- targetIndex = findCardInDirection(selectedIndex, 'left');
2947
+ targetIndex = findCardInDirection(selectedIndex, "left");
2265
2948
  }
2266
2949
  else if (key.rightArrow) {
2267
- targetIndex = findCardInDirection(selectedIndex, 'right');
2950
+ targetIndex = findCardInDirection(selectedIndex, "right");
2268
2951
  }
2269
2952
  if (targetIndex !== null) {
2270
2953
  setSelectedIndex(targetIndex);
2271
2954
  }
2272
2955
  return;
2273
2956
  }
2274
- if ((input === 'm' || key.return) && selectedIndex < panes.length) {
2275
- // Open kebab menu for selected pane
2276
- const selectedPane = panes[selectedIndex];
2277
- const actions = getAvailableActions(selectedPane, projectSettings);
2278
- setKebabMenuActions(actions);
2279
- setShowKebabMenu(true);
2280
- setKebabMenuPaneIndex(selectedIndex);
2281
- setKebabMenuOption(0);
2282
- }
2283
- else if (input === 's') {
2284
- // Open settings dialog
2285
- setShowSettingsDialog(true);
2286
- setSettingsMode('list');
2287
- setSettingsSelectedIndex(0);
2288
- }
2289
- 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") {
2290
2980
  cleanExit();
2291
2981
  }
2292
- else if (input === 'r' && server) {
2293
- // Create tunnel if not already created, then show QR code
2294
- if (!tunnelUrl) {
2295
- setIsCreatingTunnel(true);
2296
- setStatusMessage('Creating tunnel...');
2297
- try {
2298
- const url = await server.startTunnel();
2299
- setTunnelUrl(url);
2300
- setStatusMessage('');
2301
- setShowQRCode(true);
2302
- }
2303
- catch (error) {
2304
- setStatusMessage('Failed to create tunnel');
2305
- setTimeout(() => setStatusMessage(''), 3000);
2306
- }
2307
- finally {
2308
- setIsCreatingTunnel(false);
2309
- }
2982
+ else if (input === "r" && server) {
2983
+ // Handle remote tunnel
2984
+ if (tunnelUrl) {
2985
+ // Tunnel exists - open popup with QR code
2986
+ await launchRemotePopup();
2310
2987
  }
2311
- else {
2312
- // Tunnel already exists, just show the QR code
2313
- 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
+ })();
2314
3004
  }
3005
+ // If tunnelCreating is true, do nothing (already creating)
3006
+ return;
2315
3007
  }
2316
- else if (!isLoading && (input === 'n' || (key.return && selectedIndex === panes.length))) {
2317
- // Clear the prompt and show dialog in next tick to prevent 'n' bleeding through
2318
- setNewPanePrompt('');
2319
- setShowNewPaneDialog(true);
2320
- 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;
2321
3040
  }
2322
- else if (input === 'j' && selectedIndex < panes.length) {
3041
+ else if (input === "j" && selectedIndex < panes.length) {
2323
3042
  // Jump to pane (NEW: using action system)
2324
3043
  StateManager.getInstance().setDebugMessage(\`Jumping to pane: \${panes[selectedIndex].slug}\`);
2325
- setTimeout(() => StateManager.getInstance().setDebugMessage(''), 2000);
3044
+ setTimeout(() => StateManager.getInstance().setDebugMessage(""), 2000);
2326
3045
  actionSystem.executeAction(PaneAction.VIEW, panes[selectedIndex]);
2327
3046
  }
2328
- else if (input === 'x' && selectedIndex < panes.length) {
3047
+ else if (input === "x" && selectedIndex < panes.length) {
2329
3048
  // Close pane (NEW: using action system)
2330
3049
  StateManager.getInstance().setDebugMessage(\`Closing pane: \${panes[selectedIndex].slug}\`);
2331
- setTimeout(() => StateManager.getInstance().setDebugMessage(''), 2000);
3050
+ setTimeout(() => StateManager.getInstance().setDebugMessage(""), 2000);
2332
3051
  actionSystem.executeAction(PaneAction.CLOSE, panes[selectedIndex]);
2333
3052
  }
2334
3053
  else if (key.return && selectedIndex < panes.length) {
2335
3054
  // Jump to pane (NEW: using action system)
2336
3055
  StateManager.getInstance().setDebugMessage(\`Jumping to pane: \${panes[selectedIndex].slug}\`);
2337
- setTimeout(() => StateManager.getInstance().setDebugMessage(''), 2000);
3056
+ setTimeout(() => StateManager.getInstance().setDebugMessage(""), 2000);
2338
3057
  actionSystem.executeAction(PaneAction.VIEW, panes[selectedIndex]);
2339
3058
  }
2340
3059
  });
2341
- // If showing QR code, render only that
2342
- if (showQRCode && tunnelUrl) {
2343
- return (React.createElement(Box, { flexDirection: "column" },
2344
- React.createElement(Box, { marginBottom: 1 },
2345
- React.createElement(Text, { bold: true, color: "cyan" }, "dmux - Remote Access")),
2346
- React.createElement(QRCode, { url: tunnelUrl }),
2347
- React.createElement(Box, { marginTop: 1 },
2348
- 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;
2349
3072
  }
2350
- return (React.createElement(Box, { flexDirection: "column" },
3073
+ else {
3074
+ // Base footer (logs divider + logs + shortcuts - always shown)
3075
+ footerLines = 4; // marginTop + logs divider + logs + shortcuts
3076
+ // Add network section (now 2 lines for local IP + remote tunnel, plus 2 dividers)
3077
+ if (serverPort && serverPort > 0) {
3078
+ footerLines += 4;
3079
+ }
3080
+ // Add debug info
3081
+ if (process.env.DEBUG_DMUX) {
3082
+ footerLines += 1;
3083
+ }
3084
+ // Add status line
3085
+ if (updateAvailable || currentBranch || debugMessage) {
3086
+ footerLines += 1;
3087
+ }
3088
+ // Add line for each active status message
3089
+ if (statusMessage) {
3090
+ footerLines += 1;
3091
+ }
3092
+ if (actionSystem.actionState.statusMessage) {
3093
+ footerLines += 1;
3094
+ }
3095
+ }
3096
+ const contentHeight = Math.max(terminalHeight - footerLines, 10);
3097
+ return (React.createElement(Box, { flexDirection: "column", height: terminalHeight },
2351
3098
  showRepaintSpinner && (React.createElement(Box, { marginTop: -10, marginLeft: -100 },
2352
3099
  React.createElement(Text, null, "\\u27F3"))),
2353
- React.createElement(PanesGrid, { panes: panes, selectedIndex: selectedIndex, isLoading: isLoading, showNewPaneDialog: showNewPaneDialog, agentStatuses: agentStatuses, kebabMenuPaneIndex: kebabMenuPaneIndex ?? undefined }),
2354
- isLoading && (React.createElement(LoadingIndicator, null)),
2355
- showNewPaneDialog && !showAgentChoiceDialog && (React.createElement(NewPaneDialog, { value: newPanePrompt, onChange: setNewPanePrompt, onSubmit: (value) => {
2356
- const promptValue = value;
2357
- const agents = availableAgents;
2358
- if (agents.length === 0) {
2359
- setShowNewPaneDialog(false);
2360
- setNewPanePrompt('');
2361
- createNewPaneHook(promptValue);
2362
- }
2363
- else if (agents.length === 1) {
2364
- setShowNewPaneDialog(false);
2365
- setNewPanePrompt('');
2366
- createNewPaneHook(promptValue, agents[0]);
2367
- }
2368
- else {
2369
- setPendingPrompt(promptValue);
2370
- setShowNewPaneDialog(false);
2371
- setNewPanePrompt('');
2372
- setShowAgentChoiceDialog(true);
2373
- setAgentChoice(agentChoice || 'claude');
2374
- }
2375
- } })),
2376
- showAgentChoiceDialog && (React.createElement(AgentChoiceDialog, { agentChoice: agentChoice })),
2377
- isCreatingPane && (React.createElement(CreatingIndicator, { message: statusMessage })),
2378
- showCommandPrompt && (React.createElement(CommandPromptDialog, { type: showCommandPrompt, value: commandInput, onChange: setCommandInput })),
2379
- showFileCopyPrompt && (React.createElement(FileCopyPrompt, null)),
2380
- showKebabMenu && kebabMenuPaneIndex !== null && panes[kebabMenuPaneIndex] && (React.createElement(KebabMenu, { selectedOption: kebabMenuOption, actions: kebabMenuActions, paneName: panes[kebabMenuPaneIndex].slug })),
2381
- showSettingsDialog && (React.createElement(SettingsDialog, { settings: settingsManager.getSettings(), globalSettings: settingsManager.getGlobalSettings(), projectSettings: settingsManager.getProjectSettings(), settingDefinitions: SETTING_DEFINITIONS, selectedIndex: settingsSelectedIndex, mode: settingsMode, editingKey: settingsEditingKey, editingValueIndex: settingsEditingValueIndex, scopeIndex: settingsScopeIndex })),
2382
- showHooksDialog && (React.createElement(HooksDialog, { hooks: hooksData, selectedIndex: hooksSelectedIndex })),
2383
- actionSystem.actionState.showConfirmDialog && (React.createElement(ActionConfirmDialog, { key: "confirm-dialog", title: actionSystem.actionState.confirmTitle, message: actionSystem.actionState.confirmMessage, yesLabel: actionSystem.actionState.confirmYesLabel, noLabel: actionSystem.actionState.confirmNoLabel, selectedIndex: actionSystem.actionState.confirmSelectedIndex })),
2384
- actionSystem.actionState.showChoiceDialog && (React.createElement(ActionChoiceDialog, { key: "choice-dialog", title: actionSystem.actionState.choiceTitle, message: actionSystem.actionState.choiceMessage, options: actionSystem.actionState.choiceOptions, selectedIndex: actionSystem.actionState.choiceSelectedIndex })),
2385
- actionSystem.actionState.showInputDialog && (React.createElement(ActionInputDialog, { key: "input-dialog", title: actionSystem.actionState.inputTitle, message: actionSystem.actionState.inputMessage, placeholder: actionSystem.actionState.inputPlaceholder, value: actionSystem.actionState.inputValue, onValueChange: (value) => {
2386
- actionSystem.setActionState(prev => ({ ...prev, inputValue: value }));
2387
- } })),
2388
- actionSystem.actionState.showProgressDialog && (React.createElement(ActionProgressDialog, { key: "progress-dialog", message: actionSystem.actionState.progressMessage, percent: actionSystem.actionState.progressPercent })),
2389
- runningCommand && (React.createElement(RunningIndicator, null)),
2390
- isUpdating && (React.createElement(UpdatingIndicator, null)),
2391
- isCreatingTunnel && (React.createElement(Box, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", padding: 1, marginTop: 1 },
2392
- React.createElement(Text, { bold: true, color: "cyan" }, "Creating tunnel..."),
2393
- React.createElement(Box, { marginTop: 1 },
2394
- React.createElement(Text, { dimColor: true }, "This may take a few moments...")))),
2395
- statusMessage && (React.createElement(Box, { marginTop: 1 },
3100
+ React.createElement(Box, { flexDirection: "column", height: contentHeight, overflow: "hidden" },
3101
+ React.createElement(PanesGrid, { panes: panes, selectedIndex: selectedIndex, isLoading: isLoading, agentStatuses: agentStatuses }),
3102
+ isLoading && React.createElement(LoadingIndicator, null),
3103
+ showCommandPrompt && (React.createElement(CommandPromptDialog, { type: showCommandPrompt, value: commandInput, onChange: setCommandInput })),
3104
+ showFileCopyPrompt && React.createElement(FileCopyPrompt, null),
3105
+ runningCommand && React.createElement(RunningIndicator, null),
3106
+ isUpdating && React.createElement(UpdatingIndicator, null)),
3107
+ statusMessage && (React.createElement(Box, null,
2396
3108
  React.createElement(Text, { color: "green" }, statusMessage))),
2397
- actionSystem.actionState.statusMessage && (React.createElement(Box, { marginTop: 1 },
2398
- React.createElement(Text, { color: actionSystem.actionState.statusType === 'error' ? 'red' :
2399
- actionSystem.actionState.statusType === 'success' ? 'green' :
2400
- 'cyan' }, actionSystem.actionState.statusMessage))),
2401
- 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: (() => {
2402
3130
  if (!process.env.DEBUG_DMUX)
2403
3131
  return undefined;
2404
3132
  const cols = Math.max(1, Math.floor(terminalWidth / 37));
@@ -2406,23 +3134,21 @@ const DmuxApp = ({ panesFile, projectName, sessionName, settingsFile, projectRoo
2406
3134
  const pos = getCardGridPosition(selectedIndex);
2407
3135
  return \`Grid: \${cols} cols × \${rows} rows | Selected: row \${pos.row}, col \${pos.col} | Terminal: \${terminalWidth}w\`;
2408
3136
  })() }),
2409
- React.createElement(Text, { dimColor: true },
2410
- updateAvailable && updateInfo && (React.createElement(Text, { color: "red", bold: true }, "Update available: npm i -g dmux@latest ")),
2411
- "v",
2412
- packageJson.version,
2413
- serverPort && serverPort > 0 && (React.createElement(Text, { dimColor: true },
2414
- " \\u2022 ",
2415
- React.createElement(Text, { color: "cyan" },
2416
- "http://127.0.0.1:",
2417
- serverPort))),
2418
- debugMessage && (React.createElement(Text, { dimColor: true },
3137
+ (updateAvailable || currentBranch || debugMessage) && (React.createElement(Text, { dimColor: true },
3138
+ updateAvailable && updateInfo && (React.createElement(Text, { color: "red", bold: true },
3139
+ "Update available: npm i -g dmux@latest",
3140
+ " ")),
3141
+ currentBranch && (React.createElement(Text, { color: "magenta", bold: true },
3142
+ "branch: ",
3143
+ currentBranch)),
3144
+ debugMessage && React.createElement(Text, { dimColor: true },
2419
3145
  " \\u2022 ",
2420
3146
  debugMessage)))));
2421
3147
  };
2422
3148
  export default DmuxApp;
2423
3149
  //# sourceMappingURL=DmuxApp.js.map`,
2424
3150
  mimeType: 'application/javascript',
2425
- size: 68389
3151
+ size: 94288
2426
3152
  },
2427
3153
  'EnhancedTextInput.js': {
2428
3154
  content: `import React, { useState, useEffect } from 'react';
@@ -4787,26 +5513,174 @@ Expected function or array of functions, received type \${typeof e}.\`),ne)}func
4787
5513
  },
4788
5514
  'dashboard.js': {
4789
5515
  content: `import{d as Mt,r as u,o as zt,a as Nt,n as I,_ as Ht,c as n,b as t,t as g,e as r,f as Ot,g as P,F as y,h as w,w as M,i as at,v as it,j as lt,k as a,l as z,m as Ft}from"./chunks/_plugin-vue_export-helper-Cvoq67hi.js";const Et=Mt({__name:"Dashboard",setup(vt,{expose:i}){i();const U=u("Loading..."),e=u(""),O=u(!1),F=u([]),o=u(null),p=u("Never"),h=u({}),f=u(new Set),J=u({}),x=u(localStorage.getItem("dmux-theme")||"dark"),A=u(new Set),b=u(new Set),K=u(!1),T=u(""),C=u(null),Q=u(!1),E=u([]),L=u(!1),j=u("prompt"),pt=u([]),W=u({}),S=u(null),k=u(null),R=u(!1),D=u(!1),Z=u(!1),V=u(null),G=u([]),N=u(!1),H=u(!1),q=u([]),X=u(!1);let _=null;const Y=()=>{_&&clearInterval(_),ot(),_=setInterval(()=>{ot()},2e3)},$=()=>{_&&(clearInterval(_),_=null)},ht=()=>{x.value=x.value==="dark"?"light":"dark",localStorage.setItem("dmux-theme",x.value),document.documentElement.setAttribute("data-theme",x.value)},mt=s=>{A.value.has(s)?A.value.delete(s):A.value.add(s),A.value=new Set(A.value)},yt=()=>{K.value=!0,T.value="",C.value=null,E.value=[],L.value=!1,j.value="prompt",I(()=>{const l=document.getElementById("pane-prompt");l&&l.focus()});const s=l=>{l.key==="Escape"&&(tt(),document.removeEventListener("keydown",s))};document.addEventListener("keydown",s)},tt=()=>{K.value=!1,T.value="",C.value=null,E.value=[],L.value=!1,j.value="prompt"},et=async()=>{if(!(j.value==="prompt"&&!T.value.trim())&&!(j.value==="agent"&&!C.value))try{Q.value=!0;const s={prompt:T.value.trim()};C.value&&(s.agent=C.value);const d=await(await fetch("/api/panes",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(s)})).json();d.needsAgentChoice?(L.value=!0,E.value=d.availableAgents,j.value="agent"):tt()}catch(s){console.error("Failed to create pane:",s),alert("Failed to create pane")}finally{Q.value=!1}},kt=s=>{C.value=s,et()},ct=s=>{U.value=s.projectName||"dmux",e.value=s.sessionName||"",O.value=!0,F.value=s.panes||[],o.value=new Date,p.value="Just now";for(const l of F.value)W.value[l.id]||dt(l.id)},ot=async()=>{try{const l=await(await fetch("/api/panes")).json();ct(l)}catch(s){console.error("Failed to fetch panes:",s),O.value=!1}},dt=async s=>{try{const d=await(await fetch(\`/api/panes/\${s}/actions\`)).json();W.value[s]=d.actions||[]}catch(l){console.error(\`Failed to fetch actions for pane \${s}:\`,l)}},ft=s=>{S.value===s?S.value=null:S.value=s},bt=async(s,l)=>{var d;try{R.value=!0,S.value=null;const v=await(await fetch(\`/api/panes/\${s.id}/actions/\${l.id}\`,{method:"POST"})).json();if(v.requiresInteraction){let m={};v.interactionType==="confirm"?m={type:"confirm",title:v.title||"Confirm",message:v.message,...v.confirmData}:v.interactionType==="choice"?m={type:"choice",title:v.title||"Choose",message:v.message,...v.choiceData}:v.interactionType==="input"&&(m={type:"input",title:v.title||"Input",message:v.message,...v.inputData,inputValue:((d=v.inputData)==null?void 0:d.defaultValue)||""},I(()=>{const nt=document.querySelector(".dialog-input");nt&&(nt.focus(),nt.select())})),m.paneId=s.id,k.value=m}}catch(c){console.error("Failed to execute action:",c),alert("Failed to execute action")}finally{R.value=!1}},B=()=>{k.value=null},_t=async s=>{var l;if(k.value)try{D.value=!0;const c=await(await fetch(\`/api/callbacks/confirm/\${k.value.callbackId}\`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({confirmed:s})})).json();if(c.requiresInteraction){let v={};c.interactionType==="confirm"?v={type:"confirm",title:c.title||"Confirm",message:c.message,...c.confirmData}:c.interactionType==="choice"?v={type:"choice",title:c.title||"Choose",message:c.message,...c.choiceData}:c.interactionType==="input"&&(v={type:"input",title:c.title||"Input",message:c.message,...c.inputData,inputValue:((l=c.inputData)==null?void 0:l.defaultValue)||""},I(()=>{const m=document.querySelector(".dialog-input");m&&(m.focus(),m.select())})),k.value=v}else B()}catch(d){console.error("Failed to confirm action:",d),alert("Failed to complete action")}finally{D.value=!1}},wt=async s=>{var l;if(k.value)try{D.value=!0;const c=await(await fetch(\`/api/callbacks/choice/\${k.value.callbackId}\`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({optionId:s})})).json();if(c.requiresInteraction){let v={};c.interactionType==="confirm"?v={type:"confirm",title:c.title||"Confirm",message:c.message,...c.confirmData}:c.interactionType==="choice"?v={type:"choice",title:c.title||"Choose",message:c.message,...c.choiceData}:c.interactionType==="input"&&(v={type:"input",title:c.title||"Input",message:c.message,...c.inputData,inputValue:((l=c.inputData)==null?void 0:l.defaultValue)||""},I(()=>{const m=document.querySelector(".dialog-input");m&&(m.focus(),m.select())})),k.value=v}else B()}catch(d){console.error("Failed to select choice:",d),alert("Failed to complete action")}finally{D.value=!1}},Ct=s=>s.split(\`
4790
- \`).map(l=>{if(l.includes("|")){const d=l.split("|");if(d.length===2){const c=d[0],m=d[1].replace(/\\+/g,'<span style="color: #4ade80;">+</span>').replace(/-/g,'<span style="color: #f87171;">-</span>');return c+'<span style="opacity: 0.6;">|</span>'+m}}return l}).join("<br>"),St=async()=>{var s;if(k.value)try{D.value=!0;const d=await(await fetch(\`/api/callbacks/input/\${k.value.callbackId}\`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({value:k.value.inputValue})})).json();if(d.requiresInteraction){let c={};d.interactionType==="confirm"?c={type:"confirm",title:d.title||"Confirm",message:d.message,...d.confirmData}:d.interactionType==="choice"?c={type:"choice",title:d.title||"Choose",message:d.message,...d.choiceData}:d.interactionType==="input"&&(c={type:"input",title:d.title||"Input",message:d.message,...d.inputData,inputValue:((s=d.inputData)==null?void 0:s.defaultValue)||""},I(()=>{const v=document.querySelector(".dialog-input");v&&(v.focus(),v.select())})),k.value=c}else B()}catch(l){console.error("Failed to submit input:",l),alert("Failed to complete action")}finally{D.value=!1}},Dt=async(s,l)=>{try{b.value.add(s.id),b.value=new Set(b.value);const d=l.keys||[l.action];for(const c of d)await fetch(\`/api/keys/\${s.id}\`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({key:c})});setTimeout(()=>{b.value.delete(s.id),b.value=new Set(b.value)},1500)}catch(d){console.error("Failed to select option:",d),b.value.delete(s.id),b.value=new Set(b.value)}},Pt=async s=>{const l=h.value[s.id];if(!(!l||!l.trim()))try{f.value.add(s.id),f.value=new Set(f.value);for(const d of l)await fetch(\`/api/keys/\${s.id}\`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({key:d})});await fetch(\`/api/keys/\${s.id}\`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({key:"Enter"})}),J.value[s.id]=l.substring(0,50)+(l.length>50?"...":""),h.value[s.id]="",setTimeout(()=>{delete J.value[s.id],f.value.delete(s.id),f.value=new Set(f.value)},3e3)}catch(d){console.error("Failed to send prompt:",d),f.value.delete(s.id),f.value=new Set(f.value)}},xt=s=>{const l=s.target;l.style.height="auto",l.style.height=l.scrollHeight+"px"},At=async()=>{try{N.value=!0;const l=await(await fetch("/api/settings")).json();V.value=l.settings,G.value=l.definitions,Z.value=!0,H.value=!1}catch(s){console.error("Failed to load settings:",s),alert("Failed to load settings")}finally{N.value=!1}},rt=()=>{Z.value=!1,V.value=null,G.value=[],H.value=!1,q.value=[]},Tt=async(s,l,d)=>{try{N.value=!0,await fetch("/api/settings",{method:"PATCH",headers:{"Content-Type":"application/json"},body:JSON.stringify({key:s,value:l,scope:d})});const v=await(await fetch("/api/settings")).json();V.value=v.settings}catch(c){console.error("Failed to update setting:",c),alert("Failed to update setting")}finally{N.value=!1}},jt=async()=>{try{X.value=!0;const l=await(await fetch("/api/hooks")).json();q.value=l.hooks||[],H.value=!0}catch(s){console.error("Failed to load hooks:",s),alert("Failed to load hooks")}finally{X.value=!1}},gt=()=>{H.value=!1,q.value=[]},It=async()=>{const s="I would like to edit my dmux hooks in .dmux-hooks, please read the instructions in there and ask me what I want to edit";gt(),rt(),T.value=s,await I(),await et()},st=s=>{const l=s.target;S.value&&!l.closest(".action-menu-btn")&&!l.closest(".action-menu-dropdown")&&(S.value=null)};zt(()=>{document.documentElement.setAttribute("data-theme",x.value),Y(),document.addEventListener("click",st),document.addEventListener("visibilitychange",()=>{document.hidden?$():Y()})}),Nt(()=>{$(),document.removeEventListener("click",st)});const ut={projectName:U,sessionName:e,connected:O,panes:F,lastUpdate:o,timeSinceUpdate:p,promptInputs:h,sendingPrompts:f,queuedMessages:J,theme:x,expandedPrompts:A,loadingOptions:b,showCreateDialog:K,newPanePrompt:T,newPaneAgent:C,creatingPane:Q,availableAgents:E,needsAgentChoice:L,createStep:j,actions:pt,paneActions:W,showActionMenu:S,actionDialog:k,executingAction:R,actionDialogLoading:D,showSettingsDialog:Z,settingsData:V,settingDefinitions:G,loadingSettings:N,showHooksSection:H,hooksData:q,loadingHooks:X,get pollingInterval(){return _},set pollingInterval(s){_=s},startPolling:Y,stopPolling:$,toggleTheme:ht,togglePrompt:mt,openCreateDialog:yt,closeCreateDialog:tt,createPane:et,selectAgent:kt,updatePanesFromData:ct,fetchPanes:ot,fetchPaneActions:dt,toggleActionMenu:ft,executeAction:bt,closeActionDialog:B,confirmAction:_t,selectChoice:wt,colorizeDiffStat:Ct,submitInput:St,selectOption:Dt,sendPrompt:Pt,autoExpand:xt,openSettingsDialog:At,closeSettingsDialog:rt,updateSetting:Tt,openHooksSection:jt,closeHooksSection:gt,editHooksWithAgent:It,handleClickOutside:st};return Object.defineProperty(ut,"__isScriptSetup",{enumerable:!1,value:!0}),ut}}),Lt={class:"session-info"},Vt=["title"],qt={key:0,xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24"},Bt={key:1,xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24"},Ut={key:0},Jt={class:"container"},Kt={class:"actions-bar"},Qt=["disabled"],Wt=["disabled"],Rt={key:0,class:"no-panes"},Zt={key:1,class:"panes-grid"},Gt={class:"pane-header"},Xt={class:"pane-header-content"},Yt=["href"],$t={class:"pane-title"},te={class:"pane-meta"},ee={key:0,class:"pane-autopilot",title:"Autopilot enabled"},oe={class:"pane-id"},se=["onClick"],ne={key:0,class:"action-menu-dropdown"},ae=["onClick","disabled"],ie={class:"action-icon"},le={class:"action-label"},ce={class:"pane-prompt-section"},de=["onClick"],re={class:"prompt-header"},ge={class:"expand-icon"},ue={class:"prompt-text"},ve={key:0,class:"pane-prompt-full"},pe={key:1,class:"agent-summary"},he={key:2,class:"analyzer-error"},me={key:0,class:"options-dialog"},ye={class:"options-question"},ke={key:0,class:"options-warning"},fe={key:1,class:"analyzing-state"},be={key:2,class:"options-buttons"},_e=["onClick","disabled"],we={class:"prompt-input-wrapper"},Ce=["onUpdate:modelValue","placeholder","disabled"],Se=["onClick","disabled","title"],De={key:0,class:"button-loader"},Pe={key:1,xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 988.44 1200.05"},xe={key:0,class:"queued-message"},Ae={key:3,class:"dev-server-status"},Te=["href"],je={class:"action-dialog"},Ie={key:0},Me=["onKeydown"],ze={class:"dialog-buttons"},Ne=["disabled"],He={key:1},Oe={class:"agent-choices"},Fe=["onClick","disabled"],Ee={key:0,class:"action-dialog"},Le={key:0,class:"dialog-loading"},Ve={key:1,class:"dialog-buttons"},qe=["disabled"],Be=["disabled"],Ue={class:"action-dialog"},Je={key:0},Ke={key:1,class:"dialog-loading"},Qe={key:2},We={class:"choice-options"},Re=["onClick","disabled"],Ze={class:"choice-label"},Ge={key:0,class:"choice-description"},Xe={class:"dialog-buttons"},Ye=["disabled"],$e={class:"action-dialog"},to=["innerHTML"],eo={key:1,class:"dialog-loading"},oo={key:2},so=["placeholder"],no={class:"dialog-buttons"},ao=["disabled"],io=["disabled"],lo={class:"action-dialog settings-dialog"},co={key:0,class:"dialog-loading"},ro={key:1,class:"settings-list"},go={class:"setting-header"},uo={class:"setting-info"},vo={class:"setting-label"},po={class:"setting-description"},ho={key:0,class:"setting-control"},mo={class:"setting-value"},yo={key:0,class:"setting-scope"},ko={key:1,class:"setting-scope"},fo={class:"setting-buttons"},bo=["onClick","disabled"],_o=["onClick","disabled"],wo=["onClick","disabled"],Co=["onClick","disabled"],So={class:"setting-control"},Do={class:"setting-value"},Po={key:0,class:"setting-scope"},xo={key:1,class:"setting-scope"},Ao={class:"setting-option-label"},To={class:"setting-buttons"},jo=["onClick","disabled"],Io=["onClick","disabled"],Mo={class:"setting-control"},zo={class:"setting-buttons"},No=["disabled"],Ho={key:2,class:"hooks-section"},Oo={key:0,class:"dialog-loading"},Fo={key:1,class:"hooks-list"},Eo={class:"hook-name"};function Lo(vt,i,U,e,O,F){return a(),n(y,null,[t("header",null,[i[7]||(i[7]=t("img",{src:"https://cdn.formk.it/dmux/dmux.png",alt:"dmux",class:"logo"},null,-1)),t("h1",null,g(e.projectName),1),t("div",Lt,[t("button",{onClick:e.toggleTheme,class:"theme-toggle",title:e.theme==="dark"?"Switch to light mode":"Switch to dark mode"},[e.theme==="dark"?(a(),n("svg",qt,[...i[5]||(i[5]=[t("path",{d:"M12 2.25a.75.75 0 01.75.75v2.25a.75.75 0 01-1.5 0V3a.75.75 0 01.75-.75zM7.5 12a4.5 4.5 0 119 0 4.5 4.5 0 01-9 0zM18.894 6.166a.75.75 0 00-1.06-1.06l-1.591 1.59a.75.75 0 101.06 1.061l1.591-1.59zM21.75 12a.75.75 0 01-.75.75h-2.25a.75.75 0 010-1.5H21a.75.75 0 01.75.75zM17.834 18.894a.75.75 0 001.06-1.06l-1.59-1.591a.75.75 0 10-1.061 1.06l1.59 1.591zM12 18a.75.75 0 01.75.75V21a.75.75 0 01-1.5 0v-2.25A.75.75 0 0112 18zM7.758 17.303a.75.75 0 00-1.061-1.06l-1.591 1.59a.75.75 0 001.06 1.061l1.591-1.59zM6 12a.75.75 0 01-.75.75H3a.75.75 0 010-1.5h2.25A.75.75 0 016 12zM6.697 7.757a.75.75 0 001.06-1.06l-1.59-1.591a.75.75 0 00-1.061 1.06l1.59 1.591z"},null,-1)])])):(a(),n("svg",Bt,[...i[6]||(i[6]=[t("path",{d:"M9.528 1.718a.75.75 0 01.162.819A8.97 8.97 0 009 6.5a9 9 0 009 9 8.97 8.97 0 003.963-.69.75.75 0 01.981.98 10.503 10.503 0 01-9.694 6.46c-5.799 0-10.5-4.701-10.5-10.5 0-4.368 2.667-8.112 6.46-9.694a.75.75 0 01.818.162z"},null,-1)])]))],8,Vt),e.sessionName?(a(),n("span",Ut,g(e.sessionName),1)):r("v-if",!0),t("span",{class:"status-indicator",style:Ot({color:e.connected?"#4ade80":"#f87171"})},"●",4)])]),t("div",Jt,[t("main",null,[t("div",Kt,[t("button",{onClick:e.openCreateDialog,class:"create-pane-button",disabled:e.creatingPane},[...i[8]||(i[8]=[t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",fill:"currentColor"},[t("path",{d:"M12 4.5v15m7.5-7.5h-15",stroke:"currentColor","stroke-width":"2","stroke-linecap":"round","stroke-linejoin":"round"})],-1),P(" Create New Pane ",-1)])],8,Qt),t("button",{onClick:e.openSettingsDialog,class:"settings-button",disabled:e.loadingSettings,title:"Settings"},[...i[9]||(i[9]=[t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",fill:"currentColor"},[t("path",{d:"M10.5 1.875a1.125 1.125 0 012.25 0v.563c0 1.018.84 1.843 1.854 1.839a.75.75 0 01.585.217l.4.4a.75.75 0 01.216.585c-.004 1.014.821 1.854 1.839 1.854h.563a1.125 1.125 0 010 2.25h-.563c-1.018 0-1.843.84-1.839 1.854a.75.75 0 01-.217.585l-.4.4a.75.75 0 01-.585.216c-1.014-.004-1.854.821-1.854 1.839v.563a1.125 1.125 0 01-2.25 0v-.563c0-1.018-.84-1.843-1.854-1.839a.75.75 0 01-.585-.217l-.4-.4a.75.75 0 01-.216-.585c.004-1.014-.821-1.854-1.839-1.854H3.75a1.125 1.125 0 010-2.25h.563c1.018 0 1.843-.84 1.839-1.854a.75.75 0 01.217-.585l.4-.4a.75.75 0 01.585-.216c1.014.004 1.854-.821 1.854-1.839V1.875zM12 15a3 3 0 100-6 3 3 0 000 6z"})],-1),P(" Settings ",-1)])],8,Wt)]),e.panes.length===0?(a(),n("div",Rt,[...i[10]||(i[10]=[t("p",null,"No dmux panes active",-1),t("p",{class:"hint"},\`Click "Create New Pane" above or press 'n' in dmux\`,-1)])])):(a(),n("div",Zt,[(a(!0),n(y,null,w(e.panes,o=>(a(),n("div",{key:o.id,class:"pane-card"},[t("div",Gt,[t("div",Xt,[t("a",{href:"/panes/"+o.id,class:"pane-title-link"},[t("span",$t,g(o.slug),1),i[11]||(i[11]=t("span",{class:"pane-arrow"},"→",-1))],8,Yt),t("div",te,[o.autopilot?(a(),n("span",ee,"🤖")):r("v-if",!0),t("span",{class:z(["pane-agent",o.agent||""])},g(o.agent||"unknown"),3),t("span",oe,g(o.paneId),1)])]),t("button",{onClick:p=>e.toggleActionMenu(o.id),class:"action-menu-btn",title:"Actions"},[...i[12]||(i[12]=[t("span",null,"⋮",-1)])],8,se)]),r(" Action Menu Dropdown "),e.showActionMenu===o.id&&e.paneActions[o.id]?(a(),n("div",ne,[(a(!0),n(y,null,w(e.paneActions[o.id],p=>(a(),n("button",{key:p.id,onClick:h=>e.executeAction(o,p),class:"action-menu-item",disabled:e.executingAction},[t("span",ie,g(p.icon||"•"),1),t("span",le,g(p.label),1)],8,ae))),128))])):r("v-if",!0),t("div",ce,[t("div",{class:z(["pane-prompt-preview",{expanded:e.expandedPrompts.has(o.id)}]),onClick:p=>e.togglePrompt(o.id)},[t("div",re,[i[13]||(i[13]=t("span",{class:"prompt-label"},"Initial Prompt",-1)),t("span",ge,g(e.expandedPrompts.has(o.id)?"▼":"▶"),1)]),t("span",ue,g(o.prompt||"No prompt"),1)],10,de),e.expandedPrompts.has(o.id)?(a(),n("div",ve,g(o.prompt||"No prompt"),1)):r("v-if",!0)]),r(" Show agent summary when idle "),o.agentStatus==="idle"&&o.agentSummary?(a(),n("div",pe,g(o.agentSummary),1)):r("v-if",!0),r(" Show analyzer error if present "),o.analyzerError?(a(),n("div",he," ⚠ "+g(o.analyzerError),1)):r("v-if",!0),t("div",{class:"pane-interactive",onClick:i[0]||(i[0]=M(()=>{},["prevent"]))},[r(" Options Dialog (when waiting with options) "),o.agentStatus==="waiting"&&o.options&&o.options.length>0?(a(),n("div",me,[t("div",ye,g(o.optionsQuestion||"Choose an option:"),1),o.potentialHarm&&o.potentialHarm.hasRisk?(a(),n("div",ke," ⚠️ "+g(o.potentialHarm.description),1)):r("v-if",!0),e.loadingOptions.has(o.id)?(a(),n("div",fe,[...i[14]||(i[14]=[t("div",{class:"loader-spinner"},null,-1),t("span",null,"Processing selection...",-1)])])):(a(),n("div",be,[(a(!0),n(y,null,w(o.options,p=>(a(),n("button",{key:p.action,onClick:h=>e.selectOption(o,p),class:z(["option-button",{"option-button-danger":o.potentialHarm&&o.potentialHarm.hasRisk}]),disabled:e.loadingOptions.has(o.id)},g(p.action),11,_e))),128))]))])):o.agentStatus==="analyzing"?(a(),n(y,{key:1},[r(" Analyzing (show loader) "),i[15]||(i[15]=t("div",{class:"analyzing-state"},[t("div",{class:"loader-spinner"}),t("span",null,"Analyzing...")],-1))],2112)):(a(),n(y,{key:2},[r(" Working/Idle (show prompt input) "),t("div",null,[t("div",we,[at(t("textarea",{"onUpdate:modelValue":p=>e.promptInputs[o.id]=p,onInput:e.autoExpand,placeholder:o.agentStatus==="working"?"Queue a prompt...":"Send a prompt...",disabled:e.sendingPrompts.has(o.id),class:"prompt-textarea",rows:"1"},null,40,Ce),[[it,e.promptInputs[o.id]]]),t("button",{onClick:p=>e.sendPrompt(o),disabled:!e.promptInputs[o.id]||e.sendingPrompts.has(o.id),class:"send-button",title:o.agentStatus==="working"?"Queue prompt":"Send prompt"},[e.sendingPrompts.has(o.id)?(a(),n("span",De)):(a(),n("svg",Pe,[...i[16]||(i[16]=[t("path",{d:"M425.13,28.37L30.09,423.41C11.19,441.37.34,466.2,0,492.27c-.34,26.07,9.86,51.17,28.29,69.61,18.43,18.45,43.52,28.67,69.59,28.35,26.07-.31,50.91-11.14,68.88-30.02l233.16-233.52v776.64c0,34.56,18.43,66.48,48.36,83.76,29.93,17.28,66.8,17.28,96.72,0,29.93-17.28,48.36-49.21,48.36-83.76V328.85l231.72,231.36c24.63,23.41,59.74,32.18,92.48,23.09,32.74-9.08,58.32-34.68,67.38-67.43,9.05-32.75.25-67.85-23.18-92.46L566.73,28.37C548.63,10.16,524-.04,498.33.05c-.8-.06-1.6-.06-2.4,0-.8-.06-1.6-.06-2.4,0-25.65,0-50.25,10.19-68.4,28.32h0Z"},null,-1)])]))],8,Se)]),e.queuedMessages[o.id]?(a(),n("div",xe," ✓ "+g(e.queuedMessages[o.id]),1)):r("v-if",!0)])],2112))]),o.devStatus&&o.devStatus!=="stopped"?(a(),n("div",Ae,[i[17]||(i[17]=t("span",{class:"status-label"},"Dev Server:",-1)),t("span",{class:z(["status-badge",o.devStatus])},g(o.devStatus),3),o.devUrl?(a(),n("a",{key:0,href:o.devUrl,target:"_blank",class:"dev-link"},"↗",8,Te)):r("v-if",!0)])):r("v-if",!0)]))),128))]))]),r(" Create Pane Dialog "),e.showCreateDialog?(a(),n("div",{key:0,class:"action-dialog-overlay",onClick:M(e.closeCreateDialog,["self"])},[t("div",je,[i[21]||(i[21]=t("h3",null,"Create New Pane",-1)),e.createStep==="prompt"?(a(),n("div",Ie,[i[18]||(i[18]=t("label",{for:"pane-prompt"},"Provide an initial prompt for your agent",-1)),at(t("textarea",{id:"pane-prompt","onUpdate:modelValue":i[1]||(i[1]=o=>e.newPanePrompt=o),placeholder:"E.g., Fix the authentication bug, Add dark mode, etc.",rows:"4",onKeydown:[lt(M(e.createPane,["meta"]),["enter"]),lt(M(e.createPane,["ctrl"]),["enter"])]},null,40,Me),[[it,e.newPanePrompt]]),i[19]||(i[19]=t("div",{class:"dialog-hint"},[P(" 💡 Press "),t("kbd",null,"⌘ Enter"),P(" or "),t("kbd",null,"Ctrl Enter"),P(" to create ")],-1)),t("div",ze,[t("button",{onClick:e.closeCreateDialog,class:"dialog-btn"},"Cancel"),t("button",{onClick:e.createPane,disabled:!e.newPanePrompt.trim()||e.creatingPane,class:"dialog-btn dialog-btn-primary"},g(e.creatingPane?"Creating...":"Create Pane"),9,Ne)])])):e.createStep==="agent"?(a(),n("div",He,[i[20]||(i[20]=t("p",null,"Multiple agents available. Choose one:",-1)),t("div",Oe,[(a(!0),n(y,null,w(e.availableAgents,o=>(a(),n("button",{key:o,onClick:p=>e.selectAgent(o),class:"agent-choice-button",disabled:e.creatingPane},g(o),9,Fe))),128))]),t("div",{class:"dialog-buttons"},[t("button",{onClick:e.closeCreateDialog,class:"dialog-btn"},"Cancel")])])):r("v-if",!0)])])):r("v-if",!0),r(" Action Dialogs "),e.actionDialog?(a(),n("div",{key:1,class:"action-dialog-overlay",onClick:M(e.closeActionDialog,["self"])},[r(" Confirm Dialog "),e.actionDialog.type==="confirm"?(a(),n("div",Ee,[t("h3",null,g(e.actionDialog.title),1),t("p",null,g(e.actionDialog.message),1),e.actionDialogLoading?(a(),n("div",Le,[...i[22]||(i[22]=[t("div",{class:"loader-spinner"},null,-1),t("span",null,"Processing...",-1)])])):(a(),n("div",Ve,[t("button",{onClick:i[2]||(i[2]=o=>e.confirmAction(!1)),class:"dialog-btn",disabled:e.actionDialogLoading},"Cancel",8,qe),t("button",{onClick:i[3]||(i[3]=o=>e.confirmAction(!0)),class:"dialog-btn dialog-btn-primary",disabled:e.actionDialogLoading},"Confirm",8,Be)]))])):e.actionDialog.type==="choice"?(a(),n(y,{key:1},[r(" Choice Dialog "),t("div",Ue,[t("h3",null,g(e.actionDialog.title),1),e.actionDialog.message?(a(),n("p",Je,g(e.actionDialog.message),1)):r("v-if",!0),e.actionDialogLoading?(a(),n("div",Ke,[...i[23]||(i[23]=[t("div",{class:"loader-spinner"},null,-1),t("span",null,"Processing...",-1)])])):(a(),n("div",Qe,[t("div",We,[(a(!0),n(y,null,w(e.actionDialog.options,o=>(a(),n("button",{key:o.id,onClick:p=>e.selectChoice(o.id),class:z(["choice-option-btn",{danger:o.danger}]),disabled:e.actionDialogLoading},[t("div",Ze,g(o.label),1),o.description?(a(),n("div",Ge,g(o.description),1)):r("v-if",!0)],10,Re))),128))]),t("div",Xe,[t("button",{onClick:e.closeActionDialog,class:"dialog-btn",disabled:e.actionDialogLoading},"Cancel",8,Ye)])]))])],2112)):e.actionDialog.type==="input"?(a(),n(y,{key:2},[r(" Input Dialog "),t("div",$e,[t("h3",null,g(e.actionDialog.title),1),e.actionDialog.message?(a(),n("div",{key:0,class:"dialog-message",innerHTML:e.colorizeDiffStat(e.actionDialog.message)},null,8,to)):r("v-if",!0),e.actionDialogLoading?(a(),n("div",eo,[...i[24]||(i[24]=[t("div",{class:"loader-spinner"},null,-1),t("span",null,"Processing...",-1)])])):(a(),n("div",oo,[at(t("input",{type:"text","onUpdate:modelValue":i[4]||(i[4]=o=>e.actionDialog.inputValue=o),placeholder:e.actionDialog.placeholder,class:"dialog-input",onKeydown:lt(e.submitInput,["enter"])},null,40,so),[[it,e.actionDialog.inputValue]]),t("div",no,[t("button",{onClick:e.closeActionDialog,class:"dialog-btn",disabled:e.actionDialogLoading},"Cancel",8,ao),t("button",{onClick:e.submitInput,class:"dialog-btn dialog-btn-primary",disabled:e.actionDialogLoading},"Submit",8,io)])]))])],2112)):r("v-if",!0)])):r("v-if",!0),r(" Settings Dialog "),e.showSettingsDialog&&e.settingsData?(a(),n("div",{key:2,class:"action-dialog-overlay",onClick:M(e.closeSettingsDialog,["self"])},[t("div",lo,[i[30]||(i[30]=t("h3",null,"Settings",-1)),e.loadingSettings?(a(),n("div",co,[...i[25]||(i[25]=[t("div",{class:"loader-spinner"},null,-1),t("span",null,"Loading...",-1)])])):(a(),n("div",ro,[(a(!0),n(y,null,w(e.settingDefinitions,o=>{var p;return a(),n("div",{key:o.key,class:"setting-item"},[t("div",go,[t("div",uo,[t("div",vo,g(o.label),1),t("div",po,g(o.description),1)])]),r(" Boolean setting "),o.type==="boolean"?(a(),n("div",ho,[t("div",mo,[i[26]||(i[26]=P(" Current: ",-1)),t("strong",null,g(e.settingsData.merged[o.key]?"Enabled":"Disabled"),1),o.key in e.settingsData.project?(a(),n("span",yo,"(project)")):o.key in e.settingsData.global?(a(),n("span",ko,"(global)")):r("v-if",!0)]),t("div",fo,[t("button",{onClick:h=>e.updateSetting(o.key,!0,"global"),class:"setting-btn",disabled:e.loadingSettings}," Enable (global) ",8,bo),t("button",{onClick:h=>e.updateSetting(o.key,!1,"global"),class:"setting-btn",disabled:e.loadingSettings}," Disable (global) ",8,_o),t("button",{onClick:h=>e.updateSetting(o.key,!0,"project"),class:"setting-btn",disabled:e.loadingSettings}," Enable (project) ",8,wo),t("button",{onClick:h=>e.updateSetting(o.key,!1,"project"),class:"setting-btn",disabled:e.loadingSettings}," Disable (project) ",8,Co)])])):o.type==="select"?(a(),n(y,{key:1},[r(" Select setting "),t("div",So,[t("div",Do,[i[27]||(i[27]=P(" Current: ",-1)),t("strong",null,g(((p=o.options.find(h=>h.value===e.settingsData.merged[o.key]))==null?void 0:p.label)||"Not set"),1),o.key in e.settingsData.project?(a(),n("span",Po,"(project)")):o.key in e.settingsData.global?(a(),n("span",xo,"(global)")):r("v-if",!0)]),(a(!0),n(y,null,w(o.options,h=>(a(),n("div",{class:"setting-select-group",key:h.value},[t("div",Ao,g(h.label),1),t("div",To,[t("button",{onClick:f=>e.updateSetting(o.key,h.value,"global"),class:"setting-btn",disabled:e.loadingSettings}," Set global ",8,jo),t("button",{onClick:f=>e.updateSetting(o.key,h.value,"project"),class:"setting-btn",disabled:e.loadingSettings}," Set project ",8,Io)])]))),128))])],2112)):o.type==="action"?(a(),n(y,{key:2},[r(" Action setting "),t("div",Mo,[t("div",zo,[t("button",{onClick:e.openHooksSection,class:"setting-btn setting-btn-action",disabled:e.loadingSettings}," Open ",8,No)])])],2112)):r("v-if",!0)])}),128))])),r(" Hooks Section (shown when hooks action is triggered) "),e.showHooksSection?(a(),n("div",Ho,[t("div",{class:"hooks-header"},[t("button",{onClick:e.closeHooksSection,class:"back-btn",title:"Back to settings"},"← Back"),i[28]||(i[28]=t("h4",null,"Hooks Management",-1))]),e.loadingHooks?(a(),n("div",Oo,[...i[29]||(i[29]=[t("div",{class:"loader-spinner"},null,-1),t("span",null,"Loading hooks...",-1)])])):(a(),n("div",Fo,[(a(!0),n(y,null,w(e.hooksData,o=>(a(),n("div",{key:o.name,class:"hook-item"},[t("div",Eo,g(o.name),1),t("div",{class:z(["hook-status",{"hook-active":o.active}])},g(o.active?"✓ Active":"Inactive"),3)]))),128)),t("div",{class:"hooks-actions"},[t("button",{onClick:e.editHooksWithAgent,class:"dialog-btn dialog-btn-primary"}," Edit Hooks with Agent ")])]))])):r("v-if",!0),t("div",{class:"dialog-buttons"},[t("button",{onClick:e.closeSettingsDialog,class:"dialog-btn dialog-btn-primary"},"Close")])])])):r("v-if",!0)])],64)}const Vo=Ht(Et,[["render",Lo],["__file","/Users/justinschroeder/Projects/dmux/frontend/src/components/Dashboard.vue"]]),qo=Ft(Vo);qo.mount("#app");
5516
+ \`).map(l=>{if(l.includes("|")){const d=l.split("|");if(d.length===2){const c=d[0],m=d[1].replace(/\\+/g,'<span style="color: #4ade80;">+</span>').replace(/-/g,'<span style="color: #f87171;">-</span>');return c+'<span style="opacity: 0.6;">|</span>'+m}}return l}).join("<br>"),St=async()=>{var s;if(k.value)try{D.value=!0;const d=await(await fetch(\`/api/callbacks/input/\${k.value.callbackId}\`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({value:k.value.inputValue})})).json();if(d.requiresInteraction){let c={};d.interactionType==="confirm"?c={type:"confirm",title:d.title||"Confirm",message:d.message,...d.confirmData}:d.interactionType==="choice"?c={type:"choice",title:d.title||"Choose",message:d.message,...d.choiceData}:d.interactionType==="input"&&(c={type:"input",title:d.title||"Input",message:d.message,...d.inputData,inputValue:((s=d.inputData)==null?void 0:s.defaultValue)||""},I(()=>{const v=document.querySelector(".dialog-input");v&&(v.focus(),v.select())})),k.value=c}else B()}catch(l){console.error("Failed to submit input:",l),alert("Failed to complete action")}finally{D.value=!1}},Dt=async(s,l)=>{try{b.value.add(s.id),b.value=new Set(b.value);const d=l.keys||[l.action];for(const c of d)await fetch(\`/api/keys/\${s.id}\`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({key:c})});setTimeout(()=>{b.value.delete(s.id),b.value=new Set(b.value)},1500)}catch(d){console.error("Failed to select option:",d),b.value.delete(s.id),b.value=new Set(b.value)}},Pt=async s=>{const l=h.value[s.id];if(!(!l||!l.trim()))try{f.value.add(s.id),f.value=new Set(f.value);for(const d of l)await fetch(\`/api/keys/\${s.id}\`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({key:d})});await fetch(\`/api/keys/\${s.id}\`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({key:"Enter"})}),J.value[s.id]=l.substring(0,50)+(l.length>50?"...":""),h.value[s.id]="",setTimeout(()=>{delete J.value[s.id],f.value.delete(s.id),f.value=new Set(f.value)},3e3)}catch(d){console.error("Failed to send prompt:",d),f.value.delete(s.id),f.value=new Set(f.value)}},xt=s=>{const l=s.target;l.style.height="auto",l.style.height=l.scrollHeight+"px"},At=async()=>{try{N.value=!0;const l=await(await fetch("/api/settings")).json();V.value=l.settings,G.value=l.definitions,Z.value=!0,H.value=!1}catch(s){console.error("Failed to load settings:",s),alert("Failed to load settings")}finally{N.value=!1}},rt=()=>{Z.value=!1,V.value=null,G.value=[],H.value=!1,q.value=[]},Tt=async(s,l,d)=>{try{N.value=!0,await fetch("/api/settings",{method:"PATCH",headers:{"Content-Type":"application/json"},body:JSON.stringify({key:s,value:l,scope:d})});const v=await(await fetch("/api/settings")).json();V.value=v.settings}catch(c){console.error("Failed to update setting:",c),alert("Failed to update setting")}finally{N.value=!1}},jt=async()=>{try{X.value=!0;const l=await(await fetch("/api/hooks")).json();q.value=l.hooks||[],H.value=!0}catch(s){console.error("Failed to load hooks:",s),alert("Failed to load hooks")}finally{X.value=!1}},gt=()=>{H.value=!1,q.value=[]},It=async()=>{const s="I would like to edit my dmux hooks in .dmux-hooks, please read the instructions in there and ask me what I want to edit";gt(),rt(),T.value=s,await I(),await et()},st=s=>{const l=s.target;S.value&&!l.closest(".action-menu-btn")&&!l.closest(".action-menu-dropdown")&&(S.value=null)};zt(()=>{document.documentElement.setAttribute("data-theme",x.value),Y(),document.addEventListener("click",st),document.addEventListener("visibilitychange",()=>{document.hidden?$():Y()})}),Nt(()=>{$(),document.removeEventListener("click",st)});const ut={projectName:U,sessionName:e,connected:O,panes:F,lastUpdate:o,timeSinceUpdate:p,promptInputs:h,sendingPrompts:f,queuedMessages:J,theme:x,expandedPrompts:A,loadingOptions:b,showCreateDialog:K,newPanePrompt:T,newPaneAgent:C,creatingPane:Q,availableAgents:E,needsAgentChoice:L,createStep:j,actions:pt,paneActions:W,showActionMenu:S,actionDialog:k,executingAction:R,actionDialogLoading:D,showSettingsDialog:Z,settingsData:V,settingDefinitions:G,loadingSettings:N,showHooksSection:H,hooksData:q,loadingHooks:X,get pollingInterval(){return _},set pollingInterval(s){_=s},startPolling:Y,stopPolling:$,toggleTheme:ht,togglePrompt:mt,openCreateDialog:yt,closeCreateDialog:tt,createPane:et,selectAgent:kt,updatePanesFromData:ct,fetchPanes:ot,fetchPaneActions:dt,toggleActionMenu:ft,executeAction:bt,closeActionDialog:B,confirmAction:_t,selectChoice:wt,colorizeDiffStat:Ct,submitInput:St,selectOption:Dt,sendPrompt:Pt,autoExpand:xt,openSettingsDialog:At,closeSettingsDialog:rt,updateSetting:Tt,openHooksSection:jt,closeHooksSection:gt,editHooksWithAgent:It,handleClickOutside:st};return Object.defineProperty(ut,"__isScriptSetup",{enumerable:!1,value:!0}),ut}}),Lt={class:"session-info"},Vt=["title"],qt={key:0,xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24"},Bt={key:1,xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24"},Ut={key:0},Jt={class:"container"},Kt={class:"actions-bar"},Qt=["disabled"],Wt=["disabled"],Rt={key:0,class:"no-panes"},Zt={key:1,class:"panes-grid"},Gt={class:"pane-header"},Xt={class:"pane-header-content"},Yt=["href"],$t={class:"pane-title"},te={class:"pane-meta"},ee={key:0,class:"pane-autopilot",title:"Autopilot enabled"},oe={key:1,class:"pane-agent shell",title:"Shell pane"},se={class:"pane-id"},ne=["onClick"],ae={key:0,class:"action-menu-dropdown"},ie=["onClick","disabled"],le={class:"action-icon"},ce={class:"action-label"},de={class:"pane-prompt-section"},re=["onClick"],ge={class:"prompt-header"},ue={class:"expand-icon"},ve={class:"prompt-text"},pe={key:0,class:"pane-prompt-full"},he={key:1,class:"agent-summary"},me={key:2,class:"analyzer-error"},ye={key:0,class:"options-dialog"},ke={class:"options-question"},fe={key:0,class:"options-warning"},be={key:1,class:"analyzing-state"},_e={key:2,class:"options-buttons"},we=["onClick","disabled"],Ce={class:"prompt-input-wrapper"},Se=["onUpdate:modelValue","placeholder","disabled"],De=["onClick","disabled","title"],Pe={key:0,class:"button-loader"},xe={key:1,xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 988.44 1200.05"},Ae={key:0,class:"queued-message"},Te={key:3,class:"dev-server-status"},je=["href"],Ie={class:"action-dialog"},Me={key:0},ze=["onKeydown"],Ne={class:"dialog-buttons"},He=["disabled"],Oe={key:1},Fe={class:"agent-choices"},Ee=["onClick","disabled"],Le={key:0,class:"action-dialog"},Ve={key:0,class:"dialog-loading"},qe={key:1,class:"dialog-buttons"},Be=["disabled"],Ue=["disabled"],Je={class:"action-dialog"},Ke={key:0},Qe={key:1,class:"dialog-loading"},We={key:2},Re={class:"choice-options"},Ze=["onClick","disabled"],Ge={class:"choice-label"},Xe={key:0,class:"choice-description"},Ye={class:"dialog-buttons"},$e=["disabled"],to={class:"action-dialog"},eo=["innerHTML"],oo={key:1,class:"dialog-loading"},so={key:2},no=["placeholder"],ao={class:"dialog-buttons"},io=["disabled"],lo=["disabled"],co={class:"action-dialog settings-dialog"},ro={key:0,class:"dialog-loading"},go={key:1,class:"settings-list"},uo={class:"setting-header"},vo={class:"setting-info"},po={class:"setting-label"},ho={class:"setting-description"},mo={key:0,class:"setting-control"},yo={class:"setting-value"},ko={key:0,class:"setting-scope"},fo={key:1,class:"setting-scope"},bo={class:"setting-buttons"},_o=["onClick","disabled"],wo=["onClick","disabled"],Co=["onClick","disabled"],So=["onClick","disabled"],Do={class:"setting-control"},Po={class:"setting-value"},xo={key:0,class:"setting-scope"},Ao={key:1,class:"setting-scope"},To={class:"setting-option-label"},jo={class:"setting-buttons"},Io=["onClick","disabled"],Mo=["onClick","disabled"],zo={class:"setting-control"},No={class:"setting-buttons"},Ho=["disabled"],Oo={key:2,class:"hooks-section"},Fo={key:0,class:"dialog-loading"},Eo={key:1,class:"hooks-list"},Lo={class:"hook-name"};function Vo(vt,i,U,e,O,F){return a(),n(y,null,[t("header",null,[i[7]||(i[7]=t("img",{src:"https://cdn.formk.it/dmux/dmux.png",alt:"dmux",class:"logo"},null,-1)),t("h1",null,g(e.projectName),1),t("div",Lt,[t("button",{onClick:e.toggleTheme,class:"theme-toggle",title:e.theme==="dark"?"Switch to light mode":"Switch to dark mode"},[e.theme==="dark"?(a(),n("svg",qt,[...i[5]||(i[5]=[t("path",{d:"M12 2.25a.75.75 0 01.75.75v2.25a.75.75 0 01-1.5 0V3a.75.75 0 01.75-.75zM7.5 12a4.5 4.5 0 119 0 4.5 4.5 0 01-9 0zM18.894 6.166a.75.75 0 00-1.06-1.06l-1.591 1.59a.75.75 0 101.06 1.061l1.591-1.59zM21.75 12a.75.75 0 01-.75.75h-2.25a.75.75 0 010-1.5H21a.75.75 0 01.75.75zM17.834 18.894a.75.75 0 001.06-1.06l-1.59-1.591a.75.75 0 10-1.061 1.06l1.59 1.591zM12 18a.75.75 0 01.75.75V21a.75.75 0 01-1.5 0v-2.25A.75.75 0 0112 18zM7.758 17.303a.75.75 0 00-1.061-1.06l-1.591 1.59a.75.75 0 001.06 1.061l1.591-1.59zM6 12a.75.75 0 01-.75.75H3a.75.75 0 010-1.5h2.25A.75.75 0 016 12zM6.697 7.757a.75.75 0 001.06-1.06l-1.59-1.591a.75.75 0 00-1.061 1.06l1.59 1.591z"},null,-1)])])):(a(),n("svg",Bt,[...i[6]||(i[6]=[t("path",{d:"M9.528 1.718a.75.75 0 01.162.819A8.97 8.97 0 009 6.5a9 9 0 009 9 8.97 8.97 0 003.963-.69.75.75 0 01.981.98 10.503 10.503 0 01-9.694 6.46c-5.799 0-10.5-4.701-10.5-10.5 0-4.368 2.667-8.112 6.46-9.694a.75.75 0 01.818.162z"},null,-1)])]))],8,Vt),e.sessionName?(a(),n("span",Ut,g(e.sessionName),1)):r("v-if",!0),t("span",{class:"status-indicator",style:Ot({color:e.connected?"#4ade80":"#f87171"})},"●",4)])]),t("div",Jt,[t("main",null,[t("div",Kt,[t("button",{onClick:e.openCreateDialog,class:"create-pane-button",disabled:e.creatingPane},[...i[8]||(i[8]=[t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",fill:"currentColor"},[t("path",{d:"M12 4.5v15m7.5-7.5h-15",stroke:"currentColor","stroke-width":"2","stroke-linecap":"round","stroke-linejoin":"round"})],-1),P(" Create New Pane ",-1)])],8,Qt),t("button",{onClick:e.openSettingsDialog,class:"settings-button",disabled:e.loadingSettings,title:"Settings"},[...i[9]||(i[9]=[t("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",fill:"currentColor"},[t("path",{d:"M10.5 1.875a1.125 1.125 0 012.25 0v.563c0 1.018.84 1.843 1.854 1.839a.75.75 0 01.585.217l.4.4a.75.75 0 01.216.585c-.004 1.014.821 1.854 1.839 1.854h.563a1.125 1.125 0 010 2.25h-.563c-1.018 0-1.843.84-1.839 1.854a.75.75 0 01-.217.585l-.4.4a.75.75 0 01-.585.216c-1.014-.004-1.854.821-1.854 1.839v.563a1.125 1.125 0 01-2.25 0v-.563c0-1.018-.84-1.843-1.854-1.839a.75.75 0 01-.585-.217l-.4-.4a.75.75 0 01-.216-.585c.004-1.014-.821-1.854-1.839-1.854H3.75a1.125 1.125 0 010-2.25h.563c1.018 0 1.843-.84 1.839-1.854a.75.75 0 01.217-.585l.4-.4a.75.75 0 01.585-.216c1.014.004 1.854-.821 1.854-1.839V1.875zM12 15a3 3 0 100-6 3 3 0 000 6z"})],-1),P(" Settings ",-1)])],8,Wt)]),e.panes.length===0?(a(),n("div",Rt,[...i[10]||(i[10]=[t("p",null,"No dmux panes active",-1),t("p",{class:"hint"},\`Click "Create New Pane" above or press 'n' in dmux\`,-1)])])):(a(),n("div",Zt,[(a(!0),n(y,null,w(e.panes,o=>(a(),n("div",{key:o.id,class:"pane-card"},[t("div",Gt,[t("div",Xt,[t("a",{href:"/panes/"+o.id,class:"pane-title-link"},[t("span",$t,g(o.slug),1),i[11]||(i[11]=t("span",{class:"pane-arrow"},"→",-1))],8,Yt),t("div",te,[o.autopilot?(a(),n("span",ee,"🤖")):r("v-if",!0),o.type==="shell"?(a(),n("span",oe,g(o.shellType||"shell"),1)):(a(),n("span",{key:2,class:z(["pane-agent",o.agent||""])},g(o.agent||"unknown"),3)),t("span",se,g(o.paneId),1)])]),t("button",{onClick:p=>e.toggleActionMenu(o.id),class:"action-menu-btn",title:"Actions"},[...i[12]||(i[12]=[t("span",null,"⋮",-1)])],8,ne)]),r(" Action Menu Dropdown "),e.showActionMenu===o.id&&e.paneActions[o.id]?(a(),n("div",ae,[(a(!0),n(y,null,w(e.paneActions[o.id],p=>(a(),n("button",{key:p.id,onClick:h=>e.executeAction(o,p),class:"action-menu-item",disabled:e.executingAction},[t("span",le,g(p.icon||"•"),1),t("span",ce,g(p.label),1)],8,ie))),128))])):r("v-if",!0),t("div",de,[t("div",{class:z(["pane-prompt-preview",{expanded:e.expandedPrompts.has(o.id)}]),onClick:p=>e.togglePrompt(o.id)},[t("div",ge,[i[13]||(i[13]=t("span",{class:"prompt-label"},"Initial Prompt",-1)),t("span",ue,g(e.expandedPrompts.has(o.id)?"▼":"▶"),1)]),t("span",ve,g(o.prompt||"No prompt"),1)],10,re),e.expandedPrompts.has(o.id)?(a(),n("div",pe,g(o.prompt||"No prompt"),1)):r("v-if",!0)]),r(" Show agent summary when idle "),o.agentStatus==="idle"&&o.agentSummary?(a(),n("div",he,g(o.agentSummary),1)):r("v-if",!0),r(" Show analyzer error if present "),o.analyzerError?(a(),n("div",me," ⚠ "+g(o.analyzerError),1)):r("v-if",!0),t("div",{class:"pane-interactive",onClick:i[0]||(i[0]=M(()=>{},["prevent"]))},[r(" Options Dialog (when waiting with options) "),o.agentStatus==="waiting"&&o.options&&o.options.length>0?(a(),n("div",ye,[t("div",ke,g(o.optionsQuestion||"Choose an option:"),1),o.potentialHarm&&o.potentialHarm.hasRisk?(a(),n("div",fe," ⚠️ "+g(o.potentialHarm.description),1)):r("v-if",!0),e.loadingOptions.has(o.id)?(a(),n("div",be,[...i[14]||(i[14]=[t("div",{class:"loader-spinner"},null,-1),t("span",null,"Processing selection...",-1)])])):(a(),n("div",_e,[(a(!0),n(y,null,w(o.options,p=>(a(),n("button",{key:p.action,onClick:h=>e.selectOption(o,p),class:z(["option-button",{"option-button-danger":o.potentialHarm&&o.potentialHarm.hasRisk}]),disabled:e.loadingOptions.has(o.id)},g(p.action),11,we))),128))]))])):o.agentStatus==="analyzing"?(a(),n(y,{key:1},[r(" Analyzing (show loader) "),i[15]||(i[15]=t("div",{class:"analyzing-state"},[t("div",{class:"loader-spinner"}),t("span",null,"Analyzing...")],-1))],2112)):(a(),n(y,{key:2},[r(" Working/Idle (show prompt input) "),t("div",null,[t("div",Ce,[at(t("textarea",{"onUpdate:modelValue":p=>e.promptInputs[o.id]=p,onInput:e.autoExpand,placeholder:o.agentStatus==="working"?"Queue a prompt...":"Send a prompt...",disabled:e.sendingPrompts.has(o.id),class:"prompt-textarea",rows:"1"},null,40,Se),[[it,e.promptInputs[o.id]]]),t("button",{onClick:p=>e.sendPrompt(o),disabled:!e.promptInputs[o.id]||e.sendingPrompts.has(o.id),class:"send-button",title:o.agentStatus==="working"?"Queue prompt":"Send prompt"},[e.sendingPrompts.has(o.id)?(a(),n("span",Pe)):(a(),n("svg",xe,[...i[16]||(i[16]=[t("path",{d:"M425.13,28.37L30.09,423.41C11.19,441.37.34,466.2,0,492.27c-.34,26.07,9.86,51.17,28.29,69.61,18.43,18.45,43.52,28.67,69.59,28.35,26.07-.31,50.91-11.14,68.88-30.02l233.16-233.52v776.64c0,34.56,18.43,66.48,48.36,83.76,29.93,17.28,66.8,17.28,96.72,0,29.93-17.28,48.36-49.21,48.36-83.76V328.85l231.72,231.36c24.63,23.41,59.74,32.18,92.48,23.09,32.74-9.08,58.32-34.68,67.38-67.43,9.05-32.75.25-67.85-23.18-92.46L566.73,28.37C548.63,10.16,524-.04,498.33.05c-.8-.06-1.6-.06-2.4,0-.8-.06-1.6-.06-2.4,0-25.65,0-50.25,10.19-68.4,28.32h0Z"},null,-1)])]))],8,De)]),e.queuedMessages[o.id]?(a(),n("div",Ae," ✓ "+g(e.queuedMessages[o.id]),1)):r("v-if",!0)])],2112))]),o.devStatus&&o.devStatus!=="stopped"?(a(),n("div",Te,[i[17]||(i[17]=t("span",{class:"status-label"},"Dev Server:",-1)),t("span",{class:z(["status-badge",o.devStatus])},g(o.devStatus),3),o.devUrl?(a(),n("a",{key:0,href:o.devUrl,target:"_blank",class:"dev-link"},"↗",8,je)):r("v-if",!0)])):r("v-if",!0)]))),128))]))]),r(" Create Pane Dialog "),e.showCreateDialog?(a(),n("div",{key:0,class:"action-dialog-overlay",onClick:M(e.closeCreateDialog,["self"])},[t("div",Ie,[i[21]||(i[21]=t("h3",null,"Create New Pane",-1)),e.createStep==="prompt"?(a(),n("div",Me,[i[18]||(i[18]=t("label",{for:"pane-prompt"},"Provide an initial prompt for your agent",-1)),at(t("textarea",{id:"pane-prompt","onUpdate:modelValue":i[1]||(i[1]=o=>e.newPanePrompt=o),placeholder:"E.g., Fix the authentication bug, Add dark mode, etc.",rows:"4",onKeydown:[lt(M(e.createPane,["meta"]),["enter"]),lt(M(e.createPane,["ctrl"]),["enter"])]},null,40,ze),[[it,e.newPanePrompt]]),i[19]||(i[19]=t("div",{class:"dialog-hint"},[P(" 💡 Press "),t("kbd",null,"⌘ Enter"),P(" or "),t("kbd",null,"Ctrl Enter"),P(" to create ")],-1)),t("div",Ne,[t("button",{onClick:e.closeCreateDialog,class:"dialog-btn"},"Cancel"),t("button",{onClick:e.createPane,disabled:!e.newPanePrompt.trim()||e.creatingPane,class:"dialog-btn dialog-btn-primary"},g(e.creatingPane?"Creating...":"Create Pane"),9,He)])])):e.createStep==="agent"?(a(),n("div",Oe,[i[20]||(i[20]=t("p",null,"Multiple agents available. Choose one:",-1)),t("div",Fe,[(a(!0),n(y,null,w(e.availableAgents,o=>(a(),n("button",{key:o,onClick:p=>e.selectAgent(o),class:"agent-choice-button",disabled:e.creatingPane},g(o),9,Ee))),128))]),t("div",{class:"dialog-buttons"},[t("button",{onClick:e.closeCreateDialog,class:"dialog-btn"},"Cancel")])])):r("v-if",!0)])])):r("v-if",!0),r(" Action Dialogs "),e.actionDialog?(a(),n("div",{key:1,class:"action-dialog-overlay",onClick:M(e.closeActionDialog,["self"])},[r(" Confirm Dialog "),e.actionDialog.type==="confirm"?(a(),n("div",Le,[t("h3",null,g(e.actionDialog.title),1),t("p",null,g(e.actionDialog.message),1),e.actionDialogLoading?(a(),n("div",Ve,[...i[22]||(i[22]=[t("div",{class:"loader-spinner"},null,-1),t("span",null,"Processing...",-1)])])):(a(),n("div",qe,[t("button",{onClick:i[2]||(i[2]=o=>e.confirmAction(!1)),class:"dialog-btn",disabled:e.actionDialogLoading},"Cancel",8,Be),t("button",{onClick:i[3]||(i[3]=o=>e.confirmAction(!0)),class:"dialog-btn dialog-btn-primary",disabled:e.actionDialogLoading},"Confirm",8,Ue)]))])):e.actionDialog.type==="choice"?(a(),n(y,{key:1},[r(" Choice Dialog "),t("div",Je,[t("h3",null,g(e.actionDialog.title),1),e.actionDialog.message?(a(),n("p",Ke,g(e.actionDialog.message),1)):r("v-if",!0),e.actionDialogLoading?(a(),n("div",Qe,[...i[23]||(i[23]=[t("div",{class:"loader-spinner"},null,-1),t("span",null,"Processing...",-1)])])):(a(),n("div",We,[t("div",Re,[(a(!0),n(y,null,w(e.actionDialog.options,o=>(a(),n("button",{key:o.id,onClick:p=>e.selectChoice(o.id),class:z(["choice-option-btn",{danger:o.danger}]),disabled:e.actionDialogLoading},[t("div",Ge,g(o.label),1),o.description?(a(),n("div",Xe,g(o.description),1)):r("v-if",!0)],10,Ze))),128))]),t("div",Ye,[t("button",{onClick:e.closeActionDialog,class:"dialog-btn",disabled:e.actionDialogLoading},"Cancel",8,$e)])]))])],2112)):e.actionDialog.type==="input"?(a(),n(y,{key:2},[r(" Input Dialog "),t("div",to,[t("h3",null,g(e.actionDialog.title),1),e.actionDialog.message?(a(),n("div",{key:0,class:"dialog-message",innerHTML:e.colorizeDiffStat(e.actionDialog.message)},null,8,eo)):r("v-if",!0),e.actionDialogLoading?(a(),n("div",oo,[...i[24]||(i[24]=[t("div",{class:"loader-spinner"},null,-1),t("span",null,"Processing...",-1)])])):(a(),n("div",so,[at(t("input",{type:"text","onUpdate:modelValue":i[4]||(i[4]=o=>e.actionDialog.inputValue=o),placeholder:e.actionDialog.placeholder,class:"dialog-input",onKeydown:lt(e.submitInput,["enter"])},null,40,no),[[it,e.actionDialog.inputValue]]),t("div",ao,[t("button",{onClick:e.closeActionDialog,class:"dialog-btn",disabled:e.actionDialogLoading},"Cancel",8,io),t("button",{onClick:e.submitInput,class:"dialog-btn dialog-btn-primary",disabled:e.actionDialogLoading},"Submit",8,lo)])]))])],2112)):r("v-if",!0)])):r("v-if",!0),r(" Settings Dialog "),e.showSettingsDialog&&e.settingsData?(a(),n("div",{key:2,class:"action-dialog-overlay",onClick:M(e.closeSettingsDialog,["self"])},[t("div",co,[i[30]||(i[30]=t("h3",null,"Settings",-1)),e.loadingSettings?(a(),n("div",ro,[...i[25]||(i[25]=[t("div",{class:"loader-spinner"},null,-1),t("span",null,"Loading...",-1)])])):(a(),n("div",go,[(a(!0),n(y,null,w(e.settingDefinitions,o=>{var p;return a(),n("div",{key:o.key,class:"setting-item"},[t("div",uo,[t("div",vo,[t("div",po,g(o.label),1),t("div",ho,g(o.description),1)])]),r(" Boolean setting "),o.type==="boolean"?(a(),n("div",mo,[t("div",yo,[i[26]||(i[26]=P(" Current: ",-1)),t("strong",null,g(e.settingsData.merged[o.key]?"Enabled":"Disabled"),1),o.key in e.settingsData.project?(a(),n("span",ko,"(project)")):o.key in e.settingsData.global?(a(),n("span",fo,"(global)")):r("v-if",!0)]),t("div",bo,[t("button",{onClick:h=>e.updateSetting(o.key,!0,"global"),class:"setting-btn",disabled:e.loadingSettings}," Enable (global) ",8,_o),t("button",{onClick:h=>e.updateSetting(o.key,!1,"global"),class:"setting-btn",disabled:e.loadingSettings}," Disable (global) ",8,wo),t("button",{onClick:h=>e.updateSetting(o.key,!0,"project"),class:"setting-btn",disabled:e.loadingSettings}," Enable (project) ",8,Co),t("button",{onClick:h=>e.updateSetting(o.key,!1,"project"),class:"setting-btn",disabled:e.loadingSettings}," Disable (project) ",8,So)])])):o.type==="select"?(a(),n(y,{key:1},[r(" Select setting "),t("div",Do,[t("div",Po,[i[27]||(i[27]=P(" Current: ",-1)),t("strong",null,g(((p=o.options.find(h=>h.value===e.settingsData.merged[o.key]))==null?void 0:p.label)||"Not set"),1),o.key in e.settingsData.project?(a(),n("span",xo,"(project)")):o.key in e.settingsData.global?(a(),n("span",Ao,"(global)")):r("v-if",!0)]),(a(!0),n(y,null,w(o.options,h=>(a(),n("div",{class:"setting-select-group",key:h.value},[t("div",To,g(h.label),1),t("div",jo,[t("button",{onClick:f=>e.updateSetting(o.key,h.value,"global"),class:"setting-btn",disabled:e.loadingSettings}," Set global ",8,Io),t("button",{onClick:f=>e.updateSetting(o.key,h.value,"project"),class:"setting-btn",disabled:e.loadingSettings}," Set project ",8,Mo)])]))),128))])],2112)):o.type==="action"?(a(),n(y,{key:2},[r(" Action setting "),t("div",zo,[t("div",No,[t("button",{onClick:e.openHooksSection,class:"setting-btn setting-btn-action",disabled:e.loadingSettings}," Open ",8,Ho)])])],2112)):r("v-if",!0)])}),128))])),r(" Hooks Section (shown when hooks action is triggered) "),e.showHooksSection?(a(),n("div",Oo,[t("div",{class:"hooks-header"},[t("button",{onClick:e.closeHooksSection,class:"back-btn",title:"Back to settings"},"← Back"),i[28]||(i[28]=t("h4",null,"Hooks Management",-1))]),e.loadingHooks?(a(),n("div",Fo,[...i[29]||(i[29]=[t("div",{class:"loader-spinner"},null,-1),t("span",null,"Loading hooks...",-1)])])):(a(),n("div",Eo,[(a(!0),n(y,null,w(e.hooksData,o=>(a(),n("div",{key:o.name,class:"hook-item"},[t("div",Lo,g(o.name),1),t("div",{class:z(["hook-status",{"hook-active":o.active}])},g(o.active?"✓ Active":"Inactive"),3)]))),128)),t("div",{class:"hooks-actions"},[t("button",{onClick:e.editHooksWithAgent,class:"dialog-btn dialog-btn-primary"}," Edit Hooks with Agent ")])]))])):r("v-if",!0),t("div",{class:"dialog-buttons"},[t("button",{onClick:e.closeSettingsDialog,class:"dialog-btn dialog-btn-primary"},"Close")])])])):r("v-if",!0)])],64)}const qo=Ht(Et,[["render",Vo],["__file","/Users/justinschroeder/Projects/dmux/frontend/src/components/Dashboard.vue"]]),Bo=Ft(qo);Bo.mount("#app");
4791
5517
  `,
4792
5518
  mimeType: 'application/javascript',
4793
- size: 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
4794
5660
  },
4795
5661
  'index.js': {
4796
5662
  content: `#!/usr/bin/env node
4797
5663
  import { execSync } from 'child_process';
4798
5664
  import fs from 'fs/promises';
5665
+ import * as fsSync from 'fs';
4799
5666
  import path from 'path';
4800
5667
  import { fileURLToPath } from 'url';
4801
5668
  import { render } from 'ink';
4802
5669
  import React from 'react';
4803
5670
  import { createHash } from 'crypto';
5671
+ import { createRequire } from 'module';
4804
5672
  import DmuxApp from './DmuxApp.js';
4805
5673
  import { AutoUpdater } from './AutoUpdater.js';
4806
5674
  import readline from 'readline';
4807
5675
  import { DmuxServer } from './server/index.js';
4808
5676
  import { StateManager } from './shared/StateManager.js';
5677
+ import { LogService } from './services/LogService.js';
5678
+ import { createWelcomePane } from './utils/welcomePane.js';
5679
+ import { TMUX_COLORS } from './theme/colors.js';
5680
+ import { SIDEBAR_WIDTH } from './utils/layoutManager.js';
4809
5681
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
5682
+ const require = createRequire(import.meta.url);
5683
+ const packageJson = require('../package.json');
4810
5684
  class Dmux {
4811
5685
  panesFile;
4812
5686
  settingsFile;
@@ -4845,6 +5719,11 @@ class Dmux {
4845
5719
  async init() {
4846
5720
  // Set up global signal handlers for clean exit
4847
5721
  this.setupGlobalSignalHandlers();
5722
+ // Set up hooks for this session (if in tmux)
5723
+ if (process.env.TMUX) {
5724
+ this.setupResizeHook();
5725
+ this.setupPaneSplitHook();
5726
+ }
4848
5727
  // Ensure .dmux directory exists and is in .gitignore
4849
5728
  await this.ensureDmuxDirectory();
4850
5729
  // Check for migration from old config location
@@ -4856,13 +5735,16 @@ class Dmux {
4856
5735
  projectRoot: this.projectRoot,
4857
5736
  panes: [],
4858
5737
  settings: {},
4859
- lastUpdated: new Date().toISOString()
5738
+ lastUpdated: new Date().toISOString(),
5739
+ controlPaneId: undefined,
5740
+ controlPaneSize: 40 // Sidebar width
4860
5741
  };
4861
5742
  await fs.writeFile(this.panesFile, JSON.stringify(initialConfig, null, 2));
4862
5743
  }
4863
5744
  // Check for updates in background if needed
4864
5745
  this.checkForUpdatesBackground();
4865
5746
  const inTmux = process.env.TMUX !== undefined;
5747
+ const isDev = process.env.DMUX_DEV === 'true';
4866
5748
  if (!inTmux) {
4867
5749
  // Check if project-specific session already exists
4868
5750
  try {
@@ -4875,27 +5757,116 @@ class Dmux {
4875
5757
  execSync(\`tmux new-session -d -s \${this.sessionName}\`, { stdio: 'inherit' });
4876
5758
  // Enable pane borders to show titles
4877
5759
  execSync(\`tmux set-option -t \${this.sessionName} pane-border-status top\`, { stdio: 'inherit' });
5760
+ // Set border colors (foreground only - respects user's terminal background)
5761
+ execSync(\`tmux set-option -t \${this.sessionName} pane-active-border-style "fg=colour\${TMUX_COLORS.activeBorder}"\`, { stdio: 'inherit' });
5762
+ execSync(\`tmux set-option -t \${this.sessionName} pane-border-style "fg=colour\${TMUX_COLORS.inactiveBorder}"\`, { stdio: 'inherit' });
5763
+ // Set pane border format
5764
+ execSync(\`tmux set-option -t \${this.sessionName} pane-border-format " #{pane_title} "\`, { stdio: 'inherit' });
4878
5765
  // Set pane title for the main dmux pane
4879
- execSync(\`tmux select-pane -t \${this.sessionName} -T "dmux-\${this.projectName}"\`, { stdio: 'inherit' });
4880
- // Send dmux command to the new session
4881
- 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' });
4882
5792
  }
4883
5793
  execSync(\`tmux attach-session -t \${this.sessionName}\`, { stdio: 'inherit' });
4884
5794
  return;
4885
5795
  }
4886
5796
  // Enable pane borders to show titles
4887
- try {
4888
- execSync(\`tmux set-option pane-border-status top\`, { stdio: 'pipe' });
4889
- }
4890
- catch {
4891
- // Ignore if it fails
4892
- }
5797
+ // NOTE: Temporarily disabled to test if border updates cause UI shifts
5798
+ // try {
5799
+ // execSync(\`tmux set-option pane-border-status top\`, { stdio: 'pipe' });
5800
+ // } catch {
5801
+ // // Ignore if it fails
5802
+ // }
4893
5803
  // Set pane title for the current pane running dmux
5804
+ // NOTE: Temporarily disabled to test if title updates cause UI shifts
5805
+ // try {
5806
+ // execSync(\`tmux select-pane -T "dmux v\${packageJson.version} - \${this.projectName}"\`, { stdio: 'pipe' });
5807
+ // } catch {
5808
+ // // Ignore if it fails (might not have permission or tmux version doesn't support it)
5809
+ // }
5810
+ // Get current pane ID (control pane for left sidebar)
5811
+ let controlPaneId;
4894
5812
  try {
4895
- 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
+ }
4896
5866
  }
4897
- catch {
4898
- // 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);
4899
5870
  }
4900
5871
  // Update state manager with project info
4901
5872
  this.stateManager.updateProjectInfo(this.projectName, this.sessionName, this.projectRoot, this.panesFile);
@@ -4908,24 +5879,29 @@ class Dmux {
4908
5879
  // Don't log the local URL - tunnel will be created on demand when "r" is pressed
4909
5880
  }
4910
5881
  catch (err) {
4911
- console.error('Failed to start HTTP server:', err);
5882
+ LogService.getInstance().error('Failed to start HTTP server', 'Setup', undefined, err instanceof Error ? err : undefined);
4912
5883
  // Continue without server - not critical for main functionality
4913
5884
  }
4914
- // 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
4915
5900
  process.stdout.write('\\x1b[2J\\x1b[H'); // Clear screen and move cursor to home
4916
- process.stdout.write('\\x1b[3J'); // Clear scrollback buffer
4917
- // Clear tmux history
4918
- try {
4919
- execSync('tmux clear-history', { stdio: 'pipe' });
4920
- }
4921
- catch { }
4922
- // Force tmux to refresh before Ink renders
4923
- try {
4924
- execSync('tmux refresh-client', { stdio: 'pipe' });
4925
- }
4926
- catch { }
4927
- // Small delay to let the clear take effect before Ink renders
4928
- await new Promise(resolve => setTimeout(resolve, 100));
5901
+ // Ensure cursor is truly at home position and scrollback is clear
5902
+ process.stdout.write('\\x1b[1;1H'); // Force cursor to row 1, column 1
5903
+ // Small delay to let terminal settle
5904
+ await new Promise(resolve => setTimeout(resolve, 50));
4929
5905
  // Launch the Ink app
4930
5906
  const app = render(React.createElement(DmuxApp, {
4931
5907
  panesFile: this.panesFile,
@@ -4935,7 +5911,8 @@ class Dmux {
4935
5911
  projectRoot: this.projectRoot,
4936
5912
  autoUpdater: this.autoUpdater,
4937
5913
  serverPort: serverInfo.port,
4938
- server: this.server
5914
+ server: this.server,
5915
+ controlPaneId
4939
5916
  }), {
4940
5917
  exitOnCtrlC: false // Disable automatic exit on Ctrl+C
4941
5918
  });
@@ -4954,6 +5931,26 @@ class Dmux {
4954
5931
  return false;
4955
5932
  }
4956
5933
  }
5934
+ isWorktree() {
5935
+ try {
5936
+ // Check if current directory is different from project root
5937
+ const cwd = process.cwd();
5938
+ if (cwd === this.projectRoot) {
5939
+ return false;
5940
+ }
5941
+ // Check if we're in a git worktree by checking if .git is a file (not a directory)
5942
+ const gitPath = path.join(cwd, '.git');
5943
+ if (fsSync.existsSync(gitPath)) {
5944
+ const stats = fsSync.statSync(gitPath);
5945
+ // In a worktree, .git is a file, not a directory
5946
+ return stats.isFile();
5947
+ }
5948
+ return false;
5949
+ }
5950
+ catch {
5951
+ return false;
5952
+ }
5953
+ }
4957
5954
  getProjectRoot() {
4958
5955
  // Return cached value if available
4959
5956
  if (Dmux.cachedProjectRoot) {
@@ -5155,8 +6152,57 @@ class Dmux {
5155
6152
  getAutoUpdater() {
5156
6153
  return this.autoUpdater;
5157
6154
  }
6155
+ setupResizeHook() {
6156
+ try {
6157
+ // Set up session-specific hook that sends SIGUSR1 to dmux process on resize
6158
+ // This works inside tmux where normal SIGWINCH may not propagate
6159
+ const pid = process.pid;
6160
+ execSync(\`tmux set-hook -t '\${this.sessionName}' client-resized 'run-shell "kill -USR1 \${pid} 2>/dev/null || true"'\`, { stdio: 'pipe' });
6161
+ LogService.getInstance().debug(\`Set up resize hook for session \${this.sessionName}\`, 'Setup');
6162
+ }
6163
+ catch (error) {
6164
+ LogService.getInstance().debug('Failed to set up resize hook', 'Setup');
6165
+ }
6166
+ }
6167
+ setupPaneSplitHook() {
6168
+ try {
6169
+ // Set up hook that sends SIGUSR2 to dmux process when a pane is split
6170
+ // This allows us to detect manually created panes via Ctrl+b %
6171
+ const pid = process.pid;
6172
+ execSync(\`tmux set-hook -t '\${this.sessionName}' after-split-window 'run-shell "kill -USR2 \${pid} 2>/dev/null || true"'\`, { stdio: 'pipe' });
6173
+ LogService.getInstance().debug(\`Set up pane split detection hook for session \${this.sessionName}\`, 'Setup');
6174
+ }
6175
+ catch (error) {
6176
+ LogService.getInstance().debug('Failed to set up pane split hook', 'Setup');
6177
+ }
6178
+ }
6179
+ cleanupResizeHook() {
6180
+ try {
6181
+ // Remove session-specific hook
6182
+ execSync(\`tmux set-hook -u -t '\${this.sessionName}' client-resized\`, { stdio: 'pipe' });
6183
+ LogService.getInstance().debug('Cleaned up resize hook', 'Setup');
6184
+ }
6185
+ catch {
6186
+ // Ignore cleanup errors
6187
+ }
6188
+ }
6189
+ cleanupPaneSplitHook() {
6190
+ try {
6191
+ // Remove pane split hook
6192
+ execSync(\`tmux set-hook -u -t '\${this.sessionName}' after-split-window\`, { stdio: 'pipe' });
6193
+ LogService.getInstance().debug('Cleaned up pane split hook', 'Setup');
6194
+ }
6195
+ catch {
6196
+ // Ignore cleanup errors
6197
+ }
6198
+ }
5158
6199
  setupGlobalSignalHandlers() {
5159
6200
  const cleanTerminalExit = () => {
6201
+ // Clean up hooks
6202
+ if (process.env.TMUX) {
6203
+ this.cleanupResizeHook();
6204
+ this.cleanupPaneSplitHook();
6205
+ }
5160
6206
  // Clear screen multiple times to ensure no artifacts
5161
6207
  process.stdout.write('\\x1b[2J\\x1b[H'); // Clear screen and move to home
5162
6208
  process.stdout.write('\\x1b[3J'); // Clear scrollback buffer
@@ -5179,6 +6225,14 @@ class Dmux {
5179
6225
  // Handle Ctrl+C and SIGTERM
5180
6226
  process.on('SIGINT', cleanTerminalExit);
5181
6227
  process.on('SIGTERM', cleanTerminalExit);
6228
+ // Handle SIGUSR2 for pane split detection
6229
+ // This signal is sent by tmux hook when a new pane is created
6230
+ process.on('SIGUSR2', () => {
6231
+ // Log that a pane split was detected
6232
+ LogService.getInstance().debug('Pane split detected via SIGUSR2, triggering immediate detection', 'shellDetection');
6233
+ // Emit a custom event to trigger immediate shell pane detection
6234
+ process.emit('pane-split-detected');
6235
+ });
5182
6236
  // Handle uncaught exceptions and unhandled rejections
5183
6237
  process.on('uncaughtException', (error) => {
5184
6238
  console.error('Uncaught exception:', error);
@@ -5194,7 +6248,51 @@ const dmux = new Dmux();
5194
6248
  dmux.init().catch(() => process.exit(1));
5195
6249
  //# sourceMappingURL=index.js.map`,
5196
6250
  mimeType: 'application/javascript',
5197
- size: 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
5198
6296
  },
5199
6297
  'terminal.html': {
5200
6298
  content: `<!DOCTYPE html>