diff --git a/static/messages.js b/static/messages.js index 05dd8d2a..085f402c 100644 --- a/static/messages.js +++ b/static/messages.js @@ -1673,7 +1673,15 @@ function attachLiveStream(activeSid, streamId, uploaded=[], options={}){ tc={name:d.name||'tool', preview:d.preview||'', args:d.args||{}, snippet:'', done:true}; inflight.toolCalls.push(tc); } - tc.preview=d.preview||tc.preview||''; + // Route result to .snippet (detail) instead of overwriting .preview + // (header). During streaming .preview already holds the last progress + // text — replacing it with the same content caused header/detail + // duplication. Fallback: if no progress events were sent, use the + // result as preview so the header is not blank. + if(d.preview){ + tc.snippet=tc.snippet||d.preview; + if(!tc.preview) tc.preview=d.preview; + } tc.args=d.args||tc.args||{}; tc.done=true; tc.is_error=!!d.is_error; diff --git a/static/sessions.js b/static/sessions.js index fc90f1f5..4a89c0d6 100644 --- a/static/sessions.js +++ b/static/sessions.js @@ -142,6 +142,9 @@ function _setSessionViewedCount(sid, messageCount = 0) { const next = Number.isFinite(messageCount) ? Number(messageCount) : 0; counts[sid] = next; _saveSessionViewedCounts(); + // If the viewed count is now current, any prior completion-unread marker is + // stale — clear it so _hasUnreadForSession doesn't short-circuit (#3020). + _clearSessionCompletionUnread(sid); } function _getSessionCompletionUnread() { @@ -407,8 +410,13 @@ function _markPollingCompletionUnreadTransitions(sessions) { ) ); const completedPersistedObservedStream = Boolean(observedStreaming && !isStreaming); - if ((completedObservedStream || completedPersistedObservedStream || completedWithNewMessages) && !_isSessionActivelyViewedForList(sid)) { - _markSessionCompletionUnread(sid, s.message_count); + if (completedObservedStream || completedPersistedObservedStream || completedWithNewMessages) { + if (!_isSessionActivelyViewedForList(sid)) { + _markSessionCompletionUnread(sid, s.message_count); + } else { + // Sync viewed count so we don't flag stale unread on tab switch (#3020) + _setSessionViewedCount(sid, messageCount); + } } _sessionStreamingById.set(sid, isStreaming); if (isStreaming) { diff --git a/static/ui.js b/static/ui.js index d209e151..2afe41ab 100644 --- a/static/ui.js +++ b/static/ui.js @@ -5682,6 +5682,19 @@ function appendLiveCompressionCard(state){ node.setAttribute('data-compression-started-at',String(started)); node.setAttribute('data-compression-message',String(state.message||'Auto-compressing context...')); _startCompressionElapsedTimer(); + } else { + // Completion or error: clear the elapsed-timer attributes so the + // interval reader (_compressionLiveCardState) doesn't keep treating + // the replaced card as a running compression (#2973). + node.removeAttribute('data-compression-started-at'); + node.removeAttribute('data-compression-message'); + // Only clear the global timer when the *active* session has no running + // compression. An SSE completion for a background session must not + // kill the timer that's driving the current session's display. + const _activeCompState = _compressionStateForCurrentSession(); + if (!_activeCompState || !_activeCompState.automatic || _activeCompState.phase !== 'running') { + _clearCompressionElapsedTimer(); + } } const existing=inner.querySelector('[data-live-compression-card="1"]'); if(existing) existing.replaceWith(node);