fix: blank terminal when switching cached sessions
Two bugs introduced by the terminal instance cache that caused blank terminals: Bug 1 (primary — every second+ visit to a cached session): switchTerminal()'s section 4 calls createTerminal(), which disposes the existing _term before creating a new one. At the time section 4 runs, _term still points to the PREVIOUSLY-ACTIVE session's terminal — the one sitting live in its own cache entry. After disposal the cache entry holds a dead xterm instance; the next visit to that session via the fast-path calls fit()/focus() on the disposed terminal, both silently no-op, and the terminal renders blank. Fix: null out _term and _fitAddon in section 4's 'Reset module-level state' block BEFORE createTerminal(), so the previously-active session's terminal is left untouched in its cache entry. Bug 2 (secondary — WS dies while session is in background): When a cached session's WebSocket closes while hidden, the close handler bails early via the stale-guard (ws !== _ws), because _ws now points to the active session's socket. No reconnect is scheduled. On revisit: isTerminalCached() returns false → openSession() POSTs /connect → but switchTerminal() takes the cache-HIT branch (_terminalCache.has() is still true), shows the container, and returns — without creating a new WebSocket. Terminal container appears but has no live connection → blank. Fix: in the cache-HIT branch, check WS readyState before taking the fast path. If the socket is CLOSED or CLOSING, call destroyCachedEntry() and fall through to section 4 so a fresh terminal + connection is created. (openSession() has already POST'd /connect, so ttyd is ready.)
This commit is contained in:
@@ -661,9 +661,14 @@ function switchTerminal(sessionName, remoteId, fontSize) {
|
||||
}
|
||||
}
|
||||
|
||||
// 3. If target is in cache — instant switch, no network
|
||||
// 3. If target is in cache — check WebSocket liveness before the fast path
|
||||
if (_terminalCache.has(key)) {
|
||||
var entry = _terminalCache.get(key);
|
||||
var wsIsLive = entry.ws &&
|
||||
(entry.ws.readyState === WebSocket.OPEN ||
|
||||
entry.ws.readyState === WebSocket.CONNECTING);
|
||||
if (wsIsLive) {
|
||||
// Fast path: live connection — swap containers, no network round-trip
|
||||
if (entry.container) {
|
||||
entry.container.style.display = '';
|
||||
}
|
||||
@@ -676,6 +681,13 @@ function switchTerminal(sessionName, remoteId, fontSize) {
|
||||
if (_term) _term.focus();
|
||||
return;
|
||||
}
|
||||
// WebSocket died while this session was in the background.
|
||||
// The close handler's stale-guard (ws !== _ws) suppresses reconnects for
|
||||
// non-active sessions, so the entry is left with a dead socket.
|
||||
// Destroy the stale entry and fall through to create a fresh terminal.
|
||||
// openSession() already POST'd /connect so the backend ttyd is ready.
|
||||
destroyCachedEntry(key);
|
||||
}
|
||||
|
||||
// 4. Not in cache — create new entry
|
||||
// Evict LRU if at capacity
|
||||
@@ -698,7 +710,12 @@ function switchTerminal(sessionName, remoteId, fontSize) {
|
||||
subContainer.style.height = '100%';
|
||||
parentContainer.appendChild(subContainer);
|
||||
|
||||
// Reset module-level state for fresh creation
|
||||
// Reset module-level state for fresh creation.
|
||||
// Null _term/_fitAddon BEFORE createTerminal() so it does NOT dispose the
|
||||
// previously-active session's terminal — that terminal lives on in its own
|
||||
// cache entry and must stay alive for instant switching later.
|
||||
_term = null;
|
||||
_fitAddon = null;
|
||||
_currentSession = null;
|
||||
_reconnectAttempts = 0;
|
||||
_reconnectTimer = null;
|
||||
|
||||
Reference in New Issue
Block a user