Skip to content

Commit 9649f63

Browse files
Fix terminal input handling (#106)
* Fix terminal input handling - Fix stale executionId issue in terminal input handler - Add proper terminal focus management - Fix timing issues with input handler registration - Remove invalid terminal.off() cleanup method - Add isTerminalReady state to ensure proper initialization order - Clean up debugging console logs Fixes issue where terminal input would stop working after script restarts due to stale executionId being captured in the input handler closure. * Fix TypeScript build errors - Fix unsafe argument type for removeEventListener - Remove unused eslint-disable directive - Build now passes successfully
1 parent e63958e commit 9649f63

File tree

1 file changed

+62
-21
lines changed

1 file changed

+62
-21
lines changed

src/app/_components/Terminal.tsx

Lines changed: 62 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,12 @@ export function Terminal({ scriptPath, onClose, mode = 'local', server, isUpdate
2929
const [lastInputSent, setLastInputSent] = useState<string | null>(null);
3030
const [isMobile, setIsMobile] = useState(false);
3131
const [isStopped, setIsStopped] = useState(false);
32+
const [isTerminalReady, setIsTerminalReady] = useState(false);
3233
const terminalRef = useRef<HTMLDivElement>(null);
3334
const xtermRef = useRef<any>(null);
3435
const fitAddonRef = useRef<any>(null);
3536
const wsRef = useRef<WebSocket | null>(null);
37+
const inputHandlerRef = useRef<((data: string) => void) | null>(null);
3638
const [executionId, setExecutionId] = useState(() => `exec_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`);
3739
const isConnectingRef = useRef<boolean>(false);
3840
const hasConnectedRef = useRef<boolean>(false);
@@ -180,6 +182,20 @@ export function Terminal({ scriptPath, onClose, mode = 'local', server, isUpdate
180182
terminal.refresh(0, terminal.rows - 1);
181183
// Ensure cursor is properly positioned
182184
terminal.focus();
185+
186+
// Force focus on the terminal element
187+
terminalElement.focus();
188+
terminalElement.click();
189+
190+
// Add click handler to ensure terminal stays focused
191+
const focusHandler = () => {
192+
terminal.focus();
193+
terminalElement.focus();
194+
};
195+
terminalElement.addEventListener('click', focusHandler);
196+
197+
// Store the handler for cleanup
198+
(terminalElement as any).focusHandler = focusHandler;
183199
}, 100);
184200

185201
// Fit after a small delay to ensure proper sizing
@@ -213,18 +229,10 @@ export function Terminal({ scriptPath, onClose, mode = 'local', server, isUpdate
213229
// Store references
214230
xtermRef.current = terminal;
215231
fitAddonRef.current = fitAddon;
232+
233+
// Mark terminal as ready
234+
setIsTerminalReady(true);
216235

217-
// Handle terminal input
218-
terminal.onData((data) => {
219-
if (wsRef.current && wsRef.current.readyState === WebSocket.OPEN) {
220-
const message = {
221-
action: 'input',
222-
executionId,
223-
input: data
224-
};
225-
wsRef.current.send(JSON.stringify(message));
226-
}
227-
});
228236

229237
return () => {
230238
terminal.dispose();
@@ -236,18 +244,51 @@ export function Terminal({ scriptPath, onClose, mode = 'local', server, isUpdate
236244
void initTerminal();
237245
}, 50);
238246

239-
return () => {
240-
clearTimeout(timeoutId);
241-
if (terminalElement && (terminalElement as any).resizeHandler) {
242-
window.removeEventListener('resize', (terminalElement as any).resizeHandler as (this: Window, ev: UIEvent) => any);
243-
}
244-
if (xtermRef.current) {
245-
xtermRef.current.dispose();
246-
xtermRef.current = null;
247-
fitAddonRef.current = null;
247+
return () => {
248+
clearTimeout(timeoutId);
249+
if (terminalElement && (terminalElement as any).resizeHandler) {
250+
window.removeEventListener('resize', (terminalElement as any).resizeHandler as (this: Window, ev: UIEvent) => any);
251+
}
252+
if (terminalElement && (terminalElement as any).focusHandler) {
253+
terminalElement.removeEventListener('click', (terminalElement as any).focusHandler as (this: HTMLDivElement, ev: PointerEvent) => any);
254+
}
255+
if (xtermRef.current) {
256+
xtermRef.current.dispose();
257+
xtermRef.current = null;
258+
fitAddonRef.current = null;
259+
setIsTerminalReady(false);
260+
}
261+
};
262+
}, [isClient, isMobile]);
263+
264+
// Handle terminal input with current executionId
265+
useEffect(() => {
266+
if (!isTerminalReady || !xtermRef.current) {
267+
return;
268+
}
269+
270+
const terminal = xtermRef.current;
271+
272+
const handleData = (data: string) => {
273+
if (wsRef.current && wsRef.current.readyState === WebSocket.OPEN) {
274+
const message = {
275+
action: 'input',
276+
executionId,
277+
input: data
278+
};
279+
wsRef.current.send(JSON.stringify(message));
248280
}
249281
};
250-
}, [isClient, isMobile]); // eslint-disable-line react-hooks/exhaustive-deps
282+
283+
// Store the handler reference
284+
inputHandlerRef.current = handleData;
285+
terminal.onData(handleData);
286+
287+
return () => {
288+
// Clear the handler reference
289+
inputHandlerRef.current = null;
290+
};
291+
}, [executionId, isTerminalReady]); // Depend on terminal ready state
251292

252293
useEffect(() => {
253294
// Prevent multiple connections in React Strict Mode

0 commit comments

Comments
 (0)