From 92068d39e11b6f717adf5b5188c11ce1144ae899 Mon Sep 17 00:00:00 2001 From: Raunak Raj <71929976+bajrangCoder@users.noreply.github.com> Date: Thu, 12 Mar 2026 22:02:31 +0530 Subject: [PATCH 1/2] fix(terminal): prevent restoring stale terminal sessions --- src/components/terminal/terminal.js | 121 +++++++++++++++------ src/components/terminal/terminalManager.js | 110 +++++++++++++------ 2 files changed, 164 insertions(+), 67 deletions(-) diff --git a/src/components/terminal/terminal.js b/src/components/terminal/terminal.js index 39ede08c1..4a2a2a80d 100644 --- a/src/components/terminal/terminal.js +++ b/src/components/terminal/terminal.js @@ -657,47 +657,98 @@ export default class TerminalComponent { const wsUrl = `ws://localhost:${this.options.port}/terminals/${pid}`; - this.websocket = new WebSocket(wsUrl); - - this.websocket.onopen = () => { - this.isConnected = true; - this.onConnect?.(); - - // Load attach addon after connection - this.attachAddon = new AttachAddon(this.websocket); - this.terminal.loadAddon(this.attachAddon); - this.terminal.unicode.activeVersion = "11"; + await new Promise((resolve, reject) => { + const websocket = new WebSocket(wsUrl); + const CONNECT_TIMEOUT = 5000; + let settled = false; + let hasOpened = false; + + this.websocket = websocket; + + const rejectInitialConnect = (message, error) => { + if (settled || hasOpened) return; + settled = true; + this.isConnected = false; + try { + websocket.close(); + } catch {} + reject(error || new Error(message)); + }; - // Focus terminal and ensure it's ready - this.terminal.focus(); - this.fit(); - }; + const connectionTimeout = setTimeout(() => { + rejectInitialConnect( + `Timed out while connecting to terminal session ${pid}`, + ); + }, CONNECT_TIMEOUT); + + websocket.onopen = () => { + clearTimeout(connectionTimeout); + hasOpened = true; + this.isConnected = true; + this.onConnect?.(); + + // Load attach addon after connection + this.attachAddon = new AttachAddon(websocket); + this.terminal.loadAddon(this.attachAddon); + this.terminal.unicode.activeVersion = "11"; + + // Focus terminal and ensure it's ready + this.terminal.focus(); + this.fit(); + + if (!settled) { + settled = true; + resolve(); + } + }; - this.websocket.onmessage = (event) => { - // Handle text messages (exit events) - if (typeof event.data === "string") { - try { - const message = JSON.parse(event.data); - if (message.type === "exit") { - this.onProcessExit?.(message.data); - return; + websocket.onmessage = (event) => { + // Handle text messages (exit events) + if (typeof event.data === "string") { + try { + const message = JSON.parse(event.data); + if (message.type === "exit") { + this.onProcessExit?.(message.data); + return; + } + } catch (error) { + // Not a JSON message, let attachAddon handle it } - } catch (error) { - // Not a JSON message, let attachAddon handle it } - } - // For binary data or non-exit text messages, let attachAddon handle them - }; + // For binary data or non-exit text messages, let attachAddon handle them + }; - this.websocket.onclose = (event) => { - this.isConnected = false; - this.onDisconnect?.(); - }; + websocket.onclose = (event) => { + clearTimeout(connectionTimeout); + this.isConnected = false; + + if (!hasOpened) { + const code = event?.code ? ` (code ${event.code})` : ""; + const reason = event?.reason ? `: ${event.reason}` : ""; + rejectInitialConnect( + `Terminal session ${pid} is unavailable${code}${reason}`, + ); + return; + } - this.websocket.onerror = (error) => { - console.error("WebSocket error:", error); - this.onError?.(error); - }; + this.onDisconnect?.(); + }; + + websocket.onerror = (error) => { + console.error("WebSocket error:", error); + + if (!hasOpened) { + clearTimeout(connectionTimeout); + rejectInitialConnect( + `Failed to connect to terminal session ${pid}`, + new Error(`Failed to connect to terminal session ${pid}`), + ); + return; + } + + this.onError?.(error); + }; + }); } /** diff --git a/src/components/terminal/terminalManager.js b/src/components/terminal/terminalManager.js index 8dc394948..4887c41e4 100644 --- a/src/components/terminal/terminalManager.js +++ b/src/components/terminal/terminalManager.js @@ -50,31 +50,74 @@ class TerminalManager { return nextNumber; } + normalizePersistedSessions(stored) { + if (!Array.isArray(stored)) return []; + + const sessions = stored + .map((entry) => { + if (!entry) return null; + if (typeof entry === "string") { + return { pid: entry, name: `Terminal ${entry}` }; + } + if (typeof entry === "object" && entry.pid) { + const pid = String(entry.pid); + return { + pid, + name: entry.name || `Terminal ${pid}`, + }; + } + return null; + }) + .filter(Boolean); + const uniqueSessions = []; + const seenPids = new Set(); + + for (const session of sessions) { + const pid = String(session.pid); + if (seenPids.has(pid)) continue; + seenPids.add(pid); + uniqueSessions.push({ + pid, + name: + typeof session.name === "string" && session.name.trim() + ? session.name.trim() + : `Terminal ${pid}`, + }); + } + + return uniqueSessions; + } + + readPersistedSessions() { + try { + return this.normalizePersistedSessions( + helpers.parseJSON(localStorage.getItem(TERMINAL_SESSION_STORAGE_KEY)), + ); + } catch (error) { + console.error("Failed to read persisted terminal sessions:", error); + return []; + } + } + async getPersistedSessions() { try { + const sessions = this.readPersistedSessions(); + if (!sessions.length) return []; + + if (!(await Terminal.isAxsRunning())) { + // Once the backend is gone, previously persisted PIDs are invalid. + this.savePersistedSessions([]); + return []; + } + const stored = helpers.parseJSON( localStorage.getItem(TERMINAL_SESSION_STORAGE_KEY), ); - if (!Array.isArray(stored)) return []; - if (!(await Terminal.isAxsRunning())) { - return []; + if (Array.isArray(stored) && sessions.length !== stored.length) { + this.savePersistedSessions(sessions); } - return stored - .map((entry) => { - if (!entry) return null; - if (typeof entry === "string") { - return { pid: entry, name: `Terminal ${entry}` }; - } - if (typeof entry === "object" && entry.pid) { - const pid = String(entry.pid); - return { - pid, - name: entry.name || `Terminal ${pid}`, - }; - } - return null; - }) - .filter(Boolean); + + return sessions; } catch (error) { console.error("Failed to read persisted terminal sessions:", error); return []; @@ -96,7 +139,7 @@ class TerminalManager { if (!pid) return; const pidStr = String(pid); - const sessions = await this.getPersistedSessions(); + const sessions = this.readPersistedSessions(); const existingIndex = sessions.findIndex( (session) => session.pid === pidStr, ); @@ -121,7 +164,7 @@ class TerminalManager { if (!pid) return; const pidStr = String(pid); - const sessions = await this.getPersistedSessions(); + const sessions = this.readPersistedSessions(); const nextSessions = sessions.filter((session) => session.pid !== pidStr); if (nextSessions.length !== sessions.length) { @@ -156,17 +199,17 @@ class TerminalManager { error, ); failedSessions.push(session.name || session.pid); - this.removePersistedSession(session.pid); + await this.removePersistedSession(session.pid); } } - // Show alert for failed sessions (don't await to not block UI) + // Stale session entries are expected after force-closes; keep startup quiet. if (failedSessions.length > 0) { const message = failedSessions.length === 1 - ? `Failed to restore terminal: ${failedSessions[0]}` - : `Failed to restore ${failedSessions.length} terminals: ${failedSessions.join(", ")}`; - alert(strings["error"], message); + ? `Skipped unavailable terminal: ${failedSessions[0]}` + : `Skipped ${failedSessions.length} unavailable terminals`; + toast(message); } if (activeFileId && manager?.getFile) { @@ -184,9 +227,10 @@ class TerminalManager { */ async createTerminal(options = {}) { try { - const { render, serverMode, ...terminalOptions } = options; + const { render, serverMode, reconnecting, ...terminalOptions } = options; const shouldRender = render !== false; const isServerMode = serverMode !== false; + const isReconnecting = reconnecting === true; const terminalId = `terminal_${++this.terminalCounter}`; const providedName = @@ -305,11 +349,13 @@ class TerminalManager { } // Show alert for terminal creation failure - const errorMessage = error?.message || "Unknown error"; - alert( - strings["error"], - `Failed to create terminal: ${errorMessage}`, - ); + if (!isReconnecting) { + const errorMessage = error?.message || "Unknown error"; + alert( + strings["error"], + `Failed to create terminal: ${errorMessage}`, + ); + } reject(error); } From 4b63220e48bdff88e1078f4b5b43ee09381fe466 Mon Sep 17 00:00:00 2001 From: Raunak Raj <71929976+bajrangCoder@users.noreply.github.com> Date: Thu, 12 Mar 2026 22:19:14 +0530 Subject: [PATCH 2/2] fix --- src/components/terminal/terminal.js | 3 +- src/components/terminal/terminalManager.js | 95 +++++++++++++++------- 2 files changed, 68 insertions(+), 30 deletions(-) diff --git a/src/components/terminal/terminal.js b/src/components/terminal/terminal.js index 4a2a2a80d..b8b1b4d84 100644 --- a/src/components/terminal/terminal.js +++ b/src/components/terminal/terminal.js @@ -735,8 +735,6 @@ export default class TerminalComponent { }; websocket.onerror = (error) => { - console.error("WebSocket error:", error); - if (!hasOpened) { clearTimeout(connectionTimeout); rejectInitialConnect( @@ -746,6 +744,7 @@ export default class TerminalComponent { return; } + console.error("WebSocket error:", error); this.onError?.(error); }; }); diff --git a/src/components/terminal/terminalManager.js b/src/components/terminal/terminalManager.js index 4887c41e4..174fd9b30 100644 --- a/src/components/terminal/terminalManager.js +++ b/src/components/terminal/terminalManager.js @@ -51,30 +51,57 @@ class TerminalManager { } normalizePersistedSessions(stored) { - if (!Array.isArray(stored)) return []; + if (!Array.isArray(stored)) { + return { + sessions: [], + changed: stored != null, + }; + } - const sessions = stored - .map((entry) => { - if (!entry) return null; - if (typeof entry === "string") { - return { pid: entry, name: `Terminal ${entry}` }; - } - if (typeof entry === "object" && entry.pid) { - const pid = String(entry.pid); - return { - pid, - name: entry.name || `Terminal ${pid}`, - }; - } - return null; - }) - .filter(Boolean); + const sessions = []; const uniqueSessions = []; const seenPids = new Set(); + let changed = false; + + for (const entry of stored) { + if (!entry) { + changed = true; + continue; + } + + if (typeof entry === "string") { + sessions.push({ + pid: entry, + name: `Terminal ${entry}`, + }); + changed = true; + continue; + } + + if (typeof entry !== "object" || !entry.pid) { + changed = true; + continue; + } + + const pid = String(entry.pid); + const name = + typeof entry.name === "string" && entry.name.trim() + ? entry.name.trim() + : `Terminal ${pid}`; + + if (entry.pid !== pid || entry.name !== name) { + changed = true; + } + + sessions.push({ pid, name }); + } for (const session of sessions) { const pid = String(session.pid); - if (seenPids.has(pid)) continue; + if (seenPids.has(pid)) { + changed = true; + continue; + } seenPids.add(pid); uniqueSessions.push({ pid, @@ -85,7 +112,14 @@ class TerminalManager { }); } - return uniqueSessions; + if (uniqueSessions.length !== stored.length) { + changed = true; + } + + return { + sessions: uniqueSessions, + changed, + }; } readPersistedSessions() { @@ -95,14 +129,22 @@ class TerminalManager { ); } catch (error) { console.error("Failed to read persisted terminal sessions:", error); - return []; + return { + sessions: [], + changed: false, + }; } } async getPersistedSessions() { try { - const sessions = this.readPersistedSessions(); - if (!sessions.length) return []; + const { sessions, changed } = this.readPersistedSessions(); + if (!sessions.length) { + if (changed) { + this.savePersistedSessions([]); + } + return []; + } if (!(await Terminal.isAxsRunning())) { // Once the backend is gone, previously persisted PIDs are invalid. @@ -110,10 +152,7 @@ class TerminalManager { return []; } - const stored = helpers.parseJSON( - localStorage.getItem(TERMINAL_SESSION_STORAGE_KEY), - ); - if (Array.isArray(stored) && sessions.length !== stored.length) { + if (changed) { this.savePersistedSessions(sessions); } @@ -139,7 +178,7 @@ class TerminalManager { if (!pid) return; const pidStr = String(pid); - const sessions = this.readPersistedSessions(); + const { sessions } = this.readPersistedSessions(); const existingIndex = sessions.findIndex( (session) => session.pid === pidStr, ); @@ -164,7 +203,7 @@ class TerminalManager { if (!pid) return; const pidStr = String(pid); - const sessions = this.readPersistedSessions(); + const { sessions } = this.readPersistedSessions(); const nextSessions = sessions.filter((session) => session.pid !== pidStr); if (nextSessions.length !== sessions.length) {