diff --git a/CHANGELOG.md b/CHANGELOG.md index 11286484..c365129f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,12 @@ ## [Unreleased] +## [v0.51.145] — 2026-05-26 — Release DQ (stage-batch27 — sidebar running-state preservation) + +### Fixed + +- Sidebar session rows now preserve the server-reported running state when merging stale optimistic first-turn cache entries, so active background sessions keep their spinner and can later transition to unread correctly. (#2999, #3001) + ## [v0.51.144] — 2026-05-26 — Release DP (stage-batch26 — single-PR terminal supervisor hardening) ### Fixed diff --git a/static/sessions.js b/static/sessions.js index 0c101cff..fe84c301 100644 --- a/static/sessions.js +++ b/static/sessions.js @@ -2186,7 +2186,7 @@ function _mergeOptimisticFirstTurnSessions(fetchedSessions){ active_stream_id:fetchedIsServerIdle?null:(keepLocalOptimistic?(fetched.active_stream_id||local.active_stream_id||null):null), pending_user_message:fetchedIsServerIdle?null:(keepLocalOptimistic?(fetched.pending_user_message||local.pending_user_message||null):null), pending_started_at:fetchedIsServerIdle?null:(keepLocalOptimistic?(fetched.pending_started_at||local.pending_started_at||null):null), - is_streaming:fetchedIsServerIdle?false:(keepLocalOptimistic&&Boolean(fetched.is_streaming||local.is_streaming||_isSessionLocallyStreaming(local))), + is_streaming:fetchedIsServerIdle?false:Boolean(fetched.is_streaming||(keepLocalOptimistic&&(local.is_streaming||_isSessionLocallyStreaming(local)))), }; }else{ if(_shouldKeepLocalOnlyOptimisticSessionRow(local)){ diff --git a/tests/test_issue2454_active_session_spinner.py b/tests/test_issue2454_active_session_spinner.py index 22e7d59d..33a3f242 100644 --- a/tests/test_issue2454_active_session_spinner.py +++ b/tests/test_issue2454_active_session_spinner.py @@ -120,3 +120,57 @@ console.log(JSON.stringify(merged[0])); assert row["pending_user_message"] is None assert row["pending_started_at"] is None assert row["is_streaming"] is False + + +def test_optimistic_merge_preserves_server_running_state_when_local_cache_is_stale(): + """If the server still says running, stale local optimism must not hide the spinner.""" + helper_body = _function_body(SESSIONS_SRC, "function _isServerIdleSessionRow(") + local_body = _function_body(SESSIONS_SRC, "function _isSessionLocallyStreaming(") + optimistic_body = _function_body(SESSIONS_SRC, "function _isOptimisticFirstTurnSessionRow(") + keep_body = _function_body(SESSIONS_SRC, "function _shouldKeepLocalOnlyOptimisticSessionRow(") + drop_body = _function_body(SESSIONS_SRC, "function _dropStaleOptimisticSessionRow(") + merge_body = _function_body(SESSIONS_SRC, "function _mergeOptimisticFirstTurnSessions(") + + script = f""" +let S = {{ session: null, busy: false }}; +let _sendInProgress = false; +let _sendInProgressSid = null; +let INFLIGHT = {{}}; +let _sessionStreamingById = new Map(); +let _allSessions = [{{ + session_id: 'dogfood-session', + title: 'Local stale optimistic row', + message_count: 1, + last_message_at: 1000, + updated_at: 1000, + active_stream_id: 'stale-local-stream', + pending_user_message: 'stale pending text', + pending_started_at: 900, + is_streaming: true, +}}]; +function _isServerIdleSessionRow(s) {{{helper_body}}} +function _isSessionLocallyStreaming(s) {{{local_body}}} +function _isOptimisticFirstTurnSessionRow(s) {{{optimistic_body}}} +function _shouldKeepLocalOnlyOptimisticSessionRow(local) {{{keep_body}}} +function _dropStaleOptimisticSessionRow(sid) {{{drop_body}}} +function _mergeOptimisticFirstTurnSessions(fetchedSessions) {{{merge_body}}} +const merged = _mergeOptimisticFirstTurnSessions([{{ + session_id: 'dogfood-session', + title: 'Server running row', + message_count: 2, + last_message_at: 1100, + updated_at: 1100, + active_stream_id: null, + pending_user_message: null, + pending_started_at: null, + is_streaming: true, +}}]); +console.log(JSON.stringify(merged[0])); +""" + result = subprocess.run(["node", "-e", script], check=True, capture_output=True, text=True) + row = json.loads(result.stdout) + + assert row["session_id"] == "dogfood-session" + assert row["title"] == "Server running row" + assert row["message_count"] == 2 + assert row["is_streaming"] is True