Skip to content

Commit a8aa54e

Browse files
feat: significant performance optimizations
1 parent bd870f7 commit a8aa54e

19 files changed

+1878
-144
lines changed

src/DmuxApp.tsx

Lines changed: 82 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import React, { useState, useEffect } from "react"
2-
import { Box, Text, useApp, useStdout } from "ink"
2+
import { Box, Text, useApp, useStdout, useInput } from "ink"
33
import { createRequire } from "module"
44
import { TmuxService } from "./services/TmuxService.js"
55

@@ -60,6 +60,8 @@ import LoadingIndicator from "./components/indicators/LoadingIndicator.js"
6060
import RunningIndicator from "./components/indicators/RunningIndicator.js"
6161
import UpdatingIndicator from "./components/indicators/UpdatingIndicator.js"
6262
import FooterHelp from "./components/ui/FooterHelp.js"
63+
import TmuxHooksPromptDialog from "./components/dialogs/TmuxHooksPromptDialog.js"
64+
import { PaneEventService } from "./services/PaneEventService.js"
6365

6466
const DmuxApp: React.FC<DmuxAppProps> = ({
6567
panesFile,
@@ -153,6 +155,12 @@ const DmuxApp: React.FC<DmuxAppProps> = ({
153155
const [toastQueueLength, setToastQueueLength] = useState(0)
154156
const [toastQueuePosition, setToastQueuePosition] = useState<number | null>(null)
155157

158+
// Tmux hooks prompt state
159+
const [showHooksPrompt, setShowHooksPrompt] = useState(false)
160+
const [hooksPromptIndex, setHooksPromptIndex] = useState(0)
161+
// undefined = not yet determined, true = use hooks, false = use polling
162+
const [useHooks, setUseHooks] = useState<boolean | undefined>(undefined)
163+
156164
// Subscribe to StateManager for unread error/warning count and toast updates
157165
useEffect(() => {
158166
const stateManager = StateManager.getInstance()
@@ -178,11 +186,46 @@ const DmuxApp: React.FC<DmuxAppProps> = ({
178186
}, [])
179187

180188
// Panes state and persistence (skipLoading will be updated after actionSystem is initialized)
181-
const { panes, setPanes, isLoading, loadPanes, savePanes } = usePanes(
189+
const { panes, setPanes, isLoading, loadPanes, savePanes, eventMode } = usePanes(
182190
panesFile,
183-
false
191+
false,
192+
sessionName,
193+
controlPaneId,
194+
useHooks
184195
)
185196

197+
// Check for tmux hooks preference on startup
198+
useEffect(() => {
199+
const checkHooksPreference = async () => {
200+
// Check if user already has a preference
201+
const settings = settingsManager.getSettings()
202+
203+
if (settings.useTmuxHooks !== undefined) {
204+
// User has already decided
205+
setUseHooks(settings.useTmuxHooks)
206+
return
207+
}
208+
209+
// Check if hooks are already installed (from previous session)
210+
const paneEventService = PaneEventService.getInstance()
211+
paneEventService.initialize({ sessionName, controlPaneId })
212+
213+
const hooksInstalled = await paneEventService.canUseHooks()
214+
215+
if (hooksInstalled) {
216+
// Hooks already installed, use them automatically
217+
setUseHooks(true)
218+
// Save the preference
219+
settingsManager.updateSetting('useTmuxHooks', true, 'global')
220+
} else {
221+
// Need to ask user - show prompt
222+
setShowHooksPrompt(true)
223+
}
224+
}
225+
226+
checkHooksPreference()
227+
}, [sessionName, controlPaneId, settingsManager])
228+
186229
// Pane lifecycle manager - handles locking to prevent race conditions
187230
// Replaces the old timeout-based intentionallyClosedPanes Set
188231
const lifecycleManager = React.useMemo(() => PaneLifecycleManager.getInstance(), [])
@@ -725,6 +768,36 @@ const DmuxApp: React.FC<DmuxAppProps> = ({
725768
}, 100)
726769
}
727770

771+
// Handle tmux hooks prompt input
772+
useInput(
773+
(input, key) => {
774+
if (!showHooksPrompt) return
775+
776+
if (key.upArrow || input === 'k') {
777+
setHooksPromptIndex(Math.max(0, hooksPromptIndex - 1))
778+
} else if (key.downArrow || input === 'j') {
779+
setHooksPromptIndex(Math.min(1, hooksPromptIndex + 1))
780+
} else if (input === 'y') {
781+
// Yes - install hooks
782+
setShowHooksPrompt(false)
783+
setUseHooks(true)
784+
settingsManager.updateSetting('useTmuxHooks', true, 'global')
785+
} else if (input === 'n') {
786+
// No - use polling
787+
setShowHooksPrompt(false)
788+
setUseHooks(false)
789+
settingsManager.updateSetting('useTmuxHooks', false, 'global')
790+
} else if (key.return) {
791+
// Select current option
792+
setShowHooksPrompt(false)
793+
const selected = hooksPromptIndex === 0
794+
setUseHooks(selected)
795+
settingsManager.updateSetting('useTmuxHooks', selected, 'global')
796+
}
797+
},
798+
{ isActive: showHooksPrompt }
799+
)
800+
728801
// Input handling - extracted to dedicated hook
729802
useInputHandling({
730803
panes,
@@ -735,7 +808,7 @@ const DmuxApp: React.FC<DmuxAppProps> = ({
735808
runningCommand,
736809
isUpdating,
737810
isLoading,
738-
ignoreInput,
811+
ignoreInput: ignoreInput || showHooksPrompt, // Block other input when hooks prompt is shown
739812
quitConfirmMode,
740813
setQuitConfirmMode,
741814
showCommandPrompt,
@@ -854,6 +927,11 @@ const DmuxApp: React.FC<DmuxAppProps> = ({
854927
{runningCommand && <RunningIndicator />}
855928

856929
{isUpdating && <UpdatingIndicator />}
930+
931+
{/* Tmux hooks prompt - shown on first startup */}
932+
{showHooksPrompt && (
933+
<TmuxHooksPromptDialog selectedIndex={hooksPromptIndex} />
934+
)}
857935
</Box>
858936

859937
{/* Status messages - only render when present */}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
/**
2+
* Tmux Hooks Prompt Dialog
3+
*
4+
* Shown on first startup to ask user if they want to install tmux hooks
5+
* for event-driven pane updates (lower CPU) vs polling fallback.
6+
*/
7+
8+
import React, { memo } from 'react';
9+
import { Box, Text } from 'ink';
10+
11+
interface TmuxHooksPromptDialogProps {
12+
selectedIndex: number;
13+
}
14+
15+
const TmuxHooksPromptDialog: React.FC<TmuxHooksPromptDialogProps> = memo(({
16+
selectedIndex
17+
}) => {
18+
return (
19+
<Box
20+
flexDirection="column"
21+
borderStyle="round"
22+
borderColor="cyan"
23+
paddingX={1}
24+
marginTop={1}
25+
>
26+
<Box marginBottom={1}>
27+
<Text bold color="cyan">Performance Optimization</Text>
28+
</Box>
29+
30+
<Box marginBottom={1} flexDirection="column">
31+
<Text>dmux can install tmux hooks to detect pane changes instantly.</Text>
32+
<Text>This uses less CPU than polling and improves responsiveness.</Text>
33+
<Text dimColor>
34+
{'\n'}The hooks send signals to dmux when panes are created, closed, or resized.
35+
</Text>
36+
</Box>
37+
38+
<Box flexDirection="column">
39+
{/* Yes option - install hooks */}
40+
<Box>
41+
{selectedIndex === 0 ? (
42+
<Text color="green" bold inverse>
43+
{'► '}Yes, install hooks (recommended){' '}
44+
</Text>
45+
) : (
46+
<Text>
47+
{' '}Yes, install hooks (recommended)
48+
</Text>
49+
)}
50+
</Box>
51+
52+
{/* No option - use polling */}
53+
<Box>
54+
{selectedIndex === 1 ? (
55+
<Text color="yellow" bold inverse>
56+
{'► '}No, use polling instead{' '}
57+
</Text>
58+
) : (
59+
<Text>
60+
{' '}No, use polling instead
61+
</Text>
62+
)}
63+
</Box>
64+
</Box>
65+
66+
<Box marginTop={1}>
67+
<Text dimColor>↑/↓ to navigate • Enter to select • y/n shortcuts</Text>
68+
</Box>
69+
</Box>
70+
);
71+
});
72+
73+
export default TmuxHooksPromptDialog;

src/components/panes/PaneCard.tsx

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React from 'react';
1+
import React, { memo } from 'react';
22
import { Box, Text } from 'ink';
33
import type { DmuxPane } from '../../types.js';
44
import { COLORS } from '../../theme/colors.js';
@@ -11,7 +11,7 @@ interface PaneCardProps {
1111
isNextSelected: boolean;
1212
}
1313

14-
const PaneCard: React.FC<PaneCardProps> = ({ pane, selected, isFirstPane, isLastPane, isNextSelected }) => {
14+
const PaneCard: React.FC<PaneCardProps> = memo(({ pane, selected, isFirstPane, isLastPane, isNextSelected }) => {
1515
// Get status indicator
1616
const getStatusIcon = () => {
1717
if (pane.agentStatus === 'working') return { icon: '✻', color: COLORS.working };
@@ -64,6 +64,23 @@ const PaneCard: React.FC<PaneCardProps> = ({ pane, selected, isFirstPane, isLast
6464
</Box>
6565
</Box>
6666
);
67-
};
67+
}, (prevProps, nextProps) => {
68+
// Custom comparison for memoization - only re-render if relevant props changed
69+
return (
70+
prevProps.pane.id === nextProps.pane.id &&
71+
prevProps.pane.slug === nextProps.pane.slug &&
72+
prevProps.pane.agentStatus === nextProps.pane.agentStatus &&
73+
prevProps.pane.testStatus === nextProps.pane.testStatus &&
74+
prevProps.pane.devStatus === nextProps.pane.devStatus &&
75+
prevProps.pane.autopilot === nextProps.pane.autopilot &&
76+
prevProps.pane.type === nextProps.pane.type &&
77+
prevProps.pane.shellType === nextProps.pane.shellType &&
78+
prevProps.pane.agent === nextProps.pane.agent &&
79+
prevProps.selected === nextProps.selected &&
80+
prevProps.isFirstPane === nextProps.isFirstPane &&
81+
prevProps.isLastPane === nextProps.isLastPane &&
82+
prevProps.isNextSelected === nextProps.isNextSelected
83+
);
84+
});
6885

6986
export default PaneCard;

src/components/panes/PanesGrid.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React from "react"
1+
import React, { memo, useMemo } from "react"
22
import { Box, Text } from "ink"
33
import type { DmuxPane } from "../../types.js"
44
import type { AgentStatusMap } from "../../hooks/useAgentStatus.js"
@@ -12,7 +12,7 @@ interface PanesGridProps {
1212
agentStatuses?: AgentStatusMap
1313
}
1414

15-
const PanesGrid: React.FC<PanesGridProps> = ({
15+
const PanesGrid: React.FC<PanesGridProps> = memo(({
1616
panes,
1717
selectedIndex,
1818
isLoading,
@@ -109,6 +109,6 @@ const PanesGrid: React.FC<PanesGridProps> = ({
109109
)}
110110
</Box>
111111
)
112-
}
112+
})
113113

114114
export default PanesGrid

src/components/ui/FooterHelp.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React from 'react';
1+
import React, { memo } from 'react';
22
import { Box, Text } from 'ink';
33
import type { Toast } from '../../services/ToastService.js';
44
import ToastNotification from './ToastNotification.js';
@@ -22,7 +22,7 @@ interface FooterHelpProps {
2222
toastQueuePosition?: number | null;
2323
}
2424

25-
const FooterHelp: React.FC<FooterHelpProps> = ({
25+
const FooterHelp: React.FC<FooterHelpProps> = memo(({
2626
show,
2727
gridInfo,
2828
showRemoteKey = false,
@@ -190,6 +190,6 @@ const FooterHelp: React.FC<FooterHelpProps> = ({
190190
)}
191191
</Box>
192192
);
193-
};
193+
});
194194

195195
export default FooterHelp;

0 commit comments

Comments
 (0)