From 9702739e670edfb985eefffa32353d7efb796683 Mon Sep 17 00:00:00 2001 From: arkon Date: Thu, 12 Feb 2026 20:07:36 +0100 Subject: [PATCH] fix: restore active session tab on page reload (mobile + desktop) Save activeSessionId to localStorage on tab switch and restore it in handleInit so reloading the page jumps back to the session you were viewing instead of always selecting the first tab. Co-Authored-By: Claude Opus 4.6 --- src/web/public/app.js | 45 ++++++++++++++++++++++++++++++------------- 1 file changed, 32 insertions(+), 13 deletions(-) diff --git a/src/web/public/app.js b/src/web/public/app.js index 12ccbfd3..9054f88a 100644 --- a/src/web/public/app.js +++ b/src/web/public/app.js @@ -1191,7 +1191,7 @@ class ClaudemanApp { this.pendingHooks = new Map(); // Terminal write batching with DEC 2026 sync support - this.pendingWrites = ''; + this.pendingWrites = []; this.writeFrameScheduled = false; this._wasAtBottomBeforeWrite = true; // Default to true for sticky scroll this.syncWaitTimeout = null; // Timeout for incomplete sync blocks @@ -1765,15 +1765,17 @@ class ClaudemanApp { } // Accumulate raw data (may contain DEC 2026 markers) - this.pendingWrites += data; + this.pendingWrites.push(data); if (!this.writeFrameScheduled) { this.writeFrameScheduled = true; requestAnimationFrame(() => { - if (this.pendingWrites && this.terminal) { + if (this.pendingWrites.length > 0 && this.terminal) { + // Join chunks for sync marker detection + const pending = this.pendingWrites.join(''); // Check if we have an incomplete sync block (SYNC_START without SYNC_END) - const hasStart = this.pendingWrites.includes(DEC_SYNC_START); - const hasEnd = this.pendingWrites.includes(DEC_SYNC_END); + const hasStart = pending.includes(DEC_SYNC_START); + const hasEnd = pending.includes(DEC_SYNC_END); if (hasStart && !hasEnd) { // Incomplete sync block - wait for more data (up to 50ms max) @@ -1809,7 +1811,7 @@ class ClaudemanApp { if (!this.flickerFilterBuffer) return; // Transfer buffered data to normal pending writes - this.pendingWrites += this.flickerFilterBuffer; + this.pendingWrites.push(this.flickerFilterBuffer); this.flickerFilterBuffer = ''; this.flickerFilterActive = false; @@ -1828,12 +1830,12 @@ class ClaudemanApp { * Strips markers and writes content atomically within a single frame. */ flushPendingWrites() { - if (!this.pendingWrites || !this.terminal) return; + if (this.pendingWrites.length === 0 || !this.terminal) return; // Extract segments, stripping DEC 2026 markers // This implements synchronized output for xterm.js which doesn't support DEC 2026 natively - const segments = extractSyncSegments(this.pendingWrites); - this.pendingWrites = ''; + const segments = extractSyncSegments(this.pendingWrites.join('')); + this.pendingWrites = []; // Write all segments in a single batch (atomic within this frame) // xterm.js internally batches multiple write() calls within same frame @@ -2126,6 +2128,7 @@ class ClaudemanApp { delete this.respawnActionLogs[data.id]; if (this.activeSessionId === data.id) { this.activeSessionId = null; + try { localStorage.removeItem('claudeman-active-session'); } catch {} this.terminal.clear(); this.showWelcome(); } @@ -3082,7 +3085,7 @@ class ClaudemanApp { clearTimeout(this.syncWaitTimeout); this.syncWaitTimeout = null; } - this.pendingWrites = ''; + this.pendingWrites = []; this.writeFrameScheduled = false; // Clear pending hooks this.pendingHooks.clear(); @@ -3208,9 +3211,15 @@ class ClaudemanApp { }); } - // Auto-select first session if any (use sessionOrder for user's preferred order) + // Restore previously active session (survives page reload), or fall back to first if (this.sessionOrder.length > 0 && !this.activeSessionId) { - this.selectSession(this.sessionOrder[0]); + let restoreId = null; + try { restoreId = localStorage.getItem('claudeman-active-session'); } catch {} + if (restoreId && this.sessions.has(restoreId)) { + this.selectSession(restoreId); + } else { + this.selectSession(this.sessionOrder[0]); + } } } @@ -3747,10 +3756,11 @@ class ClaudemanApp { clearTimeout(this.syncWaitTimeout); this.syncWaitTimeout = null; } - this.pendingWrites = ''; + this.pendingWrites = []; this.writeFrameScheduled = false; this.activeSessionId = sessionId; + try { localStorage.setItem('claudeman-active-session', sessionId); } catch {} this.hideWelcome(); // Clear idle hooks on view, but keep action hooks until user interacts this.clearPendingHooks(sessionId, 'idle_prompt'); @@ -3876,6 +3886,7 @@ class ClaudemanApp { if (this.activeSessionId === sessionId) { this.activeSessionId = null; + try { localStorage.removeItem('claudeman-active-session'); } catch {} // Select another session or show welcome (use sessionOrder for consistent ordering) if (this.sessionOrder.length > 0 && this.sessions.size > 0) { const nextSessionId = this.sessionOrder[0]; @@ -3949,6 +3960,7 @@ class ClaudemanApp { goHome() { // Deselect active session and show welcome screen this.activeSessionId = null; + try { localStorage.removeItem('claudeman-active-session'); } catch {} this.terminal.clear(); this.showWelcome(); this.renderSessionTabs(); @@ -7707,6 +7719,7 @@ class ClaudemanApp { this.sessions.clear(); this.terminalBuffers.clear(); this.activeSessionId = null; + try { localStorage.removeItem('claudeman-active-session'); } catch {} this.respawnStatus = {}; this.respawnCountdownTimers = {}; this.respawnActionLogs = {}; @@ -13984,6 +13997,7 @@ class ClaudemanApp { this.sessions.clear(); this.screenSessions = []; this.activeSessionId = null; + try { localStorage.removeItem('claudeman-active-session'); } catch {} this.renderSessionTabs(); this.renderScreenSessions(); this.terminal.clear(); @@ -13994,6 +14008,7 @@ class ClaudemanApp { // Just remove tabs, keep screens running this.sessions.clear(); this.activeSessionId = null; + try { localStorage.removeItem('claudeman-active-session'); } catch {} this.renderSessionTabs(); this.terminal.clear(); this.terminal.reset(); @@ -14529,6 +14544,10 @@ class ClaudemanApp { } async fetchSystemStats() { + // Skip polling when monitor panel is not visible + const monitorPanel = document.getElementById('monitorPanel'); + if (!monitorPanel || monitorPanel.style.display === 'none') return; + try { const res = await fetch('/api/system/stats'); const stats = await res.json();