dmux 4.1.0 → 5.0.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 (138) hide show
  1. package/README.md +12 -6
  2. package/dist/DmuxApp.d.ts.map +1 -1
  3. package/dist/DmuxApp.js +119 -183
  4. package/dist/DmuxApp.js.map +1 -1
  5. package/dist/actions/implementations/closeAction.d.ts.map +1 -1
  6. package/dist/actions/implementations/closeAction.js +15 -40
  7. package/dist/actions/implementations/closeAction.js.map +1 -1
  8. package/dist/actions/types.d.ts +0 -1
  9. package/dist/actions/types.d.ts.map +1 -1
  10. package/dist/actions/types.js +7 -0
  11. package/dist/actions/types.js.map +1 -1
  12. package/dist/components/panes/PanesGrid.d.ts +2 -0
  13. package/dist/components/panes/PanesGrid.d.ts.map +1 -1
  14. package/dist/components/panes/PanesGrid.js +122 -42
  15. package/dist/components/panes/PanesGrid.js.map +1 -1
  16. package/dist/components/popups/agentChoicePopup.d.ts +1 -1
  17. package/dist/components/popups/agentChoicePopup.js +37 -23
  18. package/dist/components/popups/agentChoicePopup.js.map +1 -1
  19. package/dist/components/popups/newPanePopup.js +4 -3
  20. package/dist/components/popups/newPanePopup.js.map +1 -1
  21. package/dist/components/popups/shortcutsPopup.js +6 -4
  22. package/dist/components/popups/shortcutsPopup.js.map +1 -1
  23. package/dist/components/ui/FooterHelp.d.ts +0 -8
  24. package/dist/components/ui/FooterHelp.d.ts.map +1 -1
  25. package/dist/components/ui/FooterHelp.js +5 -24
  26. package/dist/components/ui/FooterHelp.js.map +1 -1
  27. package/dist/hooks/useActionSystem.d.ts +1 -2
  28. package/dist/hooks/useActionSystem.d.ts.map +1 -1
  29. package/dist/hooks/useActionSystem.js +9 -25
  30. package/dist/hooks/useActionSystem.js.map +1 -1
  31. package/dist/hooks/useAgentDetection.d.ts +2 -1
  32. package/dist/hooks/useAgentDetection.d.ts.map +1 -1
  33. package/dist/hooks/useAgentDetection.js.map +1 -1
  34. package/dist/hooks/useAgentStatus.d.ts.map +1 -1
  35. package/dist/hooks/useAgentStatus.js +18 -6
  36. package/dist/hooks/useAgentStatus.js.map +1 -1
  37. package/dist/hooks/useInputHandling.d.ts +6 -9
  38. package/dist/hooks/useInputHandling.d.ts.map +1 -1
  39. package/dist/hooks/useInputHandling.js +91 -69
  40. package/dist/hooks/useInputHandling.js.map +1 -1
  41. package/dist/hooks/useLayoutManagement.d.ts +1 -2
  42. package/dist/hooks/useLayoutManagement.d.ts.map +1 -1
  43. package/dist/hooks/useLayoutManagement.js +2 -9
  44. package/dist/hooks/useLayoutManagement.js.map +1 -1
  45. package/dist/hooks/useNavigation.d.ts +1 -1
  46. package/dist/hooks/useNavigation.d.ts.map +1 -1
  47. package/dist/hooks/useNavigation.js +29 -36
  48. package/dist/hooks/useNavigation.js.map +1 -1
  49. package/dist/hooks/usePaneCreation.d.ts +10 -4
  50. package/dist/hooks/usePaneCreation.d.ts.map +1 -1
  51. package/dist/hooks/usePaneCreation.js +20 -49
  52. package/dist/hooks/usePaneCreation.js.map +1 -1
  53. package/dist/hooks/usePaneLoading.d.ts.map +1 -1
  54. package/dist/hooks/usePaneLoading.js +21 -23
  55. package/dist/hooks/usePaneLoading.js.map +1 -1
  56. package/dist/hooks/usePaneRunner.d.ts +1 -1
  57. package/dist/hooks/usePaneRunner.d.ts.map +1 -1
  58. package/dist/hooks/usePaneRunner.js +5 -2
  59. package/dist/hooks/usePaneRunner.js.map +1 -1
  60. package/dist/hooks/usePaneSync.d.ts.map +1 -1
  61. package/dist/hooks/usePaneSync.js +29 -20
  62. package/dist/hooks/usePaneSync.js.map +1 -1
  63. package/dist/hooks/usePanes.d.ts.map +1 -1
  64. package/dist/hooks/usePanes.js +61 -65
  65. package/dist/hooks/usePanes.js.map +1 -1
  66. package/dist/hooks/useServices.d.ts +4 -7
  67. package/dist/hooks/useServices.d.ts.map +1 -1
  68. package/dist/hooks/useServices.js +0 -4
  69. package/dist/hooks/useServices.js.map +1 -1
  70. package/dist/hooks/useTerminalWidth.d.ts.map +1 -1
  71. package/dist/hooks/useTerminalWidth.js +0 -14
  72. package/dist/hooks/useTerminalWidth.js.map +1 -1
  73. package/dist/hooks/useWorktreeActions.d.ts +1 -2
  74. package/dist/hooks/useWorktreeActions.d.ts.map +1 -1
  75. package/dist/hooks/useWorktreeActions.js +2 -8
  76. package/dist/hooks/useWorktreeActions.js.map +1 -1
  77. package/dist/index.js +260 -51
  78. package/dist/index.js.map +1 -1
  79. package/dist/server/embedded-assets.d.ts.map +1 -1
  80. package/dist/server/embedded-assets.js +381 -236
  81. package/dist/server/embedded-assets.js.map +1 -1
  82. package/dist/server/routes/panesRoutes.d.ts.map +1 -1
  83. package/dist/server/routes/panesRoutes.js +16 -2
  84. package/dist/server/routes/panesRoutes.js.map +1 -1
  85. package/dist/services/PopupManager.d.ts +5 -7
  86. package/dist/services/PopupManager.d.ts.map +1 -1
  87. package/dist/services/PopupManager.js +8 -42
  88. package/dist/services/PopupManager.js.map +1 -1
  89. package/dist/types.d.ts +2 -5
  90. package/dist/types.d.ts.map +1 -1
  91. package/dist/utils/agentLaunch.d.ts +12 -0
  92. package/dist/utils/agentLaunch.d.ts.map +1 -0
  93. package/dist/utils/agentLaunch.js +56 -0
  94. package/dist/utils/agentLaunch.js.map +1 -0
  95. package/dist/utils/paneCreation.d.ts +4 -0
  96. package/dist/utils/paneCreation.d.ts.map +1 -1
  97. package/dist/utils/paneCreation.js +33 -17
  98. package/dist/utils/paneCreation.js.map +1 -1
  99. package/dist/utils/paneGrouping.d.ts +15 -0
  100. package/dist/utils/paneGrouping.d.ts.map +1 -0
  101. package/dist/utils/paneGrouping.js +24 -0
  102. package/dist/utils/paneGrouping.js.map +1 -0
  103. package/dist/utils/paneProject.d.ts +16 -0
  104. package/dist/utils/paneProject.d.ts.map +1 -0
  105. package/dist/utils/paneProject.js +40 -0
  106. package/dist/utils/paneProject.js.map +1 -0
  107. package/dist/utils/paneRebinding.d.ts.map +1 -1
  108. package/dist/utils/paneRebinding.js +13 -7
  109. package/dist/utils/paneRebinding.js.map +1 -1
  110. package/dist/utils/paneTitle.d.ts +13 -0
  111. package/dist/utils/paneTitle.d.ts.map +1 -0
  112. package/dist/utils/paneTitle.js +48 -0
  113. package/dist/utils/paneTitle.js.map +1 -0
  114. package/dist/utils/projectActions.d.ts +32 -0
  115. package/dist/utils/projectActions.d.ts.map +1 -0
  116. package/dist/utils/projectActions.js +108 -0
  117. package/dist/utils/projectActions.js.map +1 -0
  118. package/dist/utils/projectRoot.d.ts +10 -0
  119. package/dist/utils/projectRoot.d.ts.map +1 -0
  120. package/dist/utils/projectRoot.js +66 -0
  121. package/dist/utils/projectRoot.js.map +1 -0
  122. package/dist/utils/reopenWorktree.d.ts +2 -0
  123. package/dist/utils/reopenWorktree.d.ts.map +1 -1
  124. package/dist/utils/reopenWorktree.js +14 -4
  125. package/dist/utils/reopenWorktree.js.map +1 -1
  126. package/dist/utils/shellPaneDetection.d.ts.map +1 -1
  127. package/dist/utils/shellPaneDetection.js +21 -0
  128. package/dist/utils/shellPaneDetection.js.map +1 -1
  129. package/dist/utils/tmux.d.ts.map +1 -1
  130. package/dist/utils/tmux.js +0 -8
  131. package/dist/utils/tmux.js.map +1 -1
  132. package/dist/utils/tmuxConfigOnboarding.d.ts +19 -0
  133. package/dist/utils/tmuxConfigOnboarding.d.ts.map +1 -0
  134. package/dist/utils/tmuxConfigOnboarding.js +299 -0
  135. package/dist/utils/tmuxConfigOnboarding.js.map +1 -0
  136. package/dist/workers/panePollingWorker.js +0 -6
  137. package/dist/workers/panePollingWorker.js.map +1 -1
  138. package/package.json +1 -1
@@ -4,9 +4,8 @@
4
4
  */
5
5
  export const embeddedAssets = {
6
6
  'DmuxApp.js': {
7
- content: `import React, { useState, useEffect } from "react";
7
+ content: `import React, { useState, useEffect, useMemo } from "react";
8
8
  import { Box, Text, useApp, useStdout, useInput } from "ink";
9
- import { createRequire } from "module";
10
9
  import { TmuxService } from "./services/TmuxService.js";
11
10
  // Hooks
12
11
  import usePanes from "./hooks/usePanes.js";
@@ -23,13 +22,12 @@ import { useStatusMessages } from "./hooks/useStatusMessages.js";
23
22
  import { useLayoutManagement } from "./hooks/useLayoutManagement.js";
24
23
  import { useInputHandling } from "./hooks/useInputHandling.js";
25
24
  import { useDialogState } from "./hooks/useDialogState.js";
26
- import { useTunnelManagement } from "./hooks/useTunnelManagement.js";
27
25
  import { useDebugInfo } from "./hooks/useDebugInfo.js";
28
26
  // Utils
29
27
  import { SIDEBAR_WIDTH } from "./utils/layoutManager.js";
30
28
  import { supportsPopups } from "./utils/popup.js";
31
29
  import { StateManager } from "./shared/StateManager.js";
32
- import { REPAINT_SPINNER_DURATION, STATUS_MESSAGE_DURATION_SHORT, } from "./constants/timing.js";
30
+ import { STATUS_MESSAGE_DURATION_SHORT, } from "./constants/timing.js";
33
31
  import { getStatusDetector, } from "./services/StatusDetector.js";
34
32
  import { SettingsManager } from "./utils/settingsManager.js";
35
33
  import { useServices } from "./hooks/useServices.js";
@@ -37,10 +35,10 @@ import { PaneLifecycleManager } from "./services/PaneLifecycleManager.js";
37
35
  import { reopenWorktree } from "./utils/reopenWorktree.js";
38
36
  import { fileURLToPath } from "url";
39
37
  import { dirname } from "path";
38
+ import { getAgentSlugSuffix, } from "./utils/agentLaunch.js";
39
+ import { generateSlug } from "./utils/slug.js";
40
40
  const __filename = fileURLToPath(import.meta.url);
41
41
  const __dirname = dirname(__filename);
42
- const require = createRequire(import.meta.url);
43
- const packageJson = require("../package.json");
44
42
  import PanesGrid from "./components/panes/PanesGrid.js";
45
43
  import CommandPromptDialog from "./components/dialogs/CommandPromptDialog.js";
46
44
  import FileCopyPrompt from "./components/ui/FileCopyPrompt.js";
@@ -50,26 +48,20 @@ import UpdatingIndicator from "./components/indicators/UpdatingIndicator.js";
50
48
  import FooterHelp from "./components/ui/FooterHelp.js";
51
49
  import TmuxHooksPromptDialog from "./components/dialogs/TmuxHooksPromptDialog.js";
52
50
  import { PaneEventService } from "./services/PaneEventService.js";
53
- const DmuxApp = ({ panesFile, projectName, sessionName, settingsFile, projectRoot, autoUpdater, serverPort, server, controlPaneId, rerenderRef, }) => {
51
+ import { buildProjectActionLayout, buildVisualNavigationRows, } from "./utils/projectActions.js";
52
+ const DmuxApp = ({ panesFile, projectName, sessionName, settingsFile, projectRoot, autoUpdater, controlPaneId, }) => {
54
53
  const { stdout } = useStdout();
55
54
  const terminalHeight = stdout?.rows || 40;
56
55
  /* panes state moved to usePanes */
57
56
  const [selectedIndex, setSelectedIndex] = useState(0);
58
- const { statusMessage, setStatusMessage, showStatus, clearStatus } = useStatusMessages();
57
+ const { statusMessage, setStatusMessage } = useStatusMessages();
59
58
  const [isCreatingPane, setIsCreatingPane] = useState(false);
60
59
  // Settings state
61
60
  const [settingsManager] = useState(() => new SettingsManager(projectRoot));
62
- // Force repaint trigger - incrementing this causes Ink to re-render
63
- const [forceRepaintTrigger, setForceRepaintTrigger] = useState(0);
64
- // Spinner state - shows for a few frames to force render
65
- const [showRepaintSpinner, setShowRepaintSpinner] = useState(false);
66
61
  const { projectSettings, saveSettings } = useProjectSettings(settingsFile);
67
62
  // Dialog state management
68
63
  const dialogState = useDialogState();
69
64
  const { showCommandPrompt, setShowCommandPrompt, commandInput, setCommandInput, showFileCopyPrompt, setShowFileCopyPrompt, currentCommandType, setCurrentCommandType, runningCommand, setRunningCommand, quitConfirmMode, setQuitConfirmMode, } = dialogState;
70
- // Tunnel/network state management
71
- const tunnelState = useTunnelManagement();
72
- const { tunnelUrl, setTunnelUrl, tunnelCreating, setTunnelCreating, tunnelCopied, setTunnelCopied, localIp, setLocalIp, } = tunnelState;
73
65
  // Debug/development info
74
66
  const { debugMessage, setDebugMessage, currentBranch } = useDebugInfo(__dirname);
75
67
  // Update state handled by hook
@@ -101,11 +93,11 @@ const DmuxApp = ({ panesFile, projectName, sessionName, settingsFile, projectRoo
101
93
  const stateManager = StateManager.getInstance();
102
94
  const updateState = () => {
103
95
  const state = stateManager.getState();
104
- setUnreadErrorCount(state.unreadErrorCount);
105
- setUnreadWarningCount(state.unreadWarningCount);
106
- setCurrentToast(state.currentToast);
107
- setToastQueueLength(state.toastQueueLength);
108
- setToastQueuePosition(state.toastQueuePosition);
96
+ setUnreadErrorCount((prev) => prev === state.unreadErrorCount ? prev : state.unreadErrorCount);
97
+ setUnreadWarningCount((prev) => prev === state.unreadWarningCount ? prev : state.unreadWarningCount);
98
+ setCurrentToast((prev) => prev === state.currentToast ? prev : state.currentToast);
99
+ setToastQueueLength((prev) => prev === state.toastQueueLength ? prev : state.toastQueueLength);
100
+ setToastQueuePosition((prev) => prev === state.toastQueuePosition ? prev : state.toastQueuePosition);
109
101
  };
110
102
  // Initial state
111
103
  updateState();
@@ -116,7 +108,7 @@ const DmuxApp = ({ panesFile, projectName, sessionName, settingsFile, projectRoo
116
108
  };
117
109
  }, []);
118
110
  // Panes state and persistence (skipLoading will be updated after actionSystem is initialized)
119
- const { panes, setPanes, isLoading, loadPanes, savePanes, eventMode } = usePanes(panesFile, false, sessionName, controlPaneId, useHooks);
111
+ const { panes, setPanes, isLoading, loadPanes, savePanes } = usePanes(panesFile, false, sessionName, controlPaneId, useHooks);
120
112
  // Check for tmux hooks preference on startup
121
113
  useEffect(() => {
122
114
  const checkHooksPreference = async () => {
@@ -162,77 +154,18 @@ const DmuxApp = ({ panesFile, projectName, sessionName, settingsFile, projectRoo
162
154
  setStatusMessage,
163
155
  setRunningCommand,
164
156
  });
165
- // Force repaint helper - shows spinner for a few frames to force full re-render
166
- const forceRepaint = () => {
167
- setForceRepaintTrigger((prev) => prev + 1);
168
- setShowRepaintSpinner(true);
169
- // CRITICAL: Use Ink's official rerender method to force complete redraw
170
- // When tmux clears the pane via selectLayout, Ink's output is lost
171
- // Calling rerender forces Ink to redraw the entire component tree
172
- if (rerenderRef?.current) {
173
- rerenderRef.current(React.createElement(DmuxApp, {
174
- panesFile,
175
- projectName,
176
- sessionName,
177
- settingsFile,
178
- projectRoot,
179
- autoUpdater,
180
- serverPort,
181
- server,
182
- controlPaneId,
183
- rerenderRef,
184
- }));
185
- }
186
- // Hide spinner after a few frames (enough to trigger multiple renders)
187
- setTimeout(() => setShowRepaintSpinner(false), REPAINT_SPINNER_DURATION);
188
- };
189
- // Force repaint effect - ensures Ink re-renders when trigger changes
190
- useEffect(() => {
191
- if (forceRepaintTrigger > 0) {
192
- // Small delay to ensure terminal is ready
193
- const timer = setTimeout(async () => {
194
- try {
195
- const tmuxService = TmuxService.getInstance();
196
- await tmuxService.refreshClient();
197
- }
198
- catch { }
199
- }, 50);
200
- return () => clearTimeout(timer);
201
- }
202
- }, [forceRepaintTrigger]);
203
- // Get local network IP on mount
204
- useEffect(() => {
205
- const getLocalIp = async () => {
206
- try {
207
- // Get local IP address (not 127.0.0.1)
208
- const { execSync } = await import("child_process");
209
- const result = execSync(\`hostname -I 2>/dev/null || ifconfig | grep "inet " | grep -v 127.0.0.1 | awk '{print $2}' | head -1\`, {
210
- encoding: "utf-8",
211
- stdio: "pipe",
212
- }).trim();
213
- if (result) {
214
- setLocalIp(result.split(" ")[0]); // Take first IP if multiple
215
- }
216
- }
217
- catch {
218
- // Fallback to 127.0.0.1
219
- setLocalIp("127.0.0.1");
220
- }
221
- };
222
- getLocalIp();
223
- }, []);
224
157
  // Spinner animation and branch detection now handled in hooks
225
158
  // Pane creation
226
159
  const { createNewPane: createNewPaneHook } = usePaneCreation({
227
160
  panes,
228
161
  savePanes,
229
162
  projectName,
163
+ sessionProjectRoot: projectRoot || process.cwd(),
164
+ panesFile,
230
165
  setIsCreatingPane,
231
166
  setStatusMessage,
232
167
  loadPanes,
233
- panesFile,
234
168
  availableAgents,
235
- forceRepaint,
236
169
  });
237
170
  // Initialize services
238
171
  const { popupManager } = useServices({
@@ -244,81 +177,82 @@ const DmuxApp = ({ panesFile, projectName, sessionName, settingsFile, projectRoo
244
177
  terminalHeight,
245
178
  availableAgents,
246
179
  agentChoice,
247
- serverPort,
248
- server,
249
180
  settingsManager,
250
181
  projectSettings,
251
182
  // Callbacks
252
183
  setStatusMessage,
253
184
  setIgnoreInput,
254
- savePanes,
255
- loadPanes,
256
185
  });
257
186
  // Listen for status updates with analysis data and merge into panes
258
187
  useEffect(() => {
259
188
  const statusDetector = getStatusDetector();
260
189
  const handleStatusUpdate = (event) => {
261
190
  setPanes((prevPanes) => {
262
- const updatedPanes = prevPanes.map((pane) => {
263
- if (pane.id === event.paneId) {
264
- const updated = {
265
- ...pane,
266
- agentStatus: event.status,
267
- };
268
- // Only update analysis fields if they're present in the event (not undefined)
269
- // This prevents simple status changes from overwriting PaneAnalyzer results
270
- if (event.optionsQuestion !== undefined) {
271
- updated.optionsQuestion = event.optionsQuestion;
272
- }
273
- if (event.options !== undefined) {
274
- updated.options = event.options;
275
- }
276
- if (event.potentialHarm !== undefined) {
277
- updated.potentialHarm = event.potentialHarm;
278
- }
279
- if (event.summary !== undefined) {
280
- updated.agentSummary = event.summary;
281
- }
282
- if (event.analyzerError !== undefined) {
283
- updated.analyzerError = event.analyzerError;
284
- }
285
- // Clear option dialog data when transitioning away from 'waiting' state
286
- if (event.status !== "waiting" && pane.agentStatus === "waiting") {
287
- updated.optionsQuestion = undefined;
288
- updated.options = undefined;
289
- updated.potentialHarm = undefined;
290
- }
291
- // Clear summary when transitioning away from 'idle' state
292
- if (event.status !== "idle" && pane.agentStatus === "idle") {
293
- updated.agentSummary = undefined;
294
- }
295
- // Clear analyzer error when successfully getting a new analysis
296
- // or when transitioning to 'working' status
297
- if (event.status === "working") {
298
- updated.analyzerError = undefined;
299
- }
300
- else if (event.status === "waiting" || event.status === "idle") {
301
- if (event.analyzerError === undefined &&
302
- (event.optionsQuestion || event.summary)) {
303
- updated.analyzerError = undefined;
304
- }
305
- }
306
- return updated;
191
+ const paneIndex = prevPanes.findIndex((pane) => pane.id === event.paneId);
192
+ if (paneIndex === -1)
193
+ return prevPanes;
194
+ const pane = prevPanes[paneIndex];
195
+ const updated = {
196
+ ...pane,
197
+ agentStatus: event.status,
198
+ };
199
+ // Only update analysis fields if they're present in the event (not undefined)
200
+ // This prevents simple status changes from overwriting PaneAnalyzer results
201
+ if (event.optionsQuestion !== undefined) {
202
+ updated.optionsQuestion = event.optionsQuestion;
203
+ }
204
+ if (event.options !== undefined) {
205
+ updated.options = event.options;
206
+ }
207
+ if (event.potentialHarm !== undefined) {
208
+ updated.potentialHarm = event.potentialHarm;
209
+ }
210
+ if (event.summary !== undefined) {
211
+ updated.agentSummary = event.summary;
212
+ }
213
+ if (event.analyzerError !== undefined) {
214
+ updated.analyzerError = event.analyzerError;
215
+ }
216
+ // Clear option dialog data when transitioning away from 'waiting' state
217
+ if (event.status !== "waiting" && pane.agentStatus === "waiting") {
218
+ updated.optionsQuestion = undefined;
219
+ updated.options = undefined;
220
+ updated.potentialHarm = undefined;
221
+ }
222
+ // Clear summary when transitioning away from 'idle' state
223
+ if (event.status !== "idle" && pane.agentStatus === "idle") {
224
+ updated.agentSummary = undefined;
225
+ }
226
+ // Clear analyzer error when successfully getting a new analysis
227
+ // or when transitioning to 'working' status
228
+ if (event.status === "working") {
229
+ updated.analyzerError = undefined;
230
+ }
231
+ else if (event.status === "waiting" || event.status === "idle") {
232
+ if (event.analyzerError === undefined &&
233
+ (event.optionsQuestion || event.summary)) {
234
+ updated.analyzerError = undefined;
307
235
  }
308
- return pane;
309
- });
310
- // Persist to disk - ConfigWatcher will handle syncing to StateManager
311
- savePanes(updatedPanes).catch((err) => {
312
- console.error("Failed to save panes after status update:", err);
313
- });
314
- return updatedPanes;
236
+ }
237
+ const unchanged = pane.agentStatus === updated.agentStatus &&
238
+ pane.optionsQuestion === updated.optionsQuestion &&
239
+ pane.options === updated.options &&
240
+ pane.potentialHarm === updated.potentialHarm &&
241
+ pane.agentSummary === updated.agentSummary &&
242
+ pane.analyzerError === updated.analyzerError;
243
+ if (unchanged) {
244
+ return prevPanes;
245
+ }
246
+ const next = prevPanes.slice();
247
+ next[paneIndex] = updated;
248
+ return next;
315
249
  });
316
250
  };
317
251
  statusDetector.on("status-updated", handleStatusUpdate);
318
252
  return () => {
319
253
  statusDetector.off("status-updated", handleStatusUpdate);
320
254
  };
321
- }, [setPanes, savePanes]);
255
+ }, [setPanes]);
322
256
  // Note: No need to sync panes with StateManager here.
323
257
  // The ConfigWatcher automatically updates StateManager when the config file changes.
324
258
  // This prevents unnecessary SSE broadcasts on every local state update.
@@ -366,63 +300,80 @@ const DmuxApp = ({ panesFile, projectName, sessionName, settingsFile, projectRoo
366
300
  // No polling needed!
367
301
  // loadPanes moved to usePanes
368
302
  // getPanePositions moved to utils/tmux
303
+ const sessionProjectRoot = projectRoot || process.cwd();
304
+ const projectActionLayout = useMemo(() => buildProjectActionLayout(panes, sessionProjectRoot, projectName), [panes, sessionProjectRoot, projectName]);
305
+ const navigationRows = useMemo(() => isLoading
306
+ ? projectActionLayout.groups.flatMap((group) => group.panes.map((entry) => [entry.index]))
307
+ : buildVisualNavigationRows(projectActionLayout), [isLoading, projectActionLayout]);
369
308
  // Navigation logic moved to hook
370
- const { getCardGridPosition, findCardInDirection } = useNavigation(terminalWidth, panes.length, isLoading);
309
+ const { getCardGridPosition, findCardInDirection } = useNavigation(navigationRows);
371
310
  // findCardInDirection provided by useNavigation
372
311
  // savePanes moved to usePanes
373
312
  // applySmartLayout moved to utils/tmux
374
313
  // Helper function to handle agent choice and pane creation
375
- const handlePaneCreationWithAgent = async (prompt) => {
314
+ const handlePaneCreationWithAgent = async (prompt, targetProjectRoot) => {
376
315
  const agents = availableAgents;
316
+ const createPanesForAgents = async (selectedAgents) => {
317
+ const dedupedAgents = selectedAgents.filter((agent, index) => selectedAgents.indexOf(agent) === index);
318
+ let panesForCreation = panes;
319
+ const isMultiLaunch = dedupedAgents.length > 1;
320
+ const slugBase = isMultiLaunch ? await generateSlug(prompt) : undefined;
321
+ for (const selectedAgent of dedupedAgents) {
322
+ const pane = await createNewPaneHook(prompt, selectedAgent, {
323
+ existingPanes: panesForCreation,
324
+ slugSuffix: isMultiLaunch
325
+ ? getAgentSlugSuffix(selectedAgent)
326
+ : undefined,
327
+ slugBase,
328
+ targetProjectRoot,
329
+ });
330
+ if (!pane) {
331
+ return;
332
+ }
333
+ panesForCreation = [...panesForCreation, pane];
334
+ }
335
+ };
377
336
  if (agents.length === 0) {
378
- await createNewPaneHook(prompt);
337
+ await createNewPaneHook(prompt, undefined, { targetProjectRoot });
379
338
  }
380
339
  else if (agents.length === 1) {
381
- await createNewPaneHook(prompt, agents[0]);
340
+ await createPanesForAgents([agents[0]]);
382
341
  }
383
342
  else {
384
343
  // Multiple agents available - check for default agent setting first
385
344
  const settings = settingsManager.getSettings();
386
345
  if (settings.defaultAgent && agents.includes(settings.defaultAgent)) {
387
- await createNewPaneHook(prompt, settings.defaultAgent);
346
+ await createPanesForAgents([settings.defaultAgent]);
388
347
  }
389
348
  else {
390
349
  // Show agent choice popup
391
- const selectedAgent = await popupManager.launchAgentChoicePopup();
392
- if (selectedAgent) {
393
- await createNewPaneHook(prompt, selectedAgent);
350
+ const selectedAgents = await popupManager.launchAgentChoicePopup();
351
+ if (selectedAgents && selectedAgents.length > 0) {
352
+ await createPanesForAgents(selectedAgents);
394
353
  }
395
354
  }
396
355
  }
397
356
  };
398
357
  // Helper function to reopen a closed worktree
399
- const handleReopenWorktree = async (slug, worktreePath) => {
400
- // Force repaint first
401
- forceRepaint();
402
- // Minimal clearing
403
- process.stdout.write('\\x1b[2J\\x1b[H');
358
+ const handleReopenWorktree = async (slug, worktreePath, targetProjectRoot) => {
404
359
  try {
405
360
  setIsCreatingPane(true);
406
361
  setStatusMessage(\`Reopening \${slug}...\`);
362
+ const reopenProjectRoot = targetProjectRoot || projectRoot || process.cwd();
407
363
  const result = await reopenWorktree({
408
364
  slug,
409
365
  worktreePath,
410
- projectRoot: projectRoot || process.cwd(),
366
+ projectRoot: reopenProjectRoot,
367
+ sessionProjectRoot: projectRoot || process.cwd(),
368
+ sessionConfigPath: panesFile,
411
369
  existingPanes: panes,
412
370
  });
413
371
  // Save the pane
414
372
  const updatedPanes = [...panes, result.pane];
415
373
  await savePanes(updatedPanes);
416
- // Force repaint and refresh
417
- forceRepaint();
418
- process.stdout.write('\\x1b[2J\\x1b[3J\\x1b[H');
419
- const tmuxService = TmuxService.getInstance();
420
- tmuxService.clearHistorySync();
421
- tmuxService.refreshClientSync();
422
374
  await loadPanes();
423
375
  setStatusMessage(\`Reopened \${slug}\`);
424
376
  setTimeout(() => setStatusMessage(""), STATUS_MESSAGE_DURATION_SHORT);
425
- forceRepaint();
426
377
  }
427
378
  catch (error) {
428
379
  setStatusMessage(\`Failed to reopen: \${error.message}\`);
@@ -487,9 +438,7 @@ const DmuxApp = ({ panesFile, projectName, sessionName, settingsFile, projectRoo
487
438
  try {
488
439
  TmuxService.getInstance().selectPane(targetPane.paneId);
489
440
  }
490
- catch (error) {
491
- console.error('[onActionResult] Failed to navigate to pane:', error);
492
- }
441
+ catch { }
493
442
  }
494
443
  }
495
444
  // Show message if dismissable
@@ -521,7 +470,6 @@ const DmuxApp = ({ panesFile, projectName, sessionName, settingsFile, projectRoo
521
470
  await lifecycleManager.completeClose(paneId);
522
471
  },
523
472
  onActionResult: handleActionResult,
524
- forceRepaint,
525
473
  popupLaunchers: popupsSupported
526
474
  ? {
527
475
  launchConfirmPopup: popupManager.launchConfirmPopup.bind(popupManager),
@@ -544,7 +492,6 @@ const DmuxApp = ({ panesFile, projectName, sessionName, settingsFile, projectRoo
544
492
  isCreatingPane ||
545
493
  runningCommand ||
546
494
  isUpdating,
547
- onForceRepaint: forceRepaint,
548
495
  });
549
496
  // Monitor agent status across panes (returns a map of pane ID to status)
550
497
  const agentStatuses = useAgentStatus({
@@ -651,23 +598,19 @@ const DmuxApp = ({ panesFile, projectName, sessionName, settingsFile, projectRoo
651
598
  projectSettings,
652
599
  saveSettings,
653
600
  settingsManager,
654
- tunnelUrl,
655
- setTunnelUrl,
656
- tunnelCreating,
657
- setTunnelCreating,
658
- setTunnelCopied,
659
601
  popupManager,
660
602
  actionSystem,
661
- server,
662
603
  controlPaneId,
663
604
  setStatusMessage,
664
605
  copyNonGitFiles,
665
606
  runCommandInternal,
666
607
  handlePaneCreationWithAgent,
667
608
  handleReopenWorktree,
609
+ savePanes,
668
610
  loadPanes,
669
611
  cleanExit,
670
- projectRoot: projectRoot || process.cwd(),
612
+ projectRoot: sessionProjectRoot,
613
+ projectActionItems: projectActionLayout.actionItems,
671
614
  findCardInDirection,
672
615
  });
673
616
  // Calculate available height for content (terminal height - footer lines - active status messages)
@@ -676,7 +619,6 @@ const DmuxApp = ({ panesFile, projectName, sessionName, settingsFile, projectRoo
676
619
  // - Normal mode calculation:
677
620
  // - Base: 4 lines (marginTop + logs divider + logs line + keyboard shortcuts)
678
621
  // - Toast: +2 lines (toast message + marginBottom) if currentToast exists
679
- // - Network section: +4 lines (divider, local IP, remote tunnel, divider) if serverPort exists
680
622
  // - Debug info: +1 line if DEBUG_DMUX
681
623
  // - Status line: +1 line if updateAvailable/currentBranch/debugMessage
682
624
  // - Status messages: +1 line per active message
@@ -698,10 +640,6 @@ const DmuxApp = ({ panesFile, projectName, sessionName, settingsFile, projectRoo
698
640
  const wrappedLines = Math.ceil(toastTextLength / availableWidth);
699
641
  footerLines += wrappedLines + 1 + 1; // wrapped lines + header line + marginBottom
700
642
  }
701
- // Add network section (now 2 lines for local IP + remote tunnel, plus 2 dividers)
702
- if (serverPort && serverPort > 0) {
703
- footerLines += 4;
704
- }
705
643
  // Add debug info
706
644
  if (process.env.DEBUG_DMUX) {
707
645
  footerLines += 1;
@@ -720,10 +658,8 @@ const DmuxApp = ({ panesFile, projectName, sessionName, settingsFile, projectRoo
720
658
  }
721
659
  const contentHeight = Math.max(terminalHeight - footerLines, 10);
722
660
  return (React.createElement(Box, { flexDirection: "column", height: terminalHeight },
723
- showRepaintSpinner && (React.createElement(Box, { marginTop: -10, marginLeft: -100 },
724
- React.createElement(Text, null, "\\u27F3"))),
725
661
  React.createElement(Box, { flexDirection: "column", height: contentHeight, overflow: "hidden" },
726
- React.createElement(PanesGrid, { panes: panes, selectedIndex: selectedIndex, isLoading: isLoading, agentStatuses: agentStatuses }),
662
+ React.createElement(PanesGrid, { panes: panes, selectedIndex: selectedIndex, isLoading: isLoading, agentStatuses: agentStatuses, fallbackProjectRoot: projectRoot || process.cwd(), fallbackProjectName: projectName }),
727
663
  isLoading && React.createElement(LoadingIndicator, null),
728
664
  showCommandPrompt && (React.createElement(CommandPromptDialog, { type: showCommandPrompt, value: commandInput, onChange: setCommandInput })),
729
665
  showFileCopyPrompt && React.createElement(FileCopyPrompt, null),
@@ -738,11 +674,11 @@ const DmuxApp = ({ panesFile, projectName, sessionName, settingsFile, projectRoo
738
674
  : actionSystem.actionState.statusType === "success"
739
675
  ? "green"
740
676
  : "cyan" }, actionSystem.actionState.statusMessage))),
741
- React.createElement(FooterHelp, { show: !showCommandPrompt, showRemoteKey: !!server, quitConfirmMode: quitConfirmMode, hasSidebarLayout: !!controlPaneId, serverPort: serverPort, unreadErrorCount: unreadErrorCount, unreadWarningCount: unreadWarningCount, currentToast: currentToast, toastQueueLength: toastQueueLength, toastQueuePosition: toastQueuePosition, localIp: localIp, tunnelUrl: tunnelUrl, tunnelCreating: tunnelCreating, tunnelCopied: tunnelCopied, tunnelSpinner: tunnelState.getSpinnerChar(), gridInfo: (() => {
677
+ React.createElement(FooterHelp, { show: !showCommandPrompt, quitConfirmMode: quitConfirmMode, unreadErrorCount: unreadErrorCount, unreadWarningCount: unreadWarningCount, currentToast: currentToast, toastQueueLength: toastQueueLength, toastQueuePosition: toastQueuePosition, gridInfo: (() => {
742
678
  if (!process.env.DEBUG_DMUX)
743
679
  return undefined;
744
- const cols = Math.max(1, Math.floor(terminalWidth / 37));
745
- const rows = Math.ceil((panes.length + 1) / cols);
680
+ const rows = navigationRows.length;
681
+ const cols = Math.max(1, ...navigationRows.map((row) => row.length));
746
682
  const pos = getCardGridPosition(selectedIndex);
747
683
  return \`Grid: \${cols} cols × \${rows} rows | Selected: row \${pos.row}, col \${pos.col} | Terminal: \${terminalWidth}w\`;
748
684
  })() }),
@@ -760,7 +696,7 @@ const DmuxApp = ({ panesFile, projectName, sessionName, settingsFile, projectRoo
760
696
  export default DmuxApp;
761
697
  //# sourceMappingURL=DmuxApp.js.map`,
762
698
  mimeType: 'application/javascript',
763
- size: 35428
699
+ size: 33093
764
700
  },
765
701
  'chunks/styles.css_vue_type_style_index_0_src_true_lang-pW5mq51o.js': {
766
702
  content: `(function(){const t=document.createElement("link").relList;if(t&&t.supports&&t.supports("modulepreload"))return;for(const r of document.querySelectorAll('link[rel="modulepreload"]'))n(r);new MutationObserver(r=>{for(const i of r)if(i.type==="childList")for(const o of i.addedNodes)o.tagName==="LINK"&&o.rel==="modulepreload"&&n(o)}).observe(document,{childList:!0,subtree:!0});function s(r){const i={};return r.integrity&&(i.integrity=r.integrity),r.referrerPolicy&&(i.referrerPolicy=r.referrerPolicy),r.crossOrigin==="use-credentials"?i.credentials="include":r.crossOrigin==="anonymous"?i.credentials="omit":i.credentials="same-origin",i}function n(r){if(r.ep)return;r.ep=!0;const i=s(r);fetch(r.href,i)}})();/**
@@ -813,6 +749,7 @@ export default DmuxApp;
813
749
  'index.js': {
814
750
  content: `#!/usr/bin/env node
815
751
  import { execSync, spawnSync } from 'child_process';
752
+ import chalk from 'chalk';
816
753
  import fs from 'fs/promises';
817
754
  import * as fsSync from 'fs';
818
755
  import path from 'path';
@@ -821,9 +758,9 @@ import { render } from 'ink';
821
758
  import React from 'react';
822
759
  import { createHash } from 'crypto';
823
760
  import { createRequire } from 'module';
761
+ import { createInterface } from 'node:readline/promises';
824
762
  import DmuxApp from './DmuxApp.js';
825
763
  import { AutoUpdater } from './services/AutoUpdater.js';
826
- import { DmuxServer } from './server/index.js';
827
764
  import { StateManager } from './shared/StateManager.js';
828
765
  import { LogService } from './services/LogService.js';
829
766
  import { TmuxService } from './services/TmuxService.js';
@@ -832,6 +769,11 @@ import { TMUX_COLORS } from './theme/colors.js';
832
769
  import { SIDEBAR_WIDTH } from './utils/layoutManager.js';
833
770
  import { validateSystemRequirements, printValidationResults } from './utils/systemCheck.js';
834
771
  import { getUntrackedPanes } from './utils/shellPaneDetection.js';
772
+ import { runTmuxConfigOnboardingIfNeeded } from './utils/tmuxConfigOnboarding.js';
773
+ import { getAvailableAgents } from './utils/agentDetection.js';
774
+ import { createPane } from './utils/paneCreation.js';
775
+ import { SettingsManager } from './utils/settingsManager.js';
776
+ import { atomicWriteJson } from './utils/atomicWrite.js';
835
777
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
836
778
  const require = createRequire(import.meta.url);
837
779
  const packageJson = require('../package.json');
@@ -842,7 +784,6 @@ class Dmux {
842
784
  sessionName;
843
785
  projectRoot;
844
786
  autoUpdater;
845
- server;
846
787
  stateManager;
847
788
  constructor() {
848
789
  // Get git root directory to determine project scope
@@ -850,14 +791,8 @@ class Dmux {
850
791
  this.projectRoot = this.getProjectRoot();
851
792
  // Get project name from git root directory
852
793
  this.projectName = path.basename(this.projectRoot);
853
- // Create a unique identifier for this project based on its full path
854
- // This ensures different projects with the same folder name are kept separate
855
- const projectHash = createHash('md5').update(this.projectRoot).digest('hex').substring(0, 8);
856
- const projectIdentifier = \`\${this.projectName}-\${projectHash}\`;
857
- // Create unique session name for this project (sanitize for tmux compatibility)
858
- // tmux converts dots to underscores, so we do it explicitly to avoid mismatches
859
- const sanitizedProjectIdentifier = projectIdentifier.replace(/\\./g, '-');
860
- this.sessionName = \`dmux-\${sanitizedProjectIdentifier}\`;
794
+ // Create a stable, collision-safe session name for this project root
795
+ this.sessionName = this.buildSessionNameForRoot(this.projectRoot);
861
796
  // Store config in .dmux directory inside project root
862
797
  const dmuxDir = path.join(this.projectRoot, '.dmux');
863
798
  const configFile = path.join(dmuxDir, 'dmux.config.json');
@@ -866,22 +801,18 @@ class Dmux {
866
801
  this.settingsFile = configFile; // Same file for all config
867
802
  // Initialize auto-updater with config file
868
803
  this.autoUpdater = new AutoUpdater(configFile);
869
- // Initialize server and state manager
870
- this.server = new DmuxServer();
804
+ // Initialize state manager
871
805
  this.stateManager = StateManager.getInstance();
872
806
  }
873
807
  async init() {
874
808
  // Set up global signal handlers for clean exit
875
809
  this.setupGlobalSignalHandlers();
876
- // Set up hooks for this session (if in tmux)
877
- if (process.env.TMUX) {
878
- this.setupResizeHook();
879
- this.setupPaneSplitHook();
880
- }
881
810
  // Ensure .dmux directory exists and is in .gitignore
882
811
  await this.ensureDmuxDirectory();
883
812
  // Check for migration from old config location
884
813
  await this.migrateOldConfig();
814
+ // First-run global onboarding for tmux config presets
815
+ await runTmuxConfigOnboardingIfNeeded();
885
816
  // Initialize config file if it doesn't exist
886
817
  if (!await this.fileExists(this.panesFile)) {
887
818
  const initialConfig = {
@@ -895,10 +826,33 @@ class Dmux {
895
826
  };
896
827
  await fs.writeFile(this.panesFile, JSON.stringify(initialConfig, null, 2));
897
828
  }
898
- // Check for updates in background if needed
899
- this.checkForUpdatesBackground();
900
829
  const inTmux = process.env.TMUX !== undefined;
901
830
  const isDev = process.env.DMUX_DEV === 'true';
831
+ const currentTmuxSessionName = inTmux
832
+ ? this.getCurrentTmuxSessionName()
833
+ : null;
834
+ const sessionNameForCurrentTmux = currentTmuxSessionName || this.sessionName;
835
+ // Running dmux from another project while already inside a dmux session:
836
+ // offer to attach this project to the current sidebar/session instead.
837
+ if (inTmux &&
838
+ currentTmuxSessionName &&
839
+ currentTmuxSessionName.startsWith('dmux-') &&
840
+ currentTmuxSessionName !== this.sessionName) {
841
+ const shouldAttachToCurrent = await this.promptYesNo(\`Detected active dmux session '\${currentTmuxSessionName}'. Add project '\${this.projectName}' to this session?\`, true);
842
+ if (shouldAttachToCurrent) {
843
+ const attached = await this.attachProjectToExistingSession(currentTmuxSessionName);
844
+ if (attached) {
845
+ return;
846
+ }
847
+ }
848
+ }
849
+ // Check for updates in background if needed
850
+ this.checkForUpdatesBackground();
851
+ // Set up hooks for this session (if in tmux)
852
+ if (inTmux) {
853
+ this.setupResizeHook(sessionNameForCurrentTmux);
854
+ this.setupPaneSplitHook(sessionNameForCurrentTmux);
855
+ }
902
856
  if (!inTmux) {
903
857
  // Check if project-specific session already exists
904
858
  try {
@@ -1019,7 +973,7 @@ class Dmux {
1019
973
  }
1020
974
  // Check for untracked panes (terminal panes created outside dmux tracking)
1021
975
  const trackedPaneIds = config.panes?.map((p) => p.paneId) ?? [];
1022
- const untrackedPanes = await getUntrackedPanes(this.sessionName, trackedPaneIds, controlPaneId, config.welcomePaneId);
976
+ const untrackedPanes = await getUntrackedPanes(sessionNameForCurrentTmux, trackedPaneIds, controlPaneId, config.welcomePaneId);
1023
977
  // Only show welcome pane if there are no tracked AND no untracked panes
1024
978
  const hasAnyPanes = (config.panes?.length ?? 0) > 0 || untrackedPanes.length > 0;
1025
979
  if (controlPaneId && !hasAnyPanes) {
@@ -1056,20 +1010,13 @@ class Dmux {
1056
1010
  // Ignore errors in sidebar setup - will work without it
1057
1011
  LogService.getInstance().error('Failed to set up sidebar layout', 'Setup', undefined, error instanceof Error ? error : undefined);
1058
1012
  }
1013
+ const metadataSessionName = currentTmuxSessionName || this.getCurrentTmuxSessionName() || this.sessionName;
1014
+ const shouldPublishMetadata = !metadataSessionName.startsWith('dmux-') || metadataSessionName === this.sessionName;
1015
+ if (shouldPublishMetadata) {
1016
+ this.publishSessionMetadata(metadataSessionName);
1017
+ }
1059
1018
  // Update state manager with project info
1060
1019
  this.stateManager.updateProjectInfo(this.projectName, this.sessionName, this.projectRoot, this.panesFile);
1061
- // Start the HTTP server
1062
- let serverInfo = { port: 0, url: '' };
1063
- try {
1064
- serverInfo = await this.server.start();
1065
- // Update StateManager with server info
1066
- this.stateManager.updateServerInfo(serverInfo.port, serverInfo.url);
1067
- // Don't log the local URL - tunnel will be created on demand when "r" is pressed
1068
- }
1069
- catch (err) {
1070
- LogService.getInstance().error('Failed to start HTTP server', 'Setup', undefined, err instanceof Error ? err : undefined);
1071
- // Continue without server - not critical for main functionality
1072
- }
1073
1020
  // Logging system is ready (removed debug logs to reduce clutter)
1074
1021
  // Suppress console output from LogService to prevent interference with Ink UI
1075
1022
  LogService.getInstance().setSuppressConsole(true);
@@ -1078,8 +1025,6 @@ class Dmux {
1078
1025
  process.stdout.write('\\x1b[2J\\x1b[H'); // Clear screen and move cursor to home
1079
1026
  // Ensure cursor is truly at home position and scrollback is clear
1080
1027
  process.stdout.write('\\x1b[1;1H'); // Force cursor to row 1, column 1
1081
- // Small delay to let terminal settle
1082
- await new Promise(resolve => setTimeout(resolve, 50));
1083
1028
  // Launch the Ink app
1084
1029
  const appProps = {
1085
1030
  panesFile: this.panesFile,
@@ -1088,23 +1033,223 @@ class Dmux {
1088
1033
  sessionName: this.sessionName,
1089
1034
  projectRoot: this.projectRoot,
1090
1035
  autoUpdater: this.autoUpdater,
1091
- serverPort: serverInfo.port,
1092
- server: this.server,
1093
1036
  controlPaneId,
1094
- // Pass rerender function as a ref that will be set after first render
1095
- rerenderRef: { current: null }
1096
1037
  };
1097
1038
  const app = render(React.createElement(DmuxApp, appProps), {
1098
1039
  exitOnCtrlC: false // Disable automatic exit on Ctrl+C
1099
1040
  });
1100
- // Set the rerender function so DmuxApp can use it
1101
- appProps.rerenderRef.current = app.rerender;
1102
1041
  // Clean shutdown on app exit
1103
1042
  app.waitUntilExit().then(async () => {
1104
- await this.server.stop();
1105
1043
  process.exit(0);
1106
1044
  });
1107
1045
  }
1046
+ buildSessionNameForRoot(projectRoot) {
1047
+ const projectName = path.basename(projectRoot);
1048
+ const projectHash = createHash('md5').update(projectRoot).digest('hex').substring(0, 8);
1049
+ const projectIdentifier = \`\${projectName}-\${projectHash}\`;
1050
+ const sanitizedProjectIdentifier = projectIdentifier.replace(/\\./g, '-');
1051
+ return \`dmux-\${sanitizedProjectIdentifier}\`;
1052
+ }
1053
+ getCurrentTmuxSessionName() {
1054
+ try {
1055
+ const result = spawnSync('tmux', ['display-message', '-p', '#S'], {
1056
+ encoding: 'utf-8',
1057
+ stdio: 'pipe',
1058
+ });
1059
+ if (result.status !== 0)
1060
+ return null;
1061
+ const sessionName = (result.stdout || '').trim();
1062
+ return sessionName || null;
1063
+ }
1064
+ catch {
1065
+ return null;
1066
+ }
1067
+ }
1068
+ getTmuxOptionValue(sessionName, optionName) {
1069
+ try {
1070
+ const result = spawnSync('tmux', ['show-options', '-v', '-t', sessionName, optionName], {
1071
+ encoding: 'utf-8',
1072
+ stdio: 'pipe',
1073
+ });
1074
+ if (result.status !== 0)
1075
+ return null;
1076
+ const value = (result.stdout || '').trim();
1077
+ return value || null;
1078
+ }
1079
+ catch {
1080
+ return null;
1081
+ }
1082
+ }
1083
+ publishSessionMetadata(sessionName) {
1084
+ try {
1085
+ spawnSync('tmux', ['set-option', '-t', sessionName, '@dmux_project_root', this.projectRoot], { stdio: 'pipe' });
1086
+ spawnSync('tmux', ['set-option', '-t', sessionName, '@dmux_project_name', this.projectName], { stdio: 'pipe' });
1087
+ spawnSync('tmux', ['set-option', '-t', sessionName, '@dmux_config_path', this.panesFile], { stdio: 'pipe' });
1088
+ }
1089
+ catch {
1090
+ // Metadata is best-effort only
1091
+ }
1092
+ }
1093
+ async promptYesNo(question, defaultYes = true) {
1094
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
1095
+ return false;
1096
+ }
1097
+ const suffix = defaultYes ? '[Y/n]' : '[y/N]';
1098
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
1099
+ try {
1100
+ const answer = (await rl.question(\`\${question} \${suffix} \`)).trim().toLowerCase();
1101
+ if (!answer)
1102
+ return defaultYes;
1103
+ if (answer === 'y' || answer === 'yes')
1104
+ return true;
1105
+ if (answer === 'n' || answer === 'no')
1106
+ return false;
1107
+ return defaultYes;
1108
+ }
1109
+ finally {
1110
+ rl.close();
1111
+ }
1112
+ }
1113
+ getAncestorPaths(startPath) {
1114
+ const ancestors = [];
1115
+ let cursor = path.resolve(startPath);
1116
+ while (true) {
1117
+ ancestors.push(cursor);
1118
+ const parent = path.dirname(cursor);
1119
+ if (parent === cursor)
1120
+ break;
1121
+ cursor = parent;
1122
+ }
1123
+ return ancestors;
1124
+ }
1125
+ inferSessionContextFromPanePaths(sessionName) {
1126
+ try {
1127
+ const listResult = spawnSync('tmux', ['list-panes', '-t', sessionName, '-F', '#{pane_current_path}'], {
1128
+ encoding: 'utf-8',
1129
+ stdio: 'pipe',
1130
+ });
1131
+ if (listResult.status !== 0) {
1132
+ return null;
1133
+ }
1134
+ const panePaths = (listResult.stdout || '')
1135
+ .split('\\n')
1136
+ .map((line) => line.trim())
1137
+ .filter(Boolean);
1138
+ const seenRoots = new Set();
1139
+ for (const panePath of panePaths) {
1140
+ for (const candidateRoot of this.getAncestorPaths(panePath)) {
1141
+ if (seenRoots.has(candidateRoot))
1142
+ continue;
1143
+ seenRoots.add(candidateRoot);
1144
+ const configPath = path.join(candidateRoot, '.dmux', 'dmux.config.json');
1145
+ if (!fsSync.existsSync(configPath)) {
1146
+ continue;
1147
+ }
1148
+ if (this.buildSessionNameForRoot(candidateRoot) !== sessionName) {
1149
+ continue;
1150
+ }
1151
+ return {
1152
+ sessionName,
1153
+ sessionProjectRoot: candidateRoot,
1154
+ sessionProjectName: path.basename(candidateRoot),
1155
+ sessionConfigPath: configPath,
1156
+ };
1157
+ }
1158
+ }
1159
+ }
1160
+ catch {
1161
+ // Fall through to null
1162
+ }
1163
+ return null;
1164
+ }
1165
+ getExistingSessionContext(sessionName) {
1166
+ const optionProjectRoot = this.getTmuxOptionValue(sessionName, '@dmux_project_root');
1167
+ const optionProjectName = this.getTmuxOptionValue(sessionName, '@dmux_project_name');
1168
+ const optionConfigPath = this.getTmuxOptionValue(sessionName, '@dmux_config_path');
1169
+ const sessionProjectRoot = optionProjectRoot
1170
+ || (optionConfigPath ? path.dirname(path.dirname(optionConfigPath)) : undefined);
1171
+ const sessionConfigPath = optionConfigPath
1172
+ || (sessionProjectRoot ? path.join(sessionProjectRoot, '.dmux', 'dmux.config.json') : undefined);
1173
+ if (sessionProjectRoot &&
1174
+ sessionConfigPath &&
1175
+ fsSync.existsSync(sessionConfigPath)) {
1176
+ return {
1177
+ sessionName,
1178
+ sessionProjectRoot,
1179
+ sessionProjectName: optionProjectName || path.basename(sessionProjectRoot),
1180
+ sessionConfigPath,
1181
+ };
1182
+ }
1183
+ return this.inferSessionContextFromPanePaths(sessionName);
1184
+ }
1185
+ getPreferredAttachAgent(availableAgents) {
1186
+ if (availableAgents.length === 0) {
1187
+ return undefined;
1188
+ }
1189
+ const settings = new SettingsManager(this.projectRoot).getSettings();
1190
+ if (settings.defaultAgent && availableAgents.includes(settings.defaultAgent)) {
1191
+ return settings.defaultAgent;
1192
+ }
1193
+ return availableAgents[0];
1194
+ }
1195
+ async attachProjectToExistingSession(sessionName) {
1196
+ const context = this.getExistingSessionContext(sessionName);
1197
+ if (!context) {
1198
+ console.log(chalk.yellow(\`Unable to locate config for session '\${sessionName}'. Run dmux inside that project once, then try again.\`));
1199
+ return false;
1200
+ }
1201
+ if (path.resolve(context.sessionProjectRoot) === path.resolve(this.projectRoot)) {
1202
+ return false;
1203
+ }
1204
+ try {
1205
+ const configRaw = await fs.readFile(context.sessionConfigPath, 'utf-8');
1206
+ const config = JSON.parse(configRaw);
1207
+ const existingPanes = Array.isArray(config.panes) ? config.panes : [];
1208
+ const availableAgents = await getAvailableAgents();
1209
+ let selectedAgent = this.getPreferredAttachAgent(availableAgents);
1210
+ const prompt = \`Explore \${this.projectName} and ask what to work on first.\`;
1211
+ let creation = await createPane({
1212
+ prompt,
1213
+ agent: selectedAgent,
1214
+ projectName: context.sessionProjectName,
1215
+ existingPanes,
1216
+ projectRoot: this.projectRoot,
1217
+ sessionConfigPath: context.sessionConfigPath,
1218
+ sessionProjectRoot: context.sessionProjectRoot,
1219
+ }, availableAgents);
1220
+ if (creation.needsAgentChoice) {
1221
+ selectedAgent = availableAgents[0];
1222
+ if (!selectedAgent) {
1223
+ throw new Error('No supported agent CLI found (claude, opencode, codex)');
1224
+ }
1225
+ creation = await createPane({
1226
+ prompt,
1227
+ agent: selectedAgent,
1228
+ projectName: context.sessionProjectName,
1229
+ existingPanes,
1230
+ projectRoot: this.projectRoot,
1231
+ sessionConfigPath: context.sessionConfigPath,
1232
+ sessionProjectRoot: context.sessionProjectRoot,
1233
+ }, availableAgents);
1234
+ }
1235
+ const latestConfigRaw = await fs.readFile(context.sessionConfigPath, 'utf-8');
1236
+ const latestConfig = JSON.parse(latestConfigRaw);
1237
+ const latestPanes = Array.isArray(latestConfig.panes) ? latestConfig.panes : [];
1238
+ const alreadyPersisted = latestPanes.some((pane) => pane.id === creation.pane.id || pane.paneId === creation.pane.paneId);
1239
+ if (!alreadyPersisted) {
1240
+ latestConfig.panes = [...latestPanes, creation.pane];
1241
+ latestConfig.lastUpdated = new Date().toISOString();
1242
+ await atomicWriteJson(context.sessionConfigPath, latestConfig);
1243
+ }
1244
+ console.log(chalk.green(\`Added project '\${this.projectName}' to session '\${sessionName}'.\`));
1245
+ return true;
1246
+ }
1247
+ catch (error) {
1248
+ const message = error instanceof Error ? error.message : String(error);
1249
+ console.error(chalk.red(\`Failed to add project '\${this.projectName}': \${message}\`));
1250
+ return false;
1251
+ }
1252
+ }
1108
1253
  async fileExists(path) {
1109
1254
  try {
1110
1255
  await fs.access(path);
@@ -1336,48 +1481,48 @@ class Dmux {
1336
1481
  getAutoUpdater() {
1337
1482
  return this.autoUpdater;
1338
1483
  }
1339
- setupResizeHook() {
1484
+ setupResizeHook(sessionName = this.sessionName) {
1340
1485
  try {
1341
1486
  // Set up session-specific hook that sends SIGUSR1 to dmux process on resize
1342
1487
  // This works inside tmux where normal SIGWINCH may not propagate
1343
1488
  const pid = process.pid;
1344
- execSync(\`tmux set-hook -t '\${this.sessionName}' client-resized 'run-shell "kill -USR1 \${pid} 2>/dev/null || true"'\`, { stdio: 'pipe' });
1489
+ execSync(\`tmux set-hook -t '\${sessionName}' client-resized 'run-shell "kill -USR1 \${pid} 2>/dev/null || true"'\`, { stdio: 'pipe' });
1345
1490
  // LogService.getInstance().debug(\`Set up resize hook for session \${this.sessionName}\`, 'Setup');
1346
1491
  }
1347
1492
  catch (error) {
1348
1493
  LogService.getInstance().warn('Failed to set up resize hook', 'Setup');
1349
1494
  }
1350
1495
  }
1351
- setupPaneSplitHook() {
1496
+ setupPaneSplitHook(sessionName = this.sessionName) {
1352
1497
  try {
1353
1498
  // Set up hooks that send SIGUSR2 to dmux process for pane events
1354
1499
  // This allows immediate detection of pane changes
1355
1500
  const pid = process.pid;
1356
1501
  // Detect manually created panes via Ctrl+b %
1357
- execSync(\`tmux set-hook -t '\${this.sessionName}' after-split-window 'run-shell "kill -USR2 \${pid} 2>/dev/null || true"'\`, { stdio: 'pipe' });
1502
+ execSync(\`tmux set-hook -t '\${sessionName}' after-split-window 'run-shell "kill -USR2 \${pid} 2>/dev/null || true"'\`, { stdio: 'pipe' });
1358
1503
  // Detect pane closures via Ctrl+b x or process exit
1359
- execSync(\`tmux set-hook -t '\${this.sessionName}' pane-exited 'run-shell "kill -USR2 \${pid} 2>/dev/null || true"'\`, { stdio: 'pipe' });
1504
+ execSync(\`tmux set-hook -t '\${sessionName}' pane-exited 'run-shell "kill -USR2 \${pid} 2>/dev/null || true"'\`, { stdio: 'pipe' });
1360
1505
  // LogService.getInstance().debug(\`Set up pane detection hooks for session \${this.sessionName}\`, 'Setup');
1361
1506
  }
1362
1507
  catch (error) {
1363
1508
  LogService.getInstance().warn('Failed to set up pane hooks', 'Setup');
1364
1509
  }
1365
1510
  }
1366
- cleanupResizeHook() {
1511
+ cleanupResizeHook(sessionName = this.getCurrentTmuxSessionName() || this.sessionName) {
1367
1512
  try {
1368
1513
  // Remove session-specific hook
1369
- execSync(\`tmux set-hook -u -t '\${this.sessionName}' client-resized\`, { stdio: 'pipe' });
1514
+ execSync(\`tmux set-hook -u -t '\${sessionName}' client-resized\`, { stdio: 'pipe' });
1370
1515
  LogService.getInstance().debug('Cleaned up resize hook', 'Setup');
1371
1516
  }
1372
1517
  catch {
1373
1518
  // Ignore cleanup errors
1374
1519
  }
1375
1520
  }
1376
- cleanupPaneSplitHook() {
1521
+ cleanupPaneSplitHook(sessionName = this.getCurrentTmuxSessionName() || this.sessionName) {
1377
1522
  try {
1378
1523
  // Remove pane hooks
1379
- execSync(\`tmux set-hook -u -t '\${this.sessionName}' after-split-window\`, { stdio: 'pipe' });
1380
- execSync(\`tmux set-hook -u -t '\${this.sessionName}' pane-exited\`, { stdio: 'pipe' });
1524
+ execSync(\`tmux set-hook -u -t '\${sessionName}' after-split-window\`, { stdio: 'pipe' });
1525
+ execSync(\`tmux set-hook -u -t '\${sessionName}' pane-exited\`, { stdio: 'pipe' });
1381
1526
  LogService.getInstance().debug('Cleaned up pane hooks', 'Setup');
1382
1527
  }
1383
1528
  catch {
@@ -1447,7 +1592,7 @@ class Dmux {
1447
1592
  })();
1448
1593
  //# sourceMappingURL=index.js.map`,
1449
1594
  mimeType: 'application/javascript',
1450
- size: 30903
1595
+ size: 40231
1451
1596
  },
1452
1597
  'styles.css': {
1453
1598
  content: `*{margin:0;padding:0;box-sizing:border-box}:root{--bg-gradient-start: #0f0f23;--bg-gradient-mid: #1a1a2e;--bg-gradient-end: #16213e;--text-primary: #e0e0e0;--text-secondary: #a0a0a0;--text-tertiary: #808080;--text-dim: #606060;--text-dimmer: #666;--text-bright: #fff;--border-color: rgba(255, 255, 255, .1);--border-accent: rgba(255, 140, 0, .3);--card-bg: rgba(255, 255, 255, .05);--card-border: rgba(255, 255, 255, .1);--header-bg: rgba(255, 255, 255, .05);--input-bg: rgba(255, 255, 255, .05);--input-border: rgba(255, 255, 255, .12);--input-focus-border: rgba(255, 140, 0, .5);--input-focus-bg: rgba(255, 255, 255, .08);--input-focus-shadow: rgba(255, 140, 0, .1);--button-bg: rgba(200, 210, 230, .15);--button-border: rgba(255, 255, 255, .08);--button-hover-bg: rgba(200, 210, 230, .25);--button-hover-border: rgba(255, 255, 255, .15);--tooltip-bg: rgba(20, 20, 30, .98);--tooltip-border: rgba(255, 255, 255, .15);--hint-bg: rgba(255, 255, 255, .05);--agent-bg: rgba(255, 255, 255, .08);--agent-border: rgba(255, 255, 255, .15);--idle-badge-bg: rgba(255, 255, 255, .08);--idle-badge-border: rgba(255, 255, 255, .1)}[data-theme=light]{--bg-gradient-start: #f0f4f8;--bg-gradient-mid: #e6eef5;--bg-gradient-end: #dce7f0;--text-primary: #1a1a2e;--text-secondary: #4a5568;--text-tertiary: #718096;--text-dim: #a0aec0;--text-dimmer: #cbd5e0;--text-bright: #000;--border-color: rgba(0, 0, 0, .1);--border-accent: rgba(255, 140, 0, .4);--card-bg: rgba(255, 255, 255, .8);--card-border: rgba(0, 0, 0, .08);--header-bg: rgba(255, 255, 255, .9);--input-bg: rgba(255, 255, 255, .6);--input-border: rgba(0, 0, 0, .15);--input-focus-border: rgba(255, 140, 0, .6);--input-focus-bg: rgba(255, 255, 255, .9);--input-focus-shadow: rgba(255, 140, 0, .15);--button-bg: rgba(0, 0, 0, .05);--button-border: rgba(0, 0, 0, .1);--button-hover-bg: rgba(0, 0, 0, .1);--button-hover-border: rgba(0, 0, 0, .2);--tooltip-bg: rgba(255, 255, 255, .98);--tooltip-border: rgba(0, 0, 0, .15);--hint-bg: rgba(0, 0, 0, .03);--agent-bg: rgba(0, 0, 0, .05);--agent-border: rgba(0, 0, 0, .12);--idle-badge-bg: rgba(0, 0, 0, .05);--idle-badge-border: rgba(0, 0, 0, .1)}@keyframes fadeIn{0%{opacity:0;transform:translateY(10px)}to{opacity:1;transform:translateY(0)}}@keyframes pulse{0%,to{opacity:1}50%{opacity:.5}}@keyframes slideInFromTop{0%{opacity:0;transform:translateY(-20px)}to{opacity:1;transform:translateY(0)}}body{font-family:-apple-system,BlinkMacSystemFont,SF Pro Display,SF Pro Text,Segoe UI,Roboto,Helvetica Neue,Arial,sans-serif;background:linear-gradient(135deg,var(--bg-gradient-start) 0%,var(--bg-gradient-mid) 50%,var(--bg-gradient-end) 100%);background-attachment:fixed;color:var(--text-primary);min-height:100vh;display:flex;flex-direction:column;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;transition:background .3s ease,color .3s ease}.container{max-width:1400px;margin:0 auto;padding:40px 20px;width:100%;flex:1;display:flex;flex-direction:column;animation:fadeIn .5s ease-out}header{display:flex;justify-content:space-between;align-items:center;padding:16px 24px;margin-bottom:0;background:var(--header-bg);backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);border-bottom:2px solid var(--border-accent);animation:slideInFromTop .6s ease-out;gap:16px}.logo{height:24px;width:auto;flex-shrink:0}h1{font-size:18px;font-weight:600;letter-spacing:-.5px;color:var(--text-primary);flex:1;text-align:center;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;min-width:0;max-width:500px;margin:0 auto}.session-info{display:flex;gap:12px;align-items:center;font-size:13px;color:var(--text-secondary);flex-shrink:0}.theme-toggle{background:transparent;border:none;padding:4px;cursor:pointer;transition:all .2s ease;display:flex;align-items:center;justify-content:center;color:var(--text-secondary);flex-shrink:0;width:24px;height:24px}.theme-toggle:hover{color:var(--text-primary);transform:scale(1.1)}.theme-toggle svg{width:20px;height:20px;fill:currentColor}.session-info span{display:flex;align-items:center;gap:6px}.status-indicator{color:#4ade80;font-size:16px;animation:pulse 2s ease-in-out infinite}main{flex:1;padding-top:40px;min-height:0}.panes-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(380px,1fr));gap:24px;margin-bottom:40px}.pane-card{background:var(--card-bg);backdrop-filter:blur(20px);-webkit-backdrop-filter:blur(20px);border:1px solid var(--card-border);border-radius:12px;padding:12px;position:relative;animation:fadeIn .5s ease-out backwards;color:inherit;display:block;min-width:0;overflow:hidden}.pane-card:nth-child(1){animation-delay:.1s}.pane-card:nth-child(2){animation-delay:.15s}.pane-card:nth-child(3){animation-delay:.2s}.pane-card:nth-child(4){animation-delay:.25s}.pane-card:nth-child(5){animation-delay:.3s}.pane-card:nth-child(6){animation-delay:.35s}.pane-header{margin-bottom:16px;display:flex;align-items:flex-start;justify-content:space-between;gap:12px;position:relative}.pane-header-content{flex:1;display:flex;flex-direction:column;gap:6px}.action-menu-btn{background:transparent;border:none;color:var(--text-tertiary);font-size:20px;padding:4px 8px;cursor:pointer;transition:all .2s ease;line-height:1;flex-shrink:0}.action-menu-btn:hover{color:var(--text-primary);background:var(--button-hover-bg);border-radius:4px}.action-menu-dropdown{position:absolute;top:32px;right:0;background:var(--tooltip-bg);backdrop-filter:blur(20px);-webkit-backdrop-filter:blur(20px);border:1px solid var(--tooltip-border);border-radius:8px;padding:4px;z-index:100;min-width:180px;box-shadow:0 8px 24px #0000004d;animation:fadeIn .2s ease-out}.action-menu-item{width:100%;display:flex;align-items:center;gap:8px;padding:8px 12px;background:transparent;border:none;border-radius:4px;color:var(--text-primary);font-size:13px;cursor:pointer;transition:all .15s ease;text-align:left}.action-menu-item:hover:not(:disabled){background:var(--button-hover-bg)}.action-menu-item:disabled{opacity:.5;cursor:not-allowed}.action-icon{font-size:14px;width:16px;text-align:center}.action-label{flex:1}.pane-title-link{display:inline-flex;align-items:center;gap:8px;text-decoration:none;color:inherit;width:fit-content}.pane-title-link:hover .pane-title{text-decoration:underline}.pane-title{font-size:20px;font-weight:600;color:var(--text-bright);letter-spacing:-.3px}.pane-arrow{font-size:16px;color:var(--text-secondary);transition:all .2s ease;opacity:.6}.pane-title-link:hover .pane-arrow{color:#ff8c00;transform:translate(2px);opacity:1}.pane-meta{display:flex;align-items:center;gap:12px}.pane-agent{padding:2px 8px;border-radius:4px;font-size:10px;font-weight:600;text-transform:uppercase;letter-spacing:.3px;background:var(--agent-bg);border:1px solid var(--agent-border);color:var(--text-tertiary);white-space:nowrap}.pane-agent.claude{background:#d9775726;border-color:#d977574d;color:#d97757}.pane-agent.opencode{background:#667eea26;border-color:#667eea4d;color:#667eea}.pane-prompt-section{margin-bottom:12px}.prompt-label{font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.5px;color:var(--text-tertiary);transition:color .2s ease}.pane-prompt-preview{display:flex;flex-direction:column;gap:4px;cursor:pointer;transition:all .2s ease}.pane-prompt-preview:hover .prompt-text{color:var(--text-primary)}.pane-prompt-preview:hover .prompt-label{color:var(--text-secondary)}.prompt-header{display:flex;align-items:center;gap:6px}.prompt-text{color:var(--text-secondary);font-size:13px;line-height:1.4;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;transition:color .2s ease}.pane-prompt-preview.expanded .prompt-text{white-space:normal;display:none}.expand-icon{font-size:10px;color:var(--text-tertiary);transition:transform .2s ease;flex-shrink:0}.pane-prompt-full{color:var(--text-secondary);font-size:13px;margin-top:4px;line-height:1.6;font-family:SF Mono,Monaco,Courier New,monospace;word-wrap:break-word;overflow-wrap:break-word;word-break:break-word;max-width:100%}.agent-summary{color:var(--text-secondary);font-size:13px;margin-bottom:12px;padding:10px 12px;line-height:1.5;background:#60a5fa14;border:1px solid rgba(96,165,250,.2);border-radius:6px;font-style:italic}.analyzer-error{color:#ef4444;font-size:13px;margin-bottom:12px;padding:10px 12px;line-height:1.5;background:#ef444414;border:1px solid rgba(239,68,68,.2);border-radius:6px;font-weight:500}.tooltip{position:absolute;background:var(--tooltip-bg);backdrop-filter:blur(20px);-webkit-backdrop-filter:blur(20px);border:1px solid var(--tooltip-border);padding:16px;border-radius:12px;z-index:1000;white-space:pre-wrap;max-width:400px;max-height:200px;overflow-y:auto;box-shadow:0 20px 60px #0000004d,0 0 0 1px var(--border-color);font-size:13px;color:var(--text-primary);pointer-events:none;animation:fadeIn .2s ease-out}.pane-status{display:flex;flex-direction:column;gap:10px}.status-item{display:flex;justify-content:space-between;align-items:center;font-size:13px;padding:8px 0}.status-label{color:var(--text-tertiary);font-weight:500}.status-value{display:flex;align-items:center;gap:6px}.status-badge{padding:4px 10px;border-radius:6px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.3px;transition:all .2s ease}.status-badge.working{background:linear-gradient(135deg,#fbbf24,#f59e0b);color:#000;box-shadow:0 2px 8px #fbbf2466}.status-badge.waiting{background:linear-gradient(135deg,#60a5fa,#3b82f6);color:#000;box-shadow:0 2px 8px #60a5fa66}.status-badge.idle{background:var(--idle-badge-bg);color:var(--text-tertiary);border:1px solid var(--idle-badge-border)}.status-badge.running,.status-badge.passed{background:linear-gradient(135deg,#4ade80,#22c55e);color:#000;box-shadow:0 2px 8px #4ade8066}.status-badge.failed{background:linear-gradient(135deg,#f87171,#ef4444);color:#000;box-shadow:0 2px 8px #f8717166}.status-badge.analyzing{background:linear-gradient(135deg,#a78bfa,#8b5cf6);color:#000;box-shadow:0 2px 8px #a78bfa66;animation:pulse 2s ease-in-out infinite}.pane-id{font-family:SF Mono,Monaco,monospace;font-size:10px;color:var(--text-dimmer);font-weight:500;letter-spacing:.2px}.pane-interactive{margin-top:12px}.options-dialog{display:flex;flex-direction:column;gap:12px}.options-question{font-size:14px;font-weight:500;color:var(--text-primary);line-height:1.4}.options-warning{padding:8px 12px;background:#f871711a;border:1px solid rgba(248,113,113,.3);border-radius:6px;color:#fca5a5;font-size:12px;display:flex;align-items:center;gap:6px}.options-buttons{display:flex;flex-wrap:wrap;gap:8px}.option-button{padding:8px 16px;background:linear-gradient(135deg,#60a5fa,#3b82f6);color:#000;border:none;border-radius:6px;font-size:13px;font-weight:600;cursor:pointer;transition:all .2s ease;box-shadow:0 2px 8px #60a5fa4d}.option-button:hover{transform:translateY(-2px);box-shadow:0 4px 12px #60a5fa66}.option-button:active{transform:translateY(0)}.option-button-danger{background:linear-gradient(135deg,#f87171,#ef4444);box-shadow:0 2px 8px #f871714d}.option-button-danger:hover{box-shadow:0 4px 12px #f8717166}.analyzing-state{display:flex;align-items:center;gap:12px;padding:8px 0;color:#a78bfa;font-size:14px;font-weight:500}.loader-spinner{width:20px;height:20px;border:3px solid rgba(167,139,250,.2);border-top-color:#a78bfa;border-radius:50%;animation:spin 1s linear infinite}.prompt-input-wrapper{display:flex;align-items:flex-start;gap:8px;padding:8px;background:var(--input-bg);border:1px solid var(--input-border);border-radius:8px;transition:all .2s ease}.prompt-input-wrapper:focus-within{border-color:var(--input-focus-border);background:var(--input-focus-bg);box-shadow:0 0 0 3px var(--input-focus-shadow)}.queued-message{margin-top:8px;padding:6px 10px;background:#4ade801a;border:1px solid rgba(74,222,128,.3);border-radius:6px;color:#4ade80;font-size:12px;animation:fadeIn .3s ease-out}.prompt-textarea{flex:1;min-height:20px;max-height:150px;padding:0;background:transparent;border:none;color:var(--text-primary);font-family:SF Mono,Monaco,Courier New,monospace;font-size:13px;line-height:1.4;resize:none;overflow-y:auto}.prompt-textarea:focus{outline:none}.prompt-textarea:disabled{opacity:.5;cursor:not-allowed}.prompt-textarea::placeholder{color:var(--text-dimmer)}.send-button{flex-shrink:0;width:28px;height:28px;padding:6px;background:var(--button-bg);color:var(--text-secondary);border:1px solid var(--button-border);border-radius:50%;cursor:pointer;transition:all .2s ease;display:flex;align-items:center;justify-content:center}.send-button:hover:not(:disabled){background:var(--button-hover-bg);border-color:var(--button-hover-border)}.send-button:active:not(:disabled){transform:scale(.92)}.send-button:disabled{opacity:.3;cursor:not-allowed}.send-button svg{width:100%;height:100%;fill:currentColor}.button-loader{width:14px;height:14px;border:2px solid rgba(0,0,0,.2);border-top-color:#000;border-radius:50%;animation:spin .8s linear infinite}.dev-server-status{margin-top:12px;padding-top:12px;border-top:1px solid rgba(255,255,255,.08);display:flex;align-items:center;gap:8px;font-size:12px}.dev-link{color:#ff8c00;text-decoration:none;font-weight:600;transition:color .2s ease}.dev-link:hover{color:orange}.no-panes{text-align:center;padding:100px 20px;color:var(--text-tertiary);animation:fadeIn .6s ease-out}.no-panes p{margin-bottom:16px;font-size:18px;font-weight:500}.hint{font-size:14px;color:var(--text-dim);background:var(--hint-bg);padding:12px 24px;border-radius:12px;display:inline-block;margin-top:8px}footer{padding:12px 0;margin-top:auto;animation:fadeIn .8s ease-out}.footer-info{display:flex;justify-content:space-between;font-size:11px;color:var(--text-dim);padding:0}.footer-info span{display:flex;align-items:center;gap:8px}@media (max-width: 768px){.container{padding:0 16px 24px}header{padding:12px 18px;gap:8px}.logo{height:20px}h1{font-size:14px;max-width:none}.session-info{font-size:11px;gap:8px}.session-info span:not(.status-indicator){display:none}main{padding-top:24px}.panes-grid{grid-template-columns:1fr;gap:16px}.footer-info{flex-direction:column;gap:6px;font-size:10px}}.term-fg-black{color:#000}.term-fg-red{color:#cd3131}.term-fg-green{color:#0dbc79}.term-fg-yellow{color:#e5e510}.term-fg-blue{color:#2472c8}.term-fg-magenta{color:#bc3fbc}.term-fg-cyan{color:#11a8cd}.term-fg-white{color:#e5e5e5}.term-fg-bright-black{color:#666}.term-fg-bright-red{color:#f14c4c}.term-fg-bright-green{color:#23d18b}.term-fg-bright-yellow{color:#f5f543}.term-fg-bright-blue{color:#3b8eea}.term-fg-bright-magenta{color:#d670d6}.term-fg-bright-cyan{color:#29b8db}.term-fg-bright-white{color:#fff}.term-bg-black{background-color:#000}.term-bg-red{background-color:#cd3131}.term-bg-green{background-color:#0dbc79}.term-bg-yellow{background-color:#e5e510}.term-bg-blue{background-color:#2472c8}.term-bg-magenta{background-color:#bc3fbc}.term-bg-cyan{background-color:#11a8cd}.term-bg-white{background-color:#e5e5e5}.term-bold{font-weight:700}.term-dim{opacity:.7}.term-italic{font-style:italic}.term-underline{text-decoration:underline}.term-strikethrough{text-decoration:line-through}.action-dialog-overlay{position:fixed;top:0;left:0;right:0;bottom:0;background:#000000b3;backdrop-filter:blur(4px);-webkit-backdrop-filter:blur(4px);display:flex;align-items:center;justify-content:center;z-index:1000;animation:fadeIn .2s ease-out}.action-dialog{background:var(--card-bg);backdrop-filter:blur(20px);-webkit-backdrop-filter:blur(20px);border:1px solid var(--card-border);border-radius:16px;padding:32px;max-width:540px;width:90%;box-shadow:0 20px 60px #0006;animation:slideInFromTop .3s ease-out}.action-dialog h3{margin:0 0 20px;font-size:24px;font-weight:600;color:var(--text-bright);letter-spacing:-.5px}.action-dialog p,.action-dialog .dialog-message{margin:0 0 20px;color:var(--text-secondary);font-size:14px;line-height:1.5;white-space:pre-wrap;font-family:SF Mono,Monaco,Cascadia Code,Roboto Mono,Consolas,Courier New,monospace}.action-dialog label{display:block;font-size:14px;font-weight:600;color:var(--text-primary);margin-bottom:10px;letter-spacing:-.2px}.action-dialog textarea{width:100%;padding:14px 16px;background:var(--input-bg);border:2px solid var(--input-border);border-radius:10px;color:var(--text-primary);font-family:-apple-system,BlinkMacSystemFont,SF Pro Display,SF Pro Text,Segoe UI,Roboto,sans-serif;font-size:15px;line-height:1.5;resize:vertical;transition:all .2s ease;margin-bottom:20px}.action-dialog textarea:focus{outline:none;border-color:var(--input-focus-border);background:var(--input-focus-bg);box-shadow:0 0 0 4px var(--input-focus-shadow)}.action-dialog textarea::placeholder{color:var(--text-dim);font-style:italic}.dialog-hint{font-size:13px;color:var(--text-tertiary);margin-bottom:16px;display:flex;align-items:center;gap:6px}.dialog-hint kbd{background:var(--button-bg);border:1px solid var(--button-border);border-radius:4px;padding:2px 6px;font-family:SF Mono,Monaco,monospace;font-size:11px;font-weight:600;color:var(--text-primary);box-shadow:0 1px 2px #0000001a}.dialog-loading{display:flex;align-items:center;justify-content:center;gap:12px;padding:24px;color:var(--text-secondary);font-size:15px;font-weight:500}.dialog-loading .loader-spinner{width:24px;height:24px;border:3px solid rgba(96,165,250,.2);border-top-color:#60a5fa;border-radius:50%;animation:spin .8s linear infinite}.dialog-input{width:100%;padding:14px 16px;background:var(--input-bg);border:2px solid var(--input-border);border-radius:10px;color:var(--text-primary);font-family:-apple-system,BlinkMacSystemFont,SF Pro Display,SF Pro Text,Segoe UI,Roboto,sans-serif;font-size:15px;line-height:1.5;transition:all .2s ease;margin-bottom:20px}.dialog-input:focus{outline:none;border-color:var(--input-focus-border);background:var(--input-focus-bg);box-shadow:0 0 0 4px var(--input-focus-shadow)}.dialog-input::placeholder{color:var(--text-dim);font-style:italic}.dialog-buttons{display:flex;gap:12px;justify-content:flex-end}.dialog-btn{padding:12px 24px;border:2px solid var(--button-border);border-radius:10px;background:var(--button-bg);color:var(--text-primary);font-size:15px;font-weight:600;cursor:pointer;transition:all .2s ease}.dialog-btn:hover{background:var(--button-hover-bg);border-color:var(--button-hover-border);transform:translateY(-1px)}.dialog-btn:active{transform:translateY(0)}.dialog-btn:disabled{opacity:.5;cursor:not-allowed;transform:none}.dialog-btn-primary{background:linear-gradient(135deg,#60a5fa,#3b82f6);color:#000;border-color:transparent;box-shadow:0 2px 8px #60a5fa4d}.dialog-btn-primary:hover{transform:translateY(-2px);box-shadow:0 6px 16px #60a5fa80}.dialog-btn-primary:active{transform:translateY(-1px);box-shadow:0 4px 12px #60a5fa66}.dialog-btn-primary:disabled{opacity:.6;transform:none;box-shadow:0 2px 8px #60a5fa33}.choice-options{display:flex;flex-direction:column;gap:8px}.choice-option-btn{width:100%;padding:12px 16px;background:var(--button-bg);border:1px solid var(--button-border);border-radius:8px;color:var(--text-primary);font-size:14px;text-align:left;cursor:pointer;transition:all .2s ease;display:flex;flex-direction:column;gap:4px}.choice-option-btn:hover{background:var(--button-hover-bg);border-color:var(--button-hover-border);transform:translate(4px)}.choice-option-btn.danger{border-color:#f871714d;background:#f871711a}.choice-option-btn.danger:hover{border-color:#f8717180;background:#f8717133}.agent-choices{display:grid;grid-template-columns:repeat(auto-fit,minmax(140px,1fr));gap:12px;margin-bottom:20px}.agent-choice-button{padding:16px 20px;background:var(--button-bg);border:2px solid var(--button-border);border-radius:12px;color:var(--text-primary);font-size:15px;font-weight:600;cursor:pointer;transition:all .2s ease;text-align:center;text-transform:capitalize;position:relative;overflow:hidden}.agent-choice-button:before{content:"";position:absolute;top:0;left:0;right:0;bottom:0;background:linear-gradient(135deg,#60a5fa1a,#3b82f61a);opacity:0;transition:opacity .2s ease}.agent-choice-button:hover{background:var(--button-hover-bg);border-color:var(--input-focus-border);transform:translateY(-2px);box-shadow:0 4px 12px #60a5fa33}.agent-choice-button:hover:before{opacity:1}.agent-choice-button:active{transform:translateY(0)}.agent-choice-button:disabled{opacity:.5;cursor:not-allowed;transform:none}.option-description{font-size:12px;color:var(--text-tertiary);font-weight:400}.terminal-page{display:flex;flex-direction:column;height:100vh;background:#000}.back-button{color:#e0e0e0;text-decoration:none;font-size:14px;font-weight:500;transition:color .2s;white-space:nowrap;flex-shrink:0}.back-button:hover{color:#fff}.terminal-content{flex:1;overflow:auto;padding:10px}.terminal-page .terminal-output{font-family:JetBrains Mono,SF Mono,Monaco,Cascadia Code,Roboto Mono,monospace;line-height:1.2;color:#f0f0f0;margin:0;min-height:100%}.terminal-row{white-space:pre;margin:0;padding:0;line-height:1.2}.mobile-toolbar{display:flex;gap:6px;padding:8px;background:#1a1a1a;border-bottom:1px solid #333;overflow-x:auto;flex-wrap:nowrap}.toolbar-key{background:#2d2d2d;border:1px solid #444;border-radius:4px;color:#e0e0e0;padding:8px 12px;font-size:13px;font-family:SF Mono,Monaco,monospace;cursor:pointer;flex-shrink:0;min-width:44px;transition:all .15s;-webkit-user-select:none;user-select:none;-webkit-tap-highlight-color:transparent}.toolbar-key:active{background:#3d3d3d;transform:scale(.95)}.toolbar-key.active{background:#667eea;border-color:#667eea;color:#fff}.mobile-input{position:absolute;left:-9999px;width:1px;height:1px;opacity:.01;pointer-events:none}.actions-bar{display:flex;justify-content:flex-end;margin-bottom:20px;padding:0 4px}.create-pane-button{display:flex;align-items:center;gap:8px;padding:10px 20px;background:linear-gradient(135deg,#60a5fa,#3b82f6);color:#000;border:none;border-radius:8px;font-size:14px;font-weight:600;cursor:pointer;transition:all .2s ease;box-shadow:0 2px 8px #60a5fa4d}.create-pane-button:hover:not(:disabled){transform:translateY(-1px);box-shadow:0 4px 12px #60a5fa66}.create-pane-button:disabled{opacity:.5;cursor:not-allowed;transform:none}.create-pane-button svg{width:16px;height:16px;fill:none;stroke-width:2.5}.modal-overlay{position:fixed;top:0;left:0;width:100%;height:100%;background:#000000b3;backdrop-filter:blur(8px);-webkit-backdrop-filter:blur(8px);display:flex;align-items:center;justify-content:center;z-index:2000;animation:fadeIn .2s ease-out}.modal-dialog{background:var(--card-bg);backdrop-filter:blur(20px);-webkit-backdrop-filter:blur(20px);border:1px solid var(--card-border);border-radius:16px;width:90%;max-width:600px;max-height:90vh;overflow:hidden;display:flex;flex-direction:column;box-shadow:0 20px 60px #0006;animation:slideUp .3s ease-out}@keyframes slideUp{0%{opacity:0;transform:translateY(20px)}to{opacity:1;transform:translateY(0)}}.modal-header{display:flex;align-items:center;justify-content:space-between;padding:20px 24px;border-bottom:1px solid var(--border-color)}.modal-header h2{font-size:20px;font-weight:600;color:var(--text-primary);margin:0}.modal-close{background:none;border:none;color:var(--text-secondary);font-size:32px;line-height:1;cursor:pointer;padding:0;width:32px;height:32px;display:flex;align-items:center;justify-content:center;border-radius:6px;transition:all .2s ease}.modal-close:hover{background:var(--button-hover-bg);color:var(--text-primary)}.modal-body{padding:24px;overflow-y:auto;flex:1}.form-group{margin-bottom:20px}.form-group:last-child{margin-bottom:0}.form-group label{display:block;font-size:14px;font-weight:600;color:var(--text-primary);margin-bottom:8px}.modal-textarea{width:100%;padding:12px;background:var(--input-bg);border:1px solid var(--input-border);border-radius:8px;color:var(--text-primary);font-size:14px;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,sans-serif;resize:vertical;min-height:100px;transition:all .2s ease}.modal-textarea:focus{outline:none;border-color:var(--input-focus-border);background:var(--input-focus-bg);box-shadow:0 0 0 3px var(--input-focus-shadow)}.modal-textarea:disabled{opacity:.5;cursor:not-allowed}.input-hint{font-size:12px;color:var(--text-tertiary);margin-top:6px}.agent-selector{display:flex;gap:12px}.agent-option{flex:1;display:flex;align-items:center;gap:12px;padding:12px 20px;background:var(--button-bg);border:2px solid var(--button-border);border-radius:8px;color:var(--text-primary);font-size:14px;font-weight:600;text-transform:capitalize;cursor:pointer;transition:all .2s ease}.agent-logo{width:40px;height:40px;flex-shrink:0}.agent-option:hover:not(:disabled){background:var(--button-hover-bg);border-color:var(--button-hover-border)}.agent-option.selected{background:var(--input-focus-bg);border-color:var(--input-focus-border);color:var(--text-bright)}.agent-option:disabled{opacity:.5;cursor:not-allowed}.modal-footer{display:flex;gap:12px;padding:20px 24px;border-top:1px solid var(--border-color);justify-content:flex-end}.modal-button{padding:10px 24px;border-radius:8px;font-size:14px;font-weight:600;cursor:pointer;transition:all .2s ease;border:none;display:flex;align-items:center;gap:8px}.modal-button-secondary{background:var(--button-bg);color:var(--text-primary);border:1px solid var(--button-border)}.modal-button-secondary:hover:not(:disabled){background:var(--button-hover-bg);border-color:var(--button-hover-border)}.modal-button-primary{background:var(--input-focus-bg);color:var(--text-bright);border:1px solid var(--input-focus-border)}.modal-button-primary:hover:not(:disabled){background:var(--button-hover-bg);border-color:var(--button-hover-border)}.modal-button:disabled{opacity:.5;cursor:not-allowed}.button-loader{width:16px;height:16px;border:2px solid rgba(255,255,255,.3);border-top-color:#fff;border-radius:50%;animation:spin .6s linear infinite}@keyframes spin{to{transform:rotate(360deg)}}.settings-button{display:flex;align-items:center;gap:8px;padding:10px 20px;background:var(--button-bg);color:var(--text-primary);border:1px solid var(--button-border);border-radius:8px;font-size:14px;font-weight:600;cursor:pointer;transition:all .2s ease;margin-left:12px}.settings-button:hover:not(:disabled){background:var(--button-hover-bg);border-color:var(--button-hover-border);transform:translateY(-1px)}.settings-button:disabled{opacity:.5;cursor:not-allowed;transform:none}.settings-button svg{width:16px;height:16px;fill:currentColor}.settings-dialog{max-width:700px}.settings-list{display:flex;flex-direction:column;gap:24px;margin-bottom:24px;max-height:500px;overflow-y:auto}.setting-item{border-bottom:1px solid var(--border-color);padding-bottom:20px}.setting-item:last-child{border-bottom:none;padding-bottom:0}.setting-header{margin-bottom:12px}.setting-info{display:flex;flex-direction:column;gap:4px}.setting-label{font-size:16px;font-weight:600;color:var(--text-primary);letter-spacing:-.2px}.setting-description{font-size:13px;color:var(--text-secondary);line-height:1.5}.setting-control{display:flex;flex-direction:column;gap:12px}.setting-value{font-size:14px;color:var(--text-secondary);display:flex;align-items:center;gap:8px}.setting-value strong{color:var(--text-bright);font-weight:600}.setting-scope{font-size:11px;color:var(--text-tertiary);padding:2px 6px;background:var(--agent-bg);border:1px solid var(--agent-border);border-radius:4px;font-weight:600;text-transform:uppercase;letter-spacing:.3px}.setting-buttons{display:flex;flex-wrap:wrap;gap:8px}.setting-btn{padding:8px 16px;background:var(--button-bg);color:var(--text-primary);border:1px solid var(--button-border);border-radius:6px;font-size:13px;font-weight:600;cursor:pointer;transition:all .2s ease}.setting-btn:hover:not(:disabled){background:var(--button-hover-bg);border-color:var(--button-hover-border);transform:translateY(-1px)}.setting-btn:disabled{opacity:.5;cursor:not-allowed;transform:none}.setting-select-group{padding-left:16px;border-left:2px solid var(--border-color);display:flex;flex-direction:column;gap:8px}.setting-option-label{font-size:13px;color:var(--text-secondary);font-weight:500}.setting-btn-action{background:linear-gradient(135deg,#60a5fa,#3b82f6);color:#000;border-color:transparent;box-shadow:0 2px 8px #60a5fa4d}.setting-btn-action:hover:not(:disabled){transform:translateY(-2px);box-shadow:0 4px 12px #60a5fa66}.hooks-section{margin-top:24px;padding-top:24px;border-top:1px solid var(--border-color)}.hooks-header{display:flex;align-items:center;gap:16px;margin-bottom:20px}.hooks-header h4{font-size:18px;font-weight:600;color:var(--text-primary);margin:0;flex:1}.back-btn{background:transparent;border:none;color:var(--text-secondary);font-size:14px;cursor:pointer;padding:4px 8px;border-radius:4px;transition:all .2s ease;display:flex;align-items:center;gap:4px}.back-btn:hover{background:var(--button-hover-bg);color:var(--text-primary)}.hooks-list{display:flex;flex-direction:column;gap:12px}.hook-item{display:flex;justify-content:space-between;align-items:center;padding:12px 16px;background:var(--input-bg);border:1px solid var(--input-border);border-radius:8px;transition:all .2s ease}.hook-item:hover{background:var(--input-focus-bg);border-color:var(--input-focus-border)}.hook-name{font-family:SF Mono,Monaco,Courier New,monospace;font-size:13px;color:var(--text-primary);font-weight:500}.hook-status{font-size:12px;font-weight:600;color:var(--text-tertiary);padding:4px 10px;background:var(--idle-badge-bg);border:1px solid var(--idle-badge-border);border-radius:6px;text-transform:uppercase;letter-spacing:.3px}.hook-status.hook-active{background:#4ade801a;border-color:#4ade804d;color:#4ade80}.hooks-actions{margin-top:20px;padding-top:20px;border-top:1px solid var(--border-color);display:flex;justify-content:center}