From 526d396492b51b1ebe13159f4913073b6281a3ec Mon Sep 17 00:00:00 2001 From: Codeman maintainer Date: Tue, 6 Oct 2026 15:58:20 +0200 Subject: [PATCH] refactor(select): extract the deferred panel refresh into _refreshSessionPanels The block selectSession runs in an idle callback once the terminal content is on screen (respawn banner and countdown, action log, task panel, Ralph state, CLI info, project insights, subagent window visibility, file browser) moves verbatim into its own method. The tile grid's focus change needs the same refresh without the rest of selectSession, and one copy keeps the two from drifting. No behavior change: the stale-generation guard moves with it. Co-Authored-By: Claude Opus 5.5 (1M context) --- src/web/public/app.js | 157 +++++++++++++++++++++++------------------- 1 file changed, 85 insertions(+), 72 deletions(-) diff --git a/src/web/public/app.js b/src/web/public/app.js index 96978897..d41eeed8 100644 --- a/src/web/public/app.js +++ b/src/web/public/app.js @@ -8513,78 +8513,7 @@ class CodemanApp { // Defer secondary panel updates so they don't block the main thread // after terminal content is already visible. const idleCb = typeof requestIdleCallback === 'function' ? requestIdleCallback : (cb) => setTimeout(cb, 16); - idleCb(() => { - // Guard against stale generation — user may have switched tabs again - if (selectGen !== this._selectGeneration) return; - - // Update respawn banner - if (this.respawnStatus[sessionId]) { - this.showRespawnBanner(); - this.updateRespawnBanner(this.respawnStatus[sessionId].state); - document.getElementById('respawnCycleCount').textContent = this.respawnStatus[sessionId].cycleCount || 0; - this.updateCountdownTimerDisplay(); - this.updateActionLogDisplay(); - if (Object.keys(this.respawnCountdownTimers[sessionId] || {}).length > 0) { - this.startCountdownInterval(); - } - } else { - this.hideRespawnBanner(); - this.stopCountdownInterval(); - } - - // Update task panel if open - const taskPanel = document.getElementById('taskPanel'); - if (taskPanel && taskPanel.classList.contains('open')) { - this.renderTaskPanel(); - } - - // Update ralph state panel for this session - const curSession = this.sessions.get(sessionId); - if (curSession && (curSession.ralphLoop || curSession.ralphTodos)) { - this.updateRalphState(sessionId, { - loop: curSession.ralphLoop, - todos: curSession.ralphTodos - }); - } - this.renderRalphStatePanel(); - - // Update CLI info bar (mobile - shows Claude version/model) - this.updateCliInfoDisplay(); - - // Update project insights panel for this session - this.renderProjectInsightsPanel(); - - // Update subagent window visibility for active session - this.updateSubagentWindowVisibility(); - - // Load file browser if enabled - const settings = this.loadAppSettingsFromStorage(); - if (settings.showFileBrowser) { - const fileBrowserPanel = this.$('fileBrowserPanel'); - if (fileBrowserPanel) { - fileBrowserPanel.classList.add('visible'); - this.loadFileBrowser(sessionId); - // Attach drag listeners if not already attached - if (!this.fileBrowserDragListeners) { - const header = fileBrowserPanel.querySelector('.file-browser-header'); - if (header) { - const onFirstDrag = () => { - if (!fileBrowserPanel.style.left) { - const rect = fileBrowserPanel.getBoundingClientRect(); - fileBrowserPanel.style.left = `${rect.left}px`; - fileBrowserPanel.style.top = `${rect.top}px`; - fileBrowserPanel.style.right = 'auto'; - } - }; - header.addEventListener('mousedown', onFirstDrag); - header.addEventListener('touchstart', onFirstDrag, { passive: true }); - this.fileBrowserDragListeners = this.makeWindowDraggable(fileBrowserPanel, header); - this.fileBrowserDragListeners._onFirstDrag = onFirstDrag; - } - } - } - } - }); + idleCb(() => this._refreshSessionPanels(sessionId, selectGen)); // Open WebSocket for low-latency terminal I/O (after buffer load completes) this._connectWs(sessionId); @@ -8707,6 +8636,90 @@ class CodemanApp { } } + /** + * The panels that follow the active session (respawn banner and countdown, + * action log, task panel, Ralph state, CLI info, project insights, subagent + * window visibility, file browser). Run deferred, after the terminal content + * is on screen, by selectSession and by the tile grid's focus change + * (tile-grid.js _selectTiledSession), so both share one copy. + * + * @param {string} sessionId - the session that just became active + * @param {number} selectGen - the `_selectGeneration` of that selection; a + * newer one (the user switched again) makes this a no-op + */ + _refreshSessionPanels(sessionId, selectGen) { + // A newer selection won: the user switched tabs again. + if (selectGen !== this._selectGeneration) return; + + // Update respawn banner + if (this.respawnStatus[sessionId]) { + this.showRespawnBanner(); + this.updateRespawnBanner(this.respawnStatus[sessionId].state); + document.getElementById('respawnCycleCount').textContent = this.respawnStatus[sessionId].cycleCount || 0; + this.updateCountdownTimerDisplay(); + this.updateActionLogDisplay(); + if (Object.keys(this.respawnCountdownTimers[sessionId] || {}).length > 0) { + this.startCountdownInterval(); + } + } else { + this.hideRespawnBanner(); + this.stopCountdownInterval(); + } + + // Update task panel if open + const taskPanel = document.getElementById('taskPanel'); + if (taskPanel && taskPanel.classList.contains('open')) { + this.renderTaskPanel(); + } + + // Update ralph state panel for this session + const curSession = this.sessions.get(sessionId); + if (curSession && (curSession.ralphLoop || curSession.ralphTodos)) { + this.updateRalphState(sessionId, { + loop: curSession.ralphLoop, + todos: curSession.ralphTodos + }); + } + this.renderRalphStatePanel(); + + // Update CLI info bar (mobile - shows Claude version/model) + this.updateCliInfoDisplay(); + + // Update project insights panel for this session + this.renderProjectInsightsPanel(); + + // Update subagent window visibility for active session + this.updateSubagentWindowVisibility(); + + // Load file browser if enabled + const settings = this.loadAppSettingsFromStorage(); + if (settings.showFileBrowser) { + const fileBrowserPanel = this.$('fileBrowserPanel'); + if (fileBrowserPanel) { + fileBrowserPanel.classList.add('visible'); + this.loadFileBrowser(sessionId); + // Attach drag listeners if not already attached + if (!this.fileBrowserDragListeners) { + const header = fileBrowserPanel.querySelector('.file-browser-header'); + if (header) { + const onFirstDrag = () => { + if (!fileBrowserPanel.style.left) { + const rect = fileBrowserPanel.getBoundingClientRect(); + fileBrowserPanel.style.left = `${rect.left}px`; + fileBrowserPanel.style.top = `${rect.top}px`; + fileBrowserPanel.style.right = 'auto'; + } + }; + header.addEventListener('mousedown', onFirstDrag); + header.addEventListener('touchstart', onFirstDrag, { passive: true }); + this.fileBrowserDragListeners = this.makeWindowDraggable(fileBrowserPanel, header); + this.fileBrowserDragListeners._onFirstDrag = onFirstDrag; + } + } + } + } + } + // Shared cleanup for all session data — called from both closeSession() and session:deleted handler _cleanupSessionData(sessionId) { this.closeTabRailActionMenu?.();