From 23dc3f794cf46a309d372d64f93402894e3f6a92 Mon Sep 17 00:00:00 2001 From: Jinjing <6427696+AmethystLiang@users.noreply.github.com> Date: Mon, 23 Mar 2026 21:32:55 -0700 Subject: [PATCH] fix: use generation tracking to prevent killing new PTYs on renderer reload The did-finish-load handler was killing all PTY processes, including ones spawned during the current page load. This caused blank terminals when a race between renderer reload and PTY creation occurred. Now each PTY is tagged with a load generation counter, and only PTYs from previous generations are killed on reload. Co-Authored-By: Claude Opus 4.6 --- src/main/ipc/pty.ts | 39 +++++++++++++++++++++++++++++++++++---- 1 file changed, 35 insertions(+), 4 deletions(-) diff --git a/src/main/ipc/pty.ts b/src/main/ipc/pty.ts index b15ef6cfcd8..4bf10f5fe23 100644 --- a/src/main/ipc/pty.ts +++ b/src/main/ipc/pty.ts @@ -1,13 +1,36 @@ -import { BrowserWindow, ipcMain } from 'electron' +import { type BrowserWindow, ipcMain } from 'electron' import * as pty from 'node-pty' let ptyCounter = 0 const ptyProcesses = new Map() +// Track which "page load generation" each PTY belongs to. +// When the renderer reloads, we only kill PTYs from previous generations, +// not ones spawned during the current page load. This prevents a race +// condition where did-finish-load fires after PTYs have already been +// created by the new page, killing them and leaving blank terminals. +let loadGeneration = 0 +const ptyLoadGeneration = new Map() + export function registerPtyHandlers(mainWindow: BrowserWindow): void { - // Kill orphaned PTY processes when the renderer reloads + // Kill orphaned PTY processes from previous page loads when the renderer reloads. + // PTYs tagged with the current loadGeneration were spawned during THIS page load + // and must be preserved — only kill PTYs from earlier generations. mainWindow.webContents.on('did-finish-load', () => { - killAllPty() + for (const [id, proc] of ptyProcesses) { + const gen = ptyLoadGeneration.get(id) ?? -1 + if (gen < loadGeneration) { + try { + proc.kill() + } catch { + // Process may already be dead + } + ptyProcesses.delete(id) + ptyLoadGeneration.delete(id) + } + } + // Advance generation for the next page load + loadGeneration++ }) ipcMain.handle('pty:spawn', (_event, args: { cols: number; rows: number; cwd?: string }) => { @@ -28,6 +51,7 @@ export function registerPtyHandlers(mainWindow: BrowserWindow): void { }) ptyProcesses.set(id, ptyProcess) + ptyLoadGeneration.set(id, loadGeneration) ptyProcess.onData((data) => { if (!mainWindow.isDestroyed()) { @@ -37,6 +61,7 @@ export function registerPtyHandlers(mainWindow: BrowserWindow): void { ptyProcess.onExit(({ exitCode }) => { ptyProcesses.delete(id) + ptyLoadGeneration.delete(id) if (!mainWindow.isDestroyed()) { mainWindow.webContents.send('pty:exit', { id, code: exitCode }) } @@ -68,6 +93,7 @@ export function registerPtyHandlers(mainWindow: BrowserWindow): void { // Process may already be dead } ptyProcesses.delete(args.id) + ptyLoadGeneration.delete(args.id) } }) } @@ -77,7 +103,12 @@ export function registerPtyHandlers(mainWindow: BrowserWindow): void { */ export function killAllPty(): void { for (const [id, proc] of ptyProcesses) { - proc.kill() + try { + proc.kill() + } catch { + // Process may already be dead + } ptyProcesses.delete(id) + ptyLoadGeneration.delete(id) } }