fix: stale unread dot, compression timer leak, tool card duplication (#3020, #2973)

#3020: Sync viewed-count in the polling path for actively-viewed sessions
so navigating away doesn't show a stale unread dot. Defensive clear of
completion-unread marker in _setSessionViewedCount.

#2973: Clear elapsed-timer attributes and interval when a live compression
card transitions from running to done, preventing the orphan timer from
overwriting the completed card state. Guarded by active-session check.

Tool card duplication: Route tool_complete result to tc.snippet (detail)
instead of tc.preview (header) to prevent identical content appearing in
both the card header and expanded detail section.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
mysoul12138andClaude Opus 4.7 authored and nesquena-hermes committed 2026-05-30 00:20:49 +00:00
1 parent c8afb37ae7
commit 0002cda23d
3 files changed
+32 -3

No files matched your search

+9 -1
View File
@@ -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;
+10 -2
View File
@@ -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) {
+13
View File
@@ -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);