From 516d2a588cd63d285cbefdf6b5b526e40b9bc3ce Mon Sep 17 00:00:00 2001 From: Dennis Soong Date: Mon, 18 May 2026 13:08:38 +0800 Subject: [PATCH] fix: show auto-compression elapsed time --- CHANGELOG.md | 4 ++ static/messages.js | 1 + static/ui.js | 87 ++++++++++++++++++++++++++--- tests/test_auto_compression_card.py | 42 ++++++++++++++ 4 files changed, 125 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6e938441..a6cbfb66 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Fixed + +- Show an elapsed timer on the running automatic-compression card so long WebUI context-compression pauses no longer look frozen while the browser waits for the `compressed` event. + ## [v0.51.89] — 2026-05-18 — Release BM (stage-382 — 6-PR full sweep batch — runtime adapter approval/clarify seam + SOUL.md memory panel + #1855 resolve_model_provider fast-path + PWA sidebar spinner fix + /model active-provider preference + contributor contract docs index) ### Changed diff --git a/static/messages.js b/static/messages.js index ae7c6113..98c63daf 100644 --- a/static/messages.js +++ b/static/messages.js @@ -1785,6 +1785,7 @@ function attachLiveStream(activeSid, streamId, uploaded=[], options={}){ phase:'running', automatic:true, message:d.message||'Auto-compressing context...', + startedAt:Date.now()/1000, }; setCompressionUi(state); const liveAnswerStarted=!!(assistantRow||String(((_parseStreamState&&_parseStreamState())||{}).displayText||'').trim()); diff --git a/static/ui.js b/static/ui.js index 4fedd18e..b9bbe0a1 100644 --- a/static/ui.js +++ b/static/ui.js @@ -1970,6 +1970,45 @@ function _formatActiveElapsedTimer(seconds){ const s=total%60; return`${String(m).padStart(2,'0')}:${String(s).padStart(2,'0')}`; } +const _COMPRESSION_ELAPSED_MAX_SECONDS=5*60; +let _compressionElapsedTimer=null; +function _compressionElapsedStartedAt(state){const n=Number(state&&state.startedAt);return Number.isFinite(n)&&n>0?n:null;} +function _compressionElapsedLabel(state){ + const started=_compressionElapsedStartedAt(state); + if(!started)return''; + const elapsed=Math.min(Math.max(0,(Date.now()/1000)-started),_COMPRESSION_ELAPSED_MAX_SECONDS); + return _formatActiveElapsedTimer(elapsed); +} +function _compressionElapsedExpired(state){const started=_compressionElapsedStartedAt(state);return !!(started&&((Date.now()/1000)-started)>=_COMPRESSION_ELAPSED_MAX_SECONDS);} +function _compressionLiveCardNode(){return document.querySelector('[data-live-compression-card="1"][data-compression-started-at]');} +function _compressionLiveCardState(){ + const node=_compressionLiveCardNode(); + const started=Number(node&&node.getAttribute('data-compression-started-at')); + if(!node||!S.session||!Number.isFinite(started)||started<=0)return null; + return {sessionId:S.session.session_id,phase:'running',automatic:true,message:node.getAttribute('data-compression-message')||'Auto-compressing context...',startedAt:started}; +} +function _updateCompressionElapsedCards(state){ + if(!state)return false; + const preview=_autoCompressionPreviewText(state), detail=_autoCompressionDetailText(state); + let updated=false; + document.querySelectorAll('.tool-card-compress-auto.tool-card-compress-running').forEach(card=>{ + const previewEl=card.querySelector('.tool-card-preview'); + const detailEl=card.querySelector('.tool-card-result pre'); + if(previewEl) previewEl.textContent=preview; + if(detailEl) detailEl.textContent=detail; + updated=true; + }); + return updated; +} +function _updateCompressionElapsedTimer(){ + const state=_compressionStateForCurrentSession()||_compressionLiveCardState(); + if(state&&state.automatic&&state.phase==='running'){ + _updateCompressionElapsedCards(state); + if(_compressionElapsedExpired(state)) _clearCompressionElapsedTimer(); + }else _clearCompressionElapsedTimer(); +} +function _startCompressionElapsedTimer(){if(!_compressionElapsedTimer)_compressionElapsedTimer=setInterval(_updateCompressionElapsedTimer,1000);} +function _clearCompressionElapsedTimer(){if(_compressionElapsedTimer){clearInterval(_compressionElapsedTimer);_compressionElapsedTimer=null;}} let _activityElapsedTimer=null; let _activityElapsedTimerGroup=null; function _activityElapsedStartedAt(group){ @@ -4875,6 +4914,7 @@ function isCompressionUiRunning(){ } function clearCompressionUi(){ window._compressionUi=null; + _clearCompressionElapsedTimer(); _setCompressionSessionLock(null); renderCompressionUi(); } @@ -4883,8 +4923,14 @@ function setCompressionUi(state){ clearCompressionUi(); return; } - window._compressionUi={...state}; - if(state.sessionId) _setCompressionSessionLock(state.sessionId); + const nextState={...state}; + if(nextState.automatic&&nextState.phase==='running'&&!_compressionElapsedStartedAt(nextState)){ + nextState.startedAt=Date.now()/1000; + } + window._compressionUi=nextState; + if(nextState.sessionId) _setCompressionSessionLock(nextState.sessionId); + if(nextState.automatic&&nextState.phase==='running') _startCompressionElapsedTimer(); + else _clearCompressionElapsedTimer(); renderCompressionUi(); } function _compressionCardsHtml(state){ @@ -4950,21 +4996,38 @@ function _compressionCardsHtml(state){ ${referenceHtml}`; } -function _autoCompressionCardsHtml(state){ +function _autoCompressionBaseDetail(state){ const fallback='Context auto-compressed to continue the conversation'; const running=state&&state.phase==='running'; - const detail=running + return running ? (String(state.message||'Auto-compressing context...').trim()||'Auto-compressing context...') - : (String(state.message||fallback).trim()||fallback); - const preview=running - ? detail - : (String(state.summary?.headline||detail).trim()||detail); + : (String(state&&state.message||fallback).trim()||fallback); +} +function _autoCompressionPreviewText(state){ + const running=state&&state.phase==='running'; + const detail=_autoCompressionBaseDetail(state); + if(!running) return (String(state&&state.summary?.headline||detail).trim()||detail); + const elapsedLabel=_compressionElapsedLabel(state); + return [detail, elapsedLabel].filter(Boolean).join(' · '); +} +function _autoCompressionDetailText(state){ + const running=state&&state.phase==='running'; + const detail=_autoCompressionBaseDetail(state); + const elapsedLabel=running?_compressionElapsedLabel(state):''; + return running&&elapsedLabel + ? `${detail}\nElapsed: ${elapsedLabel}` + : detail; +} +function _autoCompressionCardsHtml(state){ + const running=state&&state.phase==='running'; + const preview=_autoCompressionPreviewText(state); + const cardDetail=_autoCompressionDetailText(state); return `
${_compressionStatusCardHtml({ statusLabel: t('auto_compress_label'), previewText: preview, - detail, + detail: cardDetail, icon: running ? '' : li('check',13), open: running, variantClass: running @@ -4994,6 +5057,12 @@ function appendLiveCompressionCard(state){ const node=_compressionCardsNode(state); if(!node) return false; node.setAttribute('data-live-compression-card','1'); + if(state.automatic&&state.phase==='running'){ + const started=_compressionElapsedStartedAt(state)||Date.now()/1000; + node.setAttribute('data-compression-started-at',String(started)); + node.setAttribute('data-compression-message',String(state.message||'Auto-compressing context...')); + _startCompressionElapsedTimer(); + } const existing=inner.querySelector('[data-live-compression-card="1"]'); if(existing) existing.replaceWith(node); else inner.appendChild(node); diff --git a/tests/test_auto_compression_card.py b/tests/test_auto_compression_card.py index d092f615..a94b318a 100644 --- a/tests/test_auto_compression_card.py +++ b/tests/test_auto_compression_card.py @@ -67,6 +67,48 @@ def test_auto_compression_completion_transition_is_preserved_after_running_liste assert "phase:'done'" in _compressed_listener_block() +def test_auto_compression_running_sse_stamps_elapsed_timer_start(): + block = _compressing_listener_block() + + assert "startedAt:Date.now()/1000" in block + assert block.index("startedAt:Date.now()/1000") < block.index("setCompressionUi(state)") + + +def test_auto_compression_running_card_renders_elapsed_timer_and_caps_updates(): + src = _read("static/ui.js") + start = src.find("function _autoCompressionPreviewText") + assert start != -1, "auto compression preview helper not found" + end = src.find("function _compressionCardsNode", start) + assert end != -1, "compression cards node helper not found after auto helper" + helper = src[start:end] + + assert "const _COMPRESSION_ELAPSED_MAX_SECONDS=5*60;" in src + assert "function _compressionElapsedLabel(state)" in src + assert "_formatActiveElapsedTimer" in src + assert "_compressionElapsedLabel(state)" in helper + assert "elapsedLabel" in helper + assert "_autoCompressionPreviewText(state)" in helper + assert "_autoCompressionDetailText(state)" in helper + assert "function _startCompressionElapsedTimer()" in src + assert "function _clearCompressionElapsedTimer()" in src + assert "function _updateCompressionElapsedCards(state)" in src + assert "_startCompressionElapsedTimer();" in src + assert "_clearCompressionElapsedTimer();" in src + + +def test_auto_compression_live_card_keeps_elapsed_state_for_timer_refresh(): + src = _read("static/ui.js") + start = src.find("function appendLiveCompressionCard") + assert start != -1, "live compression card append helper not found" + end = src.find("function _isHandoffSummaryToolPayload", start) + assert end != -1, "handoff helper not found after live compression helper" + helper = src[start:end] + + assert "data-compression-started-at" in helper + assert "data-compression-message" in helper + assert "_compressionLiveCardState" in src + + def test_auto_compression_does_not_rerender_over_live_answer_text(): block = _compressing_listener_block() src = _read("static/ui.js")