mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-09-30 20:49:41 +02:00
fix(sse): stop reloading the whole terminal when a reconnect lands on the same session
handleInit() did not distinguish a first load from an SSE reconnect: it always cleared the terminal caches in _resetAllAppState() and re-ran selectSession() for the session that was already on screen. Every reconnect therefore refetched up to 1 MiB of buffer and reset+rewrote xterm. On a link that drops a connection about once a minute (measured at ~57s intervals against a healthy server) that reads as the page refreshing itself and throwing away your reading position. A reconnect that lands back on the still-open session now keeps the terminal caches and activeSessionId and resyncs through _onSessionNeedsRefresh(). That path still reloads the buffer, so output produced during the outage is not lost, but it preserves distance-from-bottom — the same rule #259 established for a refresh the server triggered rather than the user. The WS is reconnected explicitly when it is not already on that session, since skipping selectSession() skips its _connectWs() call. First load (gen === 1) takes exactly the path it took before. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Rv24Pk4qzrsDYdVyDyJQmT
This commit is contained in:
+48
-13
@@ -3535,13 +3535,20 @@ class CodemanApp {
|
||||
* Reset all app state maps, timers, and handlers to a clean baseline.
|
||||
* Called by handleInit() on SSE reconnect / page reload to prevent
|
||||
* memory leaks and stale data.
|
||||
*
|
||||
* @param {boolean} [preserveTerminal] Keep the terminal caches. Set when an SSE
|
||||
* RECONNECT lands back on the session already on screen: the buffers still
|
||||
* describe that session, and dropping them forces a full refetch + xterm
|
||||
* reset that throws away the user's scroll position (see handleInit).
|
||||
*/
|
||||
_resetAllAppState() {
|
||||
_resetAllAppState(preserveTerminal = false) {
|
||||
this.sessions.clear();
|
||||
this.ralphStates.clear();
|
||||
this.terminalBuffers.clear();
|
||||
this.terminalBufferCache.clear();
|
||||
this._xtermSnapshots?.clear();
|
||||
if (!preserveTerminal) {
|
||||
this.terminalBuffers.clear();
|
||||
this.terminalBufferCache.clear();
|
||||
this._xtermSnapshots?.clear();
|
||||
}
|
||||
this.projectInsights.clear();
|
||||
this.teams.clear();
|
||||
this.teamTasks.clear();
|
||||
@@ -3668,7 +3675,23 @@ class CodemanApp {
|
||||
// Stop any active voice recording on reconnect
|
||||
VoiceInput.cleanup();
|
||||
|
||||
this._resetAllAppState();
|
||||
// A RECONNECT that lands back on the same session must not become a full
|
||||
// reload. This used to clear the terminal caches and re-run selectSession()
|
||||
// unconditionally, so every SSE reconnect refetched the buffer (up to 1 MiB)
|
||||
// and reset+rewrote xterm. On a link that drops a connection about once a
|
||||
// minute that reads as the page refreshing itself and losing your place.
|
||||
// Keep the caches and the active id here; the restore block below resyncs
|
||||
// through _onSessionNeedsRefresh(), which still reloads the buffer (so
|
||||
// output produced during the outage is not lost) but preserves the reading
|
||||
// position.
|
||||
const activeBefore = this.activeSessionId;
|
||||
const keepTerminal =
|
||||
gen > 1 &&
|
||||
!!activeBefore &&
|
||||
Array.isArray(data.sessions) &&
|
||||
data.sessions.some((s) => s.id === activeBefore);
|
||||
|
||||
this._resetAllAppState(keepTerminal);
|
||||
|
||||
data.sessions.forEach(s => {
|
||||
this.sessions.set(s.id, s);
|
||||
@@ -3798,20 +3821,32 @@ class CodemanApp {
|
||||
}
|
||||
|
||||
const previousActiveId = this.activeSessionId;
|
||||
this.activeSessionId = null;
|
||||
if (this.sessionOrder.length > 0) {
|
||||
if (this.sessionOrder.length === 0) {
|
||||
this.activeSessionId = null;
|
||||
} else {
|
||||
// Priority: current active > localStorage > first session
|
||||
let restoreId = previousActiveId;
|
||||
if (!restoreId || !this.sessions.has(restoreId)) {
|
||||
try { restoreId = localStorage.getItem('codeman-active-session'); } catch {}
|
||||
}
|
||||
// `auto`: the app is restoring a session on load, not a human opening
|
||||
// one, so a pending idle alert on that tab stays armed until it is
|
||||
// actually tapped (see the userInitiated note in selectSession).
|
||||
if (restoreId && this.sessions.has(restoreId)) {
|
||||
this.selectSession(restoreId, { auto: true });
|
||||
if (keepTerminal && restoreId === previousActiveId && this.sessions.has(restoreId)) {
|
||||
// Reconnect onto the session already on screen. renderSessionTabs() ran
|
||||
// above and activeSessionId never changed, so the tab strip is already
|
||||
// correct; only the buffer needs to catch up. The WS has its own
|
||||
// backoff reconnect, but if it is not on this session (dead socket, or
|
||||
// a give-up) nothing else would re-establish it from here.
|
||||
if (this._wsSessionId !== restoreId) this._connectWs(restoreId);
|
||||
void this._onSessionNeedsRefresh({ id: restoreId });
|
||||
} else {
|
||||
this.selectSession(this.sessionOrder[0], { auto: true });
|
||||
this.activeSessionId = null;
|
||||
// `auto`: the app is restoring a session on load, not a human opening
|
||||
// one, so a pending idle alert on that tab stays armed until it is
|
||||
// actually tapped (see the userInitiated note in selectSession).
|
||||
if (restoreId && this.sessions.has(restoreId)) {
|
||||
this.selectSession(restoreId, { auto: true });
|
||||
} else {
|
||||
this.selectSession(this.sessionOrder[0], { auto: true });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user