diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/Terminal.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/Terminal.tsx index 569fa7e9de4..e9ad5093ebb 100644 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/Terminal.tsx +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/Terminal.tsx @@ -74,6 +74,7 @@ export const Terminal = ({ tabId, workspaceId }: TerminalProps) => { ); const terminalTheme = useTerminalTheme(); const restartTerminalRef = useRef<() => void>(() => {}); + const ctrlDSentRef = useRef(false); // Terminal connection state and mutations const { @@ -235,6 +236,7 @@ export const Terminal = ({ tabId, workspaceId }: TerminalProps) => { isExitedRef, wasKilledByUserRef, pendingEventsRef, + ctrlDSentRef, setExitStatus, setConnectionError, updateModesFromData, @@ -408,6 +410,10 @@ export const Terminal = ({ tabId, workspaceId }: TerminalProps) => { restartTerminal(); return; } + // Track Ctrl+D (EOF character) for auto-close on exit + if (data === "\x04") { + ctrlDSentRef.current = true; + } writeRef.current({ paneId, data }); }; diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/hooks/useTerminalStream.ts b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/hooks/useTerminalStream.ts index 6a160efdc9e..58b709bf448 100644 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/hooks/useTerminalStream.ts +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/hooks/useTerminalStream.ts @@ -13,6 +13,7 @@ export interface UseTerminalStreamOptions { isExitedRef: React.MutableRefObject; wasKilledByUserRef: React.MutableRefObject; pendingEventsRef: React.MutableRefObject; + ctrlDSentRef: React.MutableRefObject; setExitStatus: (status: "killed" | "exited" | null) => void; setConnectionError: (error: string | null) => void; updateModesFromData: (data: string) => void; @@ -38,12 +39,14 @@ export function useTerminalStream({ isExitedRef, wasKilledByUserRef, pendingEventsRef, + ctrlDSentRef, setExitStatus, setConnectionError, updateModesFromData, updateCwdFromData, }: UseTerminalStreamOptions): UseTerminalStreamReturn { const setPaneStatus = useTabsStore((s) => s.setPaneStatus); + const removePane = useTabsStore((s) => s.removePane); const firstStreamDataReceivedRef = useRef(false); // Refs to use latest values in callbacks @@ -54,10 +57,19 @@ export function useTerminalStream({ const handleTerminalExit = useCallback( (exitCode: number, xterm: XTerm) => { + const wasKilledByUser = isTerminalKilledByUser(paneId); + + // Auto-close pane on Ctrl+D (clean exit after EOF) + if (exitCode === 0 && ctrlDSentRef.current && !wasKilledByUser) { + ctrlDSentRef.current = false; + removePane(paneId); + return; + } + ctrlDSentRef.current = false; + isExitedRef.current = true; isStreamReadyRef.current = false; - const wasKilledByUser = isTerminalKilledByUser(paneId); wasKilledByUserRef.current = wasKilledByUser; setExitStatus(wasKilledByUser ? "killed" : "exited"); @@ -83,8 +95,10 @@ export function useTerminalStream({ isExitedRef, isStreamReadyRef, wasKilledByUserRef, + ctrlDSentRef, setExitStatus, setPaneStatus, + removePane, ], );