From 24a92c8f3eb8dd58e7290486cdf3aeab955a0b86 Mon Sep 17 00:00:00 2001 From: timkjr Date: Wed, 16 Sep 2026 20:24:23 -0500 Subject: [PATCH] fix(split-pane): refuse to split a session against itself MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Nothing stopped a stale picker click (opened before switching tabs) or clicking Pane B's own session tab while split from landing on openSplitPane(sessionId) with sessionId === activeSessionId, or from selectSession() rebinding the primary pane onto the session Pane B was already showing — either way, two live WebSockets to one session, each independently claiming PTY dimensions via its own {t:'z',...} resize frame. openSplitPane() now refuses early when the target is already the active session, and a new selectSession() prototype patch (same top-level pattern as the existing _onSessionDeleted patch) closes an active split BEFORE the primary pane rebinds to the session Pane B holds. Co-Authored-By: Claude Sonnet 5 --- src/web/public/terminal-split.js | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/web/public/terminal-split.js b/src/web/public/terminal-split.js index 96c378ce..473d2aa2 100644 --- a/src/web/public/terminal-split.js +++ b/src/web/public/terminal-split.js @@ -186,6 +186,12 @@ Object.assign(CodemanApp.prototype, { }, openSplitPane(sessionId) { + // A stale picker click (opened before switching tabs) or clicking Pane + // B's own session tab while split can otherwise land here with + // sessionId === activeSessionId: two live WebSockets to the same + // session, each independently claiming PTY dimensions via its own `{t:'z',...}` + // resize frame. Refuse before creating any DOM or SplitTerminalPane. + if (sessionId === this.activeSessionId) return; if (this._splitPane) this.closeSplitPane(); const wrap = document.querySelector('.terminal-wrap'); @@ -284,3 +290,17 @@ CodemanApp.prototype._onSessionDeleted = function (data) { } return _originalOnSessionDeleted.call(this, data); }; + +// I2: closes an active split BEFORE the primary pane rebinds to the same +// session Pane B is showing (clicking Pane B's own session tab while split, +// or any other selectSession() call that targets _splitSessionId). Without +// this, Pane A rebinds to a session that Pane B's independent WebSocket is +// still attached to — two live WebSockets to one session, each claiming PTY +// dimensions via its own `{t:'z',...}` resize frame. +const _originalSelectSession = CodemanApp.prototype.selectSession; +CodemanApp.prototype.selectSession = function (sessionId, ...args) { + if (this._splitPane && this._splitSessionId === sessionId) { + this.closeSplitPane(); + } + return _originalSelectSession.call(this, sessionId, ...args); +};