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.
- package/README.md +12 -6
- package/dist/DmuxApp.d.ts.map +1 -1
- package/dist/DmuxApp.js +119 -183
- package/dist/DmuxApp.js.map +1 -1
- package/dist/actions/implementations/closeAction.d.ts.map +1 -1
- package/dist/actions/implementations/closeAction.js +15 -40
- package/dist/actions/implementations/closeAction.js.map +1 -1
- package/dist/actions/types.d.ts +0 -1
- package/dist/actions/types.d.ts.map +1 -1
- package/dist/actions/types.js +7 -0
- package/dist/actions/types.js.map +1 -1
- package/dist/components/panes/PanesGrid.d.ts +2 -0
- package/dist/components/panes/PanesGrid.d.ts.map +1 -1
- package/dist/components/panes/PanesGrid.js +122 -42
- package/dist/components/panes/PanesGrid.js.map +1 -1
- package/dist/components/popups/agentChoicePopup.d.ts +1 -1
- package/dist/components/popups/agentChoicePopup.js +37 -23
- package/dist/components/popups/agentChoicePopup.js.map +1 -1
- package/dist/components/popups/newPanePopup.js +4 -3
- package/dist/components/popups/newPanePopup.js.map +1 -1
- package/dist/components/popups/shortcutsPopup.js +6 -4
- package/dist/components/popups/shortcutsPopup.js.map +1 -1
- package/dist/components/ui/FooterHelp.d.ts +0 -8
- package/dist/components/ui/FooterHelp.d.ts.map +1 -1
- package/dist/components/ui/FooterHelp.js +5 -24
- package/dist/components/ui/FooterHelp.js.map +1 -1
- package/dist/hooks/useActionSystem.d.ts +1 -2
- package/dist/hooks/useActionSystem.d.ts.map +1 -1
- package/dist/hooks/useActionSystem.js +9 -25
- package/dist/hooks/useActionSystem.js.map +1 -1
- package/dist/hooks/useAgentDetection.d.ts +2 -1
- package/dist/hooks/useAgentDetection.d.ts.map +1 -1
- package/dist/hooks/useAgentDetection.js.map +1 -1
- package/dist/hooks/useAgentStatus.d.ts.map +1 -1
- package/dist/hooks/useAgentStatus.js +18 -6
- package/dist/hooks/useAgentStatus.js.map +1 -1
- package/dist/hooks/useInputHandling.d.ts +6 -9
- package/dist/hooks/useInputHandling.d.ts.map +1 -1
- package/dist/hooks/useInputHandling.js +91 -69
- package/dist/hooks/useInputHandling.js.map +1 -1
- package/dist/hooks/useLayoutManagement.d.ts +1 -2
- package/dist/hooks/useLayoutManagement.d.ts.map +1 -1
- package/dist/hooks/useLayoutManagement.js +2 -9
- package/dist/hooks/useLayoutManagement.js.map +1 -1
- package/dist/hooks/useNavigation.d.ts +1 -1
- package/dist/hooks/useNavigation.d.ts.map +1 -1
- package/dist/hooks/useNavigation.js +29 -36
- package/dist/hooks/useNavigation.js.map +1 -1
- package/dist/hooks/usePaneCreation.d.ts +10 -4
- package/dist/hooks/usePaneCreation.d.ts.map +1 -1
- package/dist/hooks/usePaneCreation.js +20 -49
- package/dist/hooks/usePaneCreation.js.map +1 -1
- package/dist/hooks/usePaneLoading.d.ts.map +1 -1
- package/dist/hooks/usePaneLoading.js +21 -23
- package/dist/hooks/usePaneLoading.js.map +1 -1
- package/dist/hooks/usePaneRunner.d.ts +1 -1
- package/dist/hooks/usePaneRunner.d.ts.map +1 -1
- package/dist/hooks/usePaneRunner.js +5 -2
- package/dist/hooks/usePaneRunner.js.map +1 -1
- package/dist/hooks/usePaneSync.d.ts.map +1 -1
- package/dist/hooks/usePaneSync.js +29 -20
- package/dist/hooks/usePaneSync.js.map +1 -1
- package/dist/hooks/usePanes.d.ts.map +1 -1
- package/dist/hooks/usePanes.js +61 -65
- package/dist/hooks/usePanes.js.map +1 -1
- package/dist/hooks/useServices.d.ts +4 -7
- package/dist/hooks/useServices.d.ts.map +1 -1
- package/dist/hooks/useServices.js +0 -4
- package/dist/hooks/useServices.js.map +1 -1
- package/dist/hooks/useTerminalWidth.d.ts.map +1 -1
- package/dist/hooks/useTerminalWidth.js +0 -14
- package/dist/hooks/useTerminalWidth.js.map +1 -1
- package/dist/hooks/useWorktreeActions.d.ts +1 -2
- package/dist/hooks/useWorktreeActions.d.ts.map +1 -1
- package/dist/hooks/useWorktreeActions.js +2 -8
- package/dist/hooks/useWorktreeActions.js.map +1 -1
- package/dist/index.js +260 -51
- package/dist/index.js.map +1 -1
- package/dist/server/embedded-assets.d.ts.map +1 -1
- package/dist/server/embedded-assets.js +381 -236
- package/dist/server/embedded-assets.js.map +1 -1
- package/dist/server/routes/panesRoutes.d.ts.map +1 -1
- package/dist/server/routes/panesRoutes.js +16 -2
- package/dist/server/routes/panesRoutes.js.map +1 -1
- package/dist/services/PopupManager.d.ts +5 -7
- package/dist/services/PopupManager.d.ts.map +1 -1
- package/dist/services/PopupManager.js +8 -42
- package/dist/services/PopupManager.js.map +1 -1
- package/dist/types.d.ts +2 -5
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/agentLaunch.d.ts +12 -0
- package/dist/utils/agentLaunch.d.ts.map +1 -0
- package/dist/utils/agentLaunch.js +56 -0
- package/dist/utils/agentLaunch.js.map +1 -0
- package/dist/utils/paneCreation.d.ts +4 -0
- package/dist/utils/paneCreation.d.ts.map +1 -1
- package/dist/utils/paneCreation.js +33 -17
- package/dist/utils/paneCreation.js.map +1 -1
- package/dist/utils/paneGrouping.d.ts +15 -0
- package/dist/utils/paneGrouping.d.ts.map +1 -0
- package/dist/utils/paneGrouping.js +24 -0
- package/dist/utils/paneGrouping.js.map +1 -0
- package/dist/utils/paneProject.d.ts +16 -0
- package/dist/utils/paneProject.d.ts.map +1 -0
- package/dist/utils/paneProject.js +40 -0
- package/dist/utils/paneProject.js.map +1 -0
- package/dist/utils/paneRebinding.d.ts.map +1 -1
- package/dist/utils/paneRebinding.js +13 -7
- package/dist/utils/paneRebinding.js.map +1 -1
- package/dist/utils/paneTitle.d.ts +13 -0
- package/dist/utils/paneTitle.d.ts.map +1 -0
- package/dist/utils/paneTitle.js +48 -0
- package/dist/utils/paneTitle.js.map +1 -0
- package/dist/utils/projectActions.d.ts +32 -0
- package/dist/utils/projectActions.d.ts.map +1 -0
- package/dist/utils/projectActions.js +108 -0
- package/dist/utils/projectActions.js.map +1 -0
- package/dist/utils/projectRoot.d.ts +10 -0
- package/dist/utils/projectRoot.d.ts.map +1 -0
- package/dist/utils/projectRoot.js +66 -0
- package/dist/utils/projectRoot.js.map +1 -0
- package/dist/utils/reopenWorktree.d.ts +2 -0
- package/dist/utils/reopenWorktree.d.ts.map +1 -1
- package/dist/utils/reopenWorktree.js +14 -4
- package/dist/utils/reopenWorktree.js.map +1 -1
- package/dist/utils/shellPaneDetection.d.ts.map +1 -1
- package/dist/utils/shellPaneDetection.js +21 -0
- package/dist/utils/shellPaneDetection.js.map +1 -1
- package/dist/utils/tmux.d.ts.map +1 -1
- package/dist/utils/tmux.js +0 -8
- package/dist/utils/tmux.js.map +1 -1
- package/dist/utils/tmuxConfigOnboarding.d.ts +19 -0
- package/dist/utils/tmuxConfigOnboarding.d.ts.map +1 -0
- package/dist/utils/tmuxConfigOnboarding.js +299 -0
- package/dist/utils/tmuxConfigOnboarding.js.map +1 -0
- package/dist/workers/panePollingWorker.js +0 -6
- package/dist/workers/panePollingWorker.js.map +1 -1
- 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 {
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
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
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
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
|
|
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(
|
|
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
|
|
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
|
|
346
|
+
await createPanesForAgents([settings.defaultAgent]);
|
|
388
347
|
}
|
|
389
348
|
else {
|
|
390
349
|
// Show agent choice popup
|
|
391
|
-
const
|
|
392
|
-
if (
|
|
393
|
-
await
|
|
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:
|
|
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
|
|
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:
|
|
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,
|
|
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
|
|
745
|
-
const
|
|
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:
|
|
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
|
|
854
|
-
|
|
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
|
|
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(
|
|
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 '\${
|
|
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 '\${
|
|
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 '\${
|
|
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 '\${
|
|
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 '\${
|
|
1380
|
-
execSync(\`tmux set-hook -u -t '\${
|
|
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:
|
|
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}
|