dmux 1.3.3 → 1.5.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 (38) hide show
  1. package/dist/AutoUpdater.d.ts +35 -0
  2. package/dist/AutoUpdater.d.ts.map +1 -0
  3. package/dist/AutoUpdater.js +225 -0
  4. package/dist/AutoUpdater.js.map +1 -0
  5. package/dist/BetterTextInput.d.ts +10 -0
  6. package/dist/BetterTextInput.d.ts.map +1 -0
  7. package/dist/BetterTextInput.js +177 -0
  8. package/dist/BetterTextInput.js.map +1 -0
  9. package/dist/CleanTextInput.d.ts +10 -0
  10. package/dist/CleanTextInput.d.ts.map +1 -0
  11. package/dist/CleanTextInput.js +572 -0
  12. package/dist/CleanTextInput.js.map +1 -0
  13. package/dist/DmuxApp.d.ts +2 -0
  14. package/dist/DmuxApp.d.ts.map +1 -1
  15. package/dist/DmuxApp.js +330 -165
  16. package/dist/DmuxApp.js.map +1 -1
  17. package/dist/EnhancedTextInput.d.ts.map +1 -1
  18. package/dist/EnhancedTextInput.js +155 -50
  19. package/dist/EnhancedTextInput.js.map +1 -1
  20. package/dist/GeminiTextInput.d.ts +12 -0
  21. package/dist/GeminiTextInput.d.ts.map +1 -0
  22. package/dist/GeminiTextInput.js +210 -0
  23. package/dist/GeminiTextInput.js.map +1 -0
  24. package/dist/MultilineTextInput.d.ts +10 -0
  25. package/dist/MultilineTextInput.d.ts.map +1 -0
  26. package/dist/MultilineTextInput.js +184 -0
  27. package/dist/MultilineTextInput.js.map +1 -0
  28. package/dist/SimpleGeminiInput.d.ts +12 -0
  29. package/dist/SimpleGeminiInput.d.ts.map +1 -0
  30. package/dist/SimpleGeminiInput.js +223 -0
  31. package/dist/SimpleGeminiInput.js.map +1 -0
  32. package/dist/StyledTextInput.d.ts +10 -0
  33. package/dist/StyledTextInput.d.ts.map +1 -0
  34. package/dist/StyledTextInput.js +10 -0
  35. package/dist/StyledTextInput.js.map +1 -0
  36. package/dist/index.js +48 -3
  37. package/dist/index.js.map +1 -1
  38. package/package.json +1 -1
package/dist/DmuxApp.js CHANGED
@@ -1,13 +1,14 @@
1
1
  import React, { useState, useEffect } from 'react';
2
2
  import { Box, Text, useInput, useApp } from 'ink';
3
- import TextInput from 'ink-text-input';
3
+ import CleanTextInput from './CleanTextInput.js';
4
+ import StyledTextInput from './StyledTextInput.js';
4
5
  import { execSync } from 'child_process';
5
6
  import fs from 'fs/promises';
6
7
  import path from 'path';
7
8
  import { createRequire } from 'module';
8
9
  const require = createRequire(import.meta.url);
9
10
  const packageJson = require('../package.json');
10
- const DmuxApp = ({ dmuxDir, panesFile, projectName, sessionName, settingsFile }) => {
11
+ const DmuxApp = ({ dmuxDir, panesFile, projectName, sessionName, settingsFile, autoUpdater }) => {
11
12
  const [panes, setPanes] = useState([]);
12
13
  const [selectedIndex, setSelectedIndex] = useState(0);
13
14
  const [showNewPaneDialog, setShowNewPaneDialog] = useState(false);
@@ -25,14 +26,23 @@ const DmuxApp = ({ dmuxDir, panesFile, projectName, sessionName, settingsFile })
25
26
  const [showFileCopyPrompt, setShowFileCopyPrompt] = useState(false);
26
27
  const [currentCommandType, setCurrentCommandType] = useState(null);
27
28
  const [runningCommand, setRunningCommand] = useState(false);
29
+ const [updateInfo, setUpdateInfo] = useState(null);
30
+ const [showUpdateDialog, setShowUpdateDialog] = useState(false);
31
+ const [isUpdating, setIsUpdating] = useState(false);
32
+ const [isLoading, setIsLoading] = useState(true);
28
33
  const { exit } = useApp();
29
34
  // Track terminal dimensions for responsive layout
30
35
  const [terminalWidth, setTerminalWidth] = useState(process.stdout.columns || 80);
31
36
  // Load panes and settings on mount and refresh periodically
32
37
  useEffect(() => {
38
+ // Load immediately for initial data
33
39
  loadPanes();
34
40
  loadSettings();
35
- const interval = setInterval(loadPanes, 2000);
41
+ // Start polling after a short delay to avoid competing with startup
42
+ let pollingInterval;
43
+ const startPolling = setTimeout(() => {
44
+ pollingInterval = setInterval(loadPanes, 3000); // Poll every 3 seconds instead of 2
45
+ }, 1000); // Wait 1 second before starting polling
36
46
  // Handle terminal resize
37
47
  const handleResize = () => {
38
48
  setTerminalWidth(process.stdout.columns || 80);
@@ -53,142 +63,202 @@ const DmuxApp = ({ dmuxDir, panesFile, projectName, sessionName, settingsFile })
53
63
  process.on('SIGINT', handleTermination);
54
64
  process.on('SIGTERM', handleTermination);
55
65
  return () => {
56
- clearInterval(interval);
66
+ clearTimeout(startPolling);
67
+ if (pollingInterval)
68
+ clearInterval(pollingInterval);
57
69
  process.stdout.removeListener('resize', handleResize);
58
70
  process.removeListener('SIGINT', handleTermination);
59
71
  process.removeListener('SIGTERM', handleTermination);
60
72
  };
61
73
  }, []);
74
+ // Auto-show new pane dialog when starting with no panes
75
+ useEffect(() => {
76
+ // Only show the dialog if:
77
+ // 1. We have no panes
78
+ // 2. We're not already showing the dialog
79
+ // 3. We're not showing any other dialogs or prompts
80
+ if (panes.length === 0 &&
81
+ !showNewPaneDialog &&
82
+ !showMergeConfirmation &&
83
+ !showCloseOptions &&
84
+ !showCommandPrompt &&
85
+ !showFileCopyPrompt &&
86
+ !showUpdateDialog &&
87
+ !isCreatingPane &&
88
+ !runningCommand &&
89
+ !isUpdating) {
90
+ setShowNewPaneDialog(true);
91
+ }
92
+ }, [panes.length, showNewPaneDialog, showMergeConfirmation, showCloseOptions, showCommandPrompt, showFileCopyPrompt, showUpdateDialog, isCreatingPane, runningCommand, isUpdating]);
93
+ // Check for updates periodically
94
+ useEffect(() => {
95
+ if (!autoUpdater)
96
+ return;
97
+ const checkForUpdates = async () => {
98
+ try {
99
+ const info = await autoUpdater.checkForUpdates();
100
+ if (await autoUpdater.shouldShowUpdateNotification(info)) {
101
+ setUpdateInfo(info);
102
+ setShowUpdateDialog(true);
103
+ }
104
+ }
105
+ catch {
106
+ // Silently ignore update check failures
107
+ }
108
+ };
109
+ // Delay initial update check to avoid slowing startup
110
+ const initialCheckTimer = setTimeout(() => {
111
+ checkForUpdates();
112
+ }, 3000); // Check after 3 seconds
113
+ // Check for updates every 6 hours
114
+ const updateInterval = setInterval(checkForUpdates, 6 * 60 * 60 * 1000);
115
+ return () => {
116
+ clearTimeout(initialCheckTimer);
117
+ clearInterval(updateInterval);
118
+ };
119
+ }, [autoUpdater]);
62
120
  // Monitor Claude status in all panes with proper dependency tracking
63
121
  useEffect(() => {
64
122
  if (panes.length === 0)
65
123
  return;
66
- const monitorClaudeStatus = async () => {
67
- // Monitor Claude Code status for all panes
68
- const updatedPanesWithNulls = await Promise.all(panes.map(async (pane) => {
69
- try {
70
- // Skip if recently checked (within 500ms to avoid overlapping checks)
71
- if (pane.lastClaudeCheck && Date.now() - pane.lastClaudeCheck < 500) {
72
- return pane;
73
- }
74
- // First check if pane exists before trying to capture
75
- const paneIds = execSync(`tmux list-panes -F '#{pane_id}'`, {
76
- encoding: 'utf-8',
77
- stdio: 'pipe'
78
- }).trim().split('\n');
79
- if (!paneIds.includes(pane.paneId)) {
80
- // Pane doesn't exist anymore, return unchanged
81
- return pane;
82
- }
83
- // Capture the last 30 lines of the pane for better detection
84
- const captureOutput = execSync(`tmux capture-pane -t '${pane.paneId}' -p -S -30`, { encoding: 'utf-8', stdio: 'pipe' });
85
- // Pattern detection for Claude Code states
86
- // Working patterns - Claude is processing
87
- // The ONLY reliable indicator is "(esc to interrupt)"
88
- const workingPatterns = [
89
- /esc to interrupt/i, // The ONLY reliable indicator that Claude is actively working
90
- ];
91
- // Extract last few lines to check for patterns
92
- const lines = captureOutput.split('\n');
93
- const lastLines = lines.slice(-10).join('\n');
94
- // Check if Claude's input box is present (waiting for input)
95
- const hasInputBox = /╭─+╮/.test(lastLines) && /╰─+╯/.test(lastLines) && /│\s+>\s+.*│/.test(lastLines);
96
- // Permission/attention patterns - needs user input (very forgiving)
97
- const attentionPatterns = [
98
- /\?\s*$/m, // Any line ending with a question mark
99
- /y\/n/i, // Any y/n prompt
100
- /yes.*no/i, // Yes or no prompts
101
- /\ballow\b.*\?/i,
102
- /\bapprove\b.*\?/i,
103
- /\bgrant\b.*\?/i,
104
- /\btrust\b.*\?/i,
105
- /\baccept\b.*\?/i,
106
- /\bcontinue\b.*\?/i,
107
- /\bproceed\b.*\?/i,
108
- /permission/i,
109
- /confirmation/i,
110
- /press.*enter/i,
111
- /waiting for/i,
112
- /are you sure/i,
113
- /would you like/i,
114
- /do you want/i,
115
- /please confirm/i,
116
- /requires.*approval/i,
117
- /needs.*input/i,
118
- /⏵⏵\s*accept edits/i, // Claude's accept edits mode
119
- /shift\+tab to cycle/i, // Claude's interface hints
120
- ];
121
- // Check if Claude is working
122
- const isWorking = workingPatterns.some(pattern => pattern.test(captureOutput));
123
- // Check if Claude needs attention
124
- const needsAttention = attentionPatterns.some(pattern => pattern.test(captureOutput)) || hasInputBox;
125
- // Determine status - working takes precedence
126
- let newStatus = 'idle';
127
- if (isWorking) {
128
- newStatus = 'working';
129
- }
130
- else if (needsAttention && !isWorking) {
131
- // Only show as waiting if NOT working (working takes precedence)
132
- newStatus = 'waiting';
133
- }
134
- // Additional checks for specific Claude states
135
- // If we see "accept edits" without other working indicators, it's waiting
136
- if (/accept edits/i.test(captureOutput) && !/esc to interrupt/i.test(captureOutput)) {
137
- newStatus = 'waiting';
138
- }
139
- // If Claude's input box is visible and no working indicators, it's waiting for input
140
- if (hasInputBox && !isWorking) {
141
- newStatus = 'waiting';
142
- }
143
- // Check for specific Claude question patterns that might not end with ?
144
- const claudeQuestionPatterns = [
145
- /I (can|could|should|would|will|may|might)/i,
146
- /Let me know/i,
147
- /Please (tell|let|inform|advise)/i,
148
- /Would you prefer/i,
149
- /Should I (proceed|continue|go ahead)/i,
150
- ];
151
- if (claudeQuestionPatterns.some(pattern => pattern.test(lastLines)) && !isWorking) {
152
- newStatus = 'waiting';
153
- }
154
- // Return updated pane if status changed
155
- if (pane.claudeStatus !== newStatus) {
124
+ // Defer Claude monitoring to avoid slowing down startup and dialog interactions
125
+ const startupDelay = setTimeout(() => {
126
+ const monitorClaudeStatus = async () => {
127
+ // Skip monitoring if any dialog is open to avoid UI freezing
128
+ if (showNewPaneDialog || showMergeConfirmation || showCloseOptions ||
129
+ showCommandPrompt || showFileCopyPrompt || showUpdateDialog) {
130
+ return;
131
+ }
132
+ // Monitor Claude Code status for all panes
133
+ const updatedPanesWithNulls = await Promise.all(panes.map(async (pane) => {
134
+ try {
135
+ // Skip if recently checked (within 500ms to avoid overlapping checks)
136
+ if (pane.lastClaudeCheck && Date.now() - pane.lastClaudeCheck < 500) {
137
+ return pane;
138
+ }
139
+ // First check if pane exists before trying to capture
140
+ const paneIds = execSync(`tmux list-panes -F '#{pane_id}'`, {
141
+ encoding: 'utf-8',
142
+ stdio: 'pipe'
143
+ }).trim().split('\n');
144
+ if (!paneIds.includes(pane.paneId)) {
145
+ // Pane doesn't exist anymore, return unchanged
146
+ return pane;
147
+ }
148
+ // Capture the last 30 lines of the pane for better detection
149
+ const captureOutput = execSync(`tmux capture-pane -t '${pane.paneId}' -p -S -30`, { encoding: 'utf-8', stdio: 'pipe' });
150
+ // Pattern detection for Claude Code states
151
+ // Working patterns - Claude is processing
152
+ // The ONLY reliable indicator is "(esc to interrupt)"
153
+ const workingPatterns = [
154
+ /esc to interrupt/i, // The ONLY reliable indicator that Claude is actively working
155
+ ];
156
+ // Extract last few lines to check for patterns
157
+ const lines = captureOutput.split('\n');
158
+ const lastLines = lines.slice(-10).join('\n');
159
+ // Check if Claude's input box is present (waiting for input)
160
+ const hasInputBox = /╭─+╮/.test(lastLines) && /╰─+╯/.test(lastLines) && /│\s+>\s+.*│/.test(lastLines);
161
+ // Permission/attention patterns - needs user input (very forgiving)
162
+ const attentionPatterns = [
163
+ /\?\s*$/m, // Any line ending with a question mark
164
+ /y\/n/i, // Any y/n prompt
165
+ /yes.*no/i, // Yes or no prompts
166
+ /\ballow\b.*\?/i,
167
+ /\bapprove\b.*\?/i,
168
+ /\bgrant\b.*\?/i,
169
+ /\btrust\b.*\?/i,
170
+ /\baccept\b.*\?/i,
171
+ /\bcontinue\b.*\?/i,
172
+ /\bproceed\b.*\?/i,
173
+ /permission/i,
174
+ /confirmation/i,
175
+ /press.*enter/i,
176
+ /waiting for/i,
177
+ /are you sure/i,
178
+ /would you like/i,
179
+ /do you want/i,
180
+ /please confirm/i,
181
+ /requires.*approval/i,
182
+ /needs.*input/i,
183
+ /⏵⏵\s*accept edits/i, // Claude's accept edits mode
184
+ /shift\+tab to cycle/i, // Claude's interface hints
185
+ ];
186
+ // Check if Claude is working
187
+ const isWorking = workingPatterns.some(pattern => pattern.test(captureOutput));
188
+ // Check if Claude needs attention
189
+ const needsAttention = attentionPatterns.some(pattern => pattern.test(captureOutput)) || hasInputBox;
190
+ // Determine status - working takes precedence
191
+ let newStatus = 'idle';
192
+ if (isWorking) {
193
+ newStatus = 'working';
194
+ }
195
+ else if (needsAttention && !isWorking) {
196
+ // Only show as waiting if NOT working (working takes precedence)
197
+ newStatus = 'waiting';
198
+ }
199
+ // Additional checks for specific Claude states
200
+ // If we see "accept edits" without other working indicators, it's waiting
201
+ if (/accept edits/i.test(captureOutput) && !/esc to interrupt/i.test(captureOutput)) {
202
+ newStatus = 'waiting';
203
+ }
204
+ // If Claude's input box is visible and no working indicators, it's waiting for input
205
+ if (hasInputBox && !isWorking) {
206
+ newStatus = 'waiting';
207
+ }
208
+ // Check for specific Claude question patterns that might not end with ?
209
+ const claudeQuestionPatterns = [
210
+ /I (can|could|should|would|will|may|might)/i,
211
+ /Let me know/i,
212
+ /Please (tell|let|inform|advise)/i,
213
+ /Would you prefer/i,
214
+ /Should I (proceed|continue|go ahead)/i,
215
+ ];
216
+ if (claudeQuestionPatterns.some(pattern => pattern.test(lastLines)) && !isWorking) {
217
+ newStatus = 'waiting';
218
+ }
219
+ // Return updated pane if status changed
220
+ if (pane.claudeStatus !== newStatus) {
221
+ return {
222
+ ...pane,
223
+ claudeStatus: newStatus,
224
+ lastClaudeCheck: Date.now()
225
+ };
226
+ }
227
+ // Just update timestamp
156
228
  return {
157
229
  ...pane,
158
- claudeStatus: newStatus,
159
230
  lastClaudeCheck: Date.now()
160
231
  };
161
232
  }
162
- // Just update timestamp
163
- return {
164
- ...pane,
165
- lastClaudeCheck: Date.now()
166
- };
167
- }
168
- catch (error) {
169
- // If we can't capture the pane, it might be dead - mark it for removal
170
- return null; // Will be filtered out below
233
+ catch (error) {
234
+ // If we can't capture the pane, it might be dead - mark it for removal
235
+ return null; // Will be filtered out below
236
+ }
237
+ }));
238
+ // Filter out null values (dead panes) and keep only valid panes
239
+ const updatedPanes = updatedPanesWithNulls.filter((pane) => pane !== null);
240
+ // Check if we have changes (including pane removals)
241
+ const hasChanges = updatedPanes.length !== panes.length ||
242
+ updatedPanes.some((pane, index) => pane.claudeStatus !== panes[index]?.claudeStatus);
243
+ if (hasChanges) {
244
+ setPanes(updatedPanes);
245
+ // Save to file
246
+ await fs.writeFile(panesFile, JSON.stringify(updatedPanes, null, 2));
171
247
  }
172
- }));
173
- // Filter out null values (dead panes) and keep only valid panes
174
- const updatedPanes = updatedPanesWithNulls.filter((pane) => pane !== null);
175
- // Check if we have changes (including pane removals)
176
- const hasChanges = updatedPanes.length !== panes.length ||
177
- updatedPanes.some((pane, index) => pane.claudeStatus !== panes[index]?.claudeStatus);
178
- if (hasChanges) {
179
- setPanes(updatedPanes);
180
- // Save to file
181
- await fs.writeFile(panesFile, JSON.stringify(updatedPanes, null, 2));
182
- }
183
- };
184
- // Run monitoring immediately
185
- monitorClaudeStatus();
186
- // Set up interval for continuous monitoring
187
- const claudeInterval = setInterval(monitorClaudeStatus, 1000); // Check every second
248
+ };
249
+ // Run monitoring after delay
250
+ monitorClaudeStatus();
251
+ // Set up interval for continuous monitoring (less frequent for better performance)
252
+ const claudeInterval = setInterval(monitorClaudeStatus, 2000); // Check every 2 seconds
253
+ return () => {
254
+ clearInterval(claudeInterval);
255
+ };
256
+ }, 500); // Wait 500ms before starting monitoring
188
257
  return () => {
189
- clearInterval(claudeInterval);
258
+ clearTimeout(startupDelay);
190
259
  };
191
- }, [panes, panesFile]); // Re-run when panes change
260
+ }, [panes, panesFile, showNewPaneDialog, showMergeConfirmation, showCloseOptions,
261
+ showCommandPrompt, showFileCopyPrompt, showUpdateDialog]); // Re-run when panes or dialogs change
192
262
  const loadSettings = async () => {
193
263
  try {
194
264
  const content = await fs.readFile(settingsFile, 'utf-8');
@@ -205,42 +275,58 @@ const DmuxApp = ({ dmuxDir, panesFile, projectName, sessionName, settingsFile })
205
275
  setProjectSettings(settings);
206
276
  };
207
277
  const loadPanes = async () => {
278
+ // Skip loading if any dialog is open to avoid UI freezing
279
+ if (showNewPaneDialog || showMergeConfirmation || showCloseOptions ||
280
+ showCommandPrompt || showFileCopyPrompt || showUpdateDialog) {
281
+ return;
282
+ }
283
+ if (isLoading) {
284
+ // Don't set loading to false immediately - keep it true for initial load
285
+ }
208
286
  try {
209
287
  const content = await fs.readFile(panesFile, 'utf-8');
210
288
  const loadedPanes = JSON.parse(content);
211
- // Filter out dead panes and update titles for active ones
212
- const activePanes = loadedPanes.filter(pane => {
213
- try {
214
- // Get list of all pane IDs
215
- const paneIds = execSync(`tmux list-panes -F '#{pane_id}'`, {
216
- encoding: 'utf-8',
217
- stdio: 'pipe'
218
- }).trim().split('\n');
219
- // Check if our pane ID exists in the list
220
- if (paneIds.includes(pane.paneId)) {
221
- // Update pane title to match the slug
222
- try {
223
- execSync(`tmux select-pane -t '${pane.paneId}' -T "${pane.slug}"`, { stdio: 'pipe' });
224
- }
225
- catch {
226
- // Ignore if setting title fails
227
- }
228
- return true;
289
+ // Get all pane IDs in a single call (much faster)
290
+ let allPaneIds = [];
291
+ try {
292
+ allPaneIds = execSync(`tmux list-panes -F '#{pane_id}'`, {
293
+ encoding: 'utf-8',
294
+ stdio: 'pipe'
295
+ }).trim().split('\n').filter(id => id);
296
+ }
297
+ catch {
298
+ // No panes or tmux error
299
+ }
300
+ // Filter out dead panes
301
+ const activePanes = loadedPanes.filter(pane => allPaneIds.includes(pane.paneId));
302
+ // Batch update all pane titles in one go (only if needed)
303
+ if (activePanes.length > 0) {
304
+ // Update titles for all active panes at once
305
+ activePanes.forEach(pane => {
306
+ try {
307
+ execSync(`tmux select-pane -t '${pane.paneId}' -T "${pane.slug}"`, { stdio: 'pipe' });
229
308
  }
230
- return false;
231
- }
232
- catch {
233
- return false;
234
- }
235
- });
309
+ catch {
310
+ // Ignore if setting title fails
311
+ }
312
+ });
313
+ }
236
314
  setPanes(activePanes);
237
315
  // Save cleaned list
238
316
  if (activePanes.length !== loadedPanes.length) {
239
317
  await fs.writeFile(panesFile, JSON.stringify(activePanes, null, 2));
240
318
  }
319
+ // Set loading to false after first load
320
+ if (isLoading) {
321
+ setIsLoading(false);
322
+ }
241
323
  }
242
324
  catch {
243
325
  setPanes([]);
326
+ // Set loading to false even on error
327
+ if (isLoading) {
328
+ setIsLoading(false);
329
+ }
244
330
  }
245
331
  };
246
332
  const getPanePositions = () => {
@@ -272,7 +358,7 @@ const DmuxApp = ({ dmuxDir, panesFile, projectName, sessionName, settingsFile })
272
358
  return { row, col };
273
359
  };
274
360
  const findCardInDirection = (currentIndex, direction) => {
275
- const totalItems = panes.length + 1; // +1 for "New dmux pane" button
361
+ const totalItems = panes.length + (isLoading ? 0 : 1); // +1 for "New dmux pane" button when not loading
276
362
  const currentPos = getCardGridPosition(currentIndex);
277
363
  // Calculate cards per row based on current terminal width
278
364
  const cardWidth = 35 + 2; // Card width plus gap
@@ -1479,6 +1565,45 @@ const DmuxApp = ({ dmuxDir, panesFile, projectName, sessionName, settingsFile })
1479
1565
  setTimeout(() => setStatusMessage(''), 3000);
1480
1566
  }
1481
1567
  };
1568
+ // Update handling functions
1569
+ const performUpdate = async () => {
1570
+ if (!autoUpdater || !updateInfo)
1571
+ return;
1572
+ try {
1573
+ setIsUpdating(true);
1574
+ setStatusMessage('Updating dmux...');
1575
+ const success = await autoUpdater.performUpdate(updateInfo);
1576
+ if (success) {
1577
+ setStatusMessage('Update completed successfully! Please restart dmux.');
1578
+ setTimeout(() => {
1579
+ process.exit(0);
1580
+ }, 3000);
1581
+ }
1582
+ else {
1583
+ setStatusMessage('Update failed. Please update manually.');
1584
+ setTimeout(() => setStatusMessage(''), 3000);
1585
+ }
1586
+ }
1587
+ catch (error) {
1588
+ setStatusMessage('Update failed. Please update manually.');
1589
+ setTimeout(() => setStatusMessage(''), 3000);
1590
+ }
1591
+ finally {
1592
+ setIsUpdating(false);
1593
+ setShowUpdateDialog(false);
1594
+ }
1595
+ };
1596
+ const skipUpdate = async () => {
1597
+ if (!autoUpdater || !updateInfo)
1598
+ return;
1599
+ await autoUpdater.skipVersion(updateInfo.latestVersion);
1600
+ setShowUpdateDialog(false);
1601
+ setUpdateInfo(null);
1602
+ };
1603
+ const dismissUpdate = () => {
1604
+ setShowUpdateDialog(false);
1605
+ setUpdateInfo(null);
1606
+ };
1482
1607
  // Cleanup function for exit
1483
1608
  const cleanExit = () => {
1484
1609
  // Clear screen multiple times to ensure no artifacts
@@ -1495,8 +1620,20 @@ const DmuxApp = ({ dmuxDir, panesFile, projectName, sessionName, settingsFile })
1495
1620
  exit();
1496
1621
  };
1497
1622
  useInput(async (input, key) => {
1498
- if (isCreatingPane || runningCommand) {
1499
- // Disable input while performing operations
1623
+ if (isCreatingPane || runningCommand || isUpdating || isLoading) {
1624
+ // Disable input while performing operations or loading
1625
+ return;
1626
+ }
1627
+ if (showUpdateDialog && updateInfo) {
1628
+ if (input === 'u' || input === 'U') {
1629
+ await performUpdate();
1630
+ }
1631
+ else if (input === 's' || input === 'S') {
1632
+ await skipUpdate();
1633
+ }
1634
+ else if (input === 'l' || input === 'L' || key.escape) {
1635
+ dismissUpdate();
1636
+ }
1500
1637
  return;
1501
1638
  }
1502
1639
  if (showFileCopyPrompt) {
@@ -1645,8 +1782,11 @@ const DmuxApp = ({ dmuxDir, panesFile, projectName, sessionName, settingsFile })
1645
1782
  if (input === 'q') {
1646
1783
  cleanExit();
1647
1784
  }
1648
- else if (input === 'n' || (key.return && selectedIndex === panes.length)) {
1785
+ else if (!isLoading && (input === 'n' || (key.return && selectedIndex === panes.length))) {
1786
+ // Defer state updates slightly to avoid blocking UI
1649
1787
  setShowNewPaneDialog(true);
1788
+ setTimeout(() => setNewPanePrompt(''), 0); // Clear after dialog renders
1789
+ return; // Consume the 'n' keystroke so it doesn't propagate
1650
1790
  }
1651
1791
  else if (input === 'j' && selectedIndex < panes.length) {
1652
1792
  jumpToPane(panes[selectedIndex].paneId);
@@ -1725,20 +1865,23 @@ const DmuxApp = ({ dmuxDir, panesFile, projectName, sessionName, settingsFile })
1725
1865
  " ",
1726
1866
  pane.devUrl.replace(/https?:\/\//, '').substring(0, 15))))))))));
1727
1867
  }),
1728
- React.createElement(Box, { paddingX: 1, borderStyle: "single", borderColor: selectedIndex === panes.length ? 'green' : 'gray', width: 35, flexShrink: 0 },
1729
- React.createElement(Text, { color: selectedIndex === panes.length ? 'green' : 'white' }, "+ New dmux pane"))),
1730
- showNewPaneDialog && (React.createElement(Box, { borderStyle: "round", borderColor: "gray", paddingX: 1 },
1731
- React.createElement(Box, { flexDirection: "column" },
1732
- React.createElement(Text, null, "Enter initial Claude prompt (ESC to cancel):"),
1733
- React.createElement(Box, { marginTop: 1 },
1734
- React.createElement(Box, { flexDirection: "column" },
1735
- React.createElement(TextInput, { value: newPanePrompt, onChange: setNewPanePrompt, placeholder: "Optional prompt... (@ to reference files)", onSubmit: () => {
1736
- createNewPane(newPanePrompt);
1737
- setShowNewPaneDialog(false);
1738
- setNewPanePrompt('');
1739
- } }),
1740
- React.createElement(Box, { marginTop: 1 },
1741
- React.createElement(Text, { dimColor: true, italic: true }, "Press Ctrl+O to open in $EDITOR for complex multi-line input"))))))),
1868
+ !isLoading && !showNewPaneDialog && (React.createElement(Box, { paddingX: 1, borderStyle: "single", borderColor: selectedIndex === panes.length ? 'green' : 'gray', width: 35, flexShrink: 0 },
1869
+ React.createElement(Text, { color: selectedIndex === panes.length ? 'green' : 'white' }, "+ New dmux pane")))),
1870
+ isLoading && (React.createElement(Box, { borderStyle: "round", borderColor: "cyan", paddingX: 2, marginTop: 1 },
1871
+ React.createElement(Box, { flexDirection: "row", gap: 1 },
1872
+ React.createElement(Text, { color: "cyan" }, "\u23F3"),
1873
+ React.createElement(Text, null, "Loading dmux sessions...")))),
1874
+ showNewPaneDialog && (React.createElement(Box, { flexDirection: "column", marginTop: 1 },
1875
+ React.createElement(Text, null, "Enter initial Claude prompt (ESC to cancel):"),
1876
+ React.createElement(Box, { borderStyle: "round", borderColor: "#E67E22", paddingX: 1, marginTop: 1 },
1877
+ React.createElement(CleanTextInput, { value: newPanePrompt, onChange: setNewPanePrompt, onSubmit: (expandedValue) => {
1878
+ // Use expanded value if provided (for paste references), otherwise use raw value
1879
+ createNewPane(expandedValue || newPanePrompt);
1880
+ setShowNewPaneDialog(false);
1881
+ setNewPanePrompt('');
1882
+ } })),
1883
+ React.createElement(Box, { marginTop: 1 },
1884
+ React.createElement(Text, { dimColor: true, italic: true }, "Press Ctrl+O to open in $EDITOR for complex multi-line input")))),
1742
1885
  isCreatingPane && (React.createElement(Box, { borderStyle: "single", borderColor: "yellow", paddingX: 1, marginTop: 1 },
1743
1886
  React.createElement(Text, { color: "yellow" },
1744
1887
  React.createElement(Text, { bold: true }, "\u23F3 Creating new pane... "),
@@ -1786,7 +1929,7 @@ const DmuxApp = ({ dmuxDir, panesFile, projectName, sessionName, settingsFile })
1786
1929
  " in worktrees"),
1787
1930
  React.createElement(Text, { dimColor: true }, "(Press Enter with empty input for suggested command, ESC to cancel)"),
1788
1931
  React.createElement(Box, { marginTop: 1 },
1789
- React.createElement(TextInput, { value: commandInput, onChange: setCommandInput, placeholder: showCommandPrompt === 'test' ? 'e.g., npm test, pnpm test' : 'e.g., npm run dev, pnpm dev' }))))),
1932
+ React.createElement(StyledTextInput, { value: commandInput, onChange: setCommandInput, placeholder: showCommandPrompt === 'test' ? 'e.g., npm test, pnpm test' : 'e.g., npm run dev, pnpm dev' }))))),
1790
1933
  showFileCopyPrompt && (React.createElement(Box, { borderStyle: "double", borderColor: "yellow", paddingX: 1, marginTop: 1 },
1791
1934
  React.createElement(Box, { flexDirection: "column" },
1792
1935
  React.createElement(Text, { color: "yellow", bold: true }, "First Run Setup"),
@@ -1797,6 +1940,28 @@ const DmuxApp = ({ dmuxDir, panesFile, projectName, sessionName, settingsFile })
1797
1940
  runningCommand && (React.createElement(Box, { borderStyle: "single", borderColor: "blue", paddingX: 1, marginTop: 1 },
1798
1941
  React.createElement(Text, { color: "blue" },
1799
1942
  React.createElement(Text, { bold: true }, "\u25B6 Running command...")))),
1943
+ isUpdating && (React.createElement(Box, { borderStyle: "single", borderColor: "yellow", paddingX: 1, marginTop: 1 },
1944
+ React.createElement(Text, { color: "yellow" },
1945
+ React.createElement(Text, { bold: true }, "\u2B07 Updating dmux...")))),
1946
+ showUpdateDialog && updateInfo && (React.createElement(Box, { borderStyle: "double", borderColor: "green", paddingX: 1, marginTop: 1 },
1947
+ React.createElement(Box, { flexDirection: "column" },
1948
+ React.createElement(Text, { color: "green", bold: true }, "\uD83C\uDF89 dmux Update Available!"),
1949
+ React.createElement(Text, null,
1950
+ "Current version: ",
1951
+ React.createElement(Text, { color: "cyan" }, updateInfo.currentVersion)),
1952
+ React.createElement(Text, null,
1953
+ "Latest version: ",
1954
+ React.createElement(Text, { color: "green" }, updateInfo.latestVersion)),
1955
+ updateInfo.installMethod === 'global' && updateInfo.packageManager && (React.createElement(Text, null,
1956
+ "Detected global install via: ",
1957
+ React.createElement(Text, { color: "yellow" }, updateInfo.packageManager))),
1958
+ React.createElement(Box, { marginTop: 1 }, updateInfo.installMethod === 'global' && updateInfo.packageManager ? (React.createElement(Text, null, "[U]pdate now \u2022 [S]kip this version \u2022 [L]ater")) : (React.createElement(Text, null,
1959
+ "Manual update required: ",
1960
+ React.createElement(Text, { color: "cyan" },
1961
+ updateInfo.packageManager || 'npm',
1962
+ " update -g dmux"),
1963
+ '\n',
1964
+ "[S]kip this version \u2022 [L]ater")))))),
1800
1965
  statusMessage && (React.createElement(Box, { marginTop: 1 },
1801
1966
  React.createElement(Text, { color: "green" }, statusMessage))),
1802
1967
  !showNewPaneDialog && !showCommandPrompt && (React.createElement(Box, { marginTop: 1, flexDirection: "column" },