feat: show active elapsed timer in compact activity
This commit is contained in:
@@ -5986,7 +5986,11 @@ def _handle_chat_start(handler, body):
|
||||
daemon=True,
|
||||
)
|
||||
thr.start()
|
||||
response = {"stream_id": stream_id, "session_id": s.session_id}
|
||||
response = {
|
||||
"stream_id": stream_id,
|
||||
"session_id": s.session_id,
|
||||
"pending_started_at": s.pending_started_at,
|
||||
}
|
||||
if normalized_model:
|
||||
response["effective_model"] = model
|
||||
if model_provider:
|
||||
|
||||
BIN
docs/pr-media/1716/active-elapsed-timer.png
Normal file
BIN
docs/pr-media/1716/active-elapsed-timer.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 43 KiB |
@@ -243,6 +243,9 @@ async function send(){
|
||||
}
|
||||
streamId=startData.stream_id;
|
||||
S.activeStreamId = streamId;
|
||||
if(S.session&&typeof startData.pending_started_at==='number'){
|
||||
S.session.pending_started_at=startData.pending_started_at;
|
||||
}
|
||||
if(S.session&&S.session.session_id===activeSid){
|
||||
S.session.active_stream_id = streamId;
|
||||
}
|
||||
|
||||
89
static/ui.js
89
static/ui.js
@@ -1420,6 +1420,72 @@ function _formatTurnDuration(seconds){
|
||||
if(h)return`${h}h ${m}m`;
|
||||
return`${m}m ${s}s`;
|
||||
}
|
||||
function _formatActiveElapsedTimer(seconds){
|
||||
const n=Number(seconds);
|
||||
if(!Number.isFinite(n)||n<0)return'';
|
||||
const total=Math.max(0,Math.floor(n));
|
||||
const m=Math.floor(total/60);
|
||||
const s=total%60;
|
||||
return`${String(m).padStart(2,'0')}:${String(s).padStart(2,'0')}`;
|
||||
}
|
||||
let _activityElapsedTimer=null;
|
||||
let _activityElapsedTimerGroup=null;
|
||||
function _activityElapsedStartedAt(group){
|
||||
if(!group)return null;
|
||||
const raw=(group.dataset&&group.dataset.turnStartedAt!==undefined&&group.dataset.turnStartedAt!=='')
|
||||
?group.dataset.turnStartedAt
|
||||
:(S.session&&S.session.pending_started_at);
|
||||
const started=Number(raw);
|
||||
return Number.isFinite(started)&&started>0?started:null;
|
||||
}
|
||||
function _activityElapsedLabel(group){
|
||||
const started=_activityElapsedStartedAt(group);
|
||||
if(!started)return'';
|
||||
return _formatActiveElapsedTimer((Date.now()/1000)-started);
|
||||
}
|
||||
function _setActivityElapsedStartedAt(group){
|
||||
if(!group||group.getAttribute('data-live-tool-call-group')!=='1')return;
|
||||
const started=_activityElapsedStartedAt(group);
|
||||
if(started)group.setAttribute('data-turn-started-at',String(started));
|
||||
}
|
||||
function _updateActiveActivityElapsedTimer(){
|
||||
const group=_activityElapsedTimerGroup;
|
||||
if(!group||!group.isConnected||group.getAttribute('data-live-tool-call-group')!=='1'){
|
||||
_clearActivityElapsedTimer();
|
||||
return;
|
||||
}
|
||||
const durationEl=group.querySelector('.tool-call-group-duration');
|
||||
const label=_activityElapsedLabel(group);
|
||||
if(label){
|
||||
group.setAttribute('data-active-turn-elapsed',label);
|
||||
}else{
|
||||
group.removeAttribute('data-active-turn-elapsed');
|
||||
}
|
||||
if(durationEl){
|
||||
durationEl.textContent=label?`Working ${label}`:'';
|
||||
durationEl.style.display=label?'':'none';
|
||||
}
|
||||
}
|
||||
function _startActivityElapsedTimer(group){
|
||||
if(!group||group.getAttribute('data-live-tool-call-group')!=='1')return;
|
||||
_setActivityElapsedStartedAt(group);
|
||||
if(_activityElapsedTimerGroup&&_activityElapsedTimerGroup!==group)_clearActivityElapsedTimer();
|
||||
_activityElapsedTimerGroup=group;
|
||||
_updateActiveActivityElapsedTimer();
|
||||
if(!_activityElapsedTimer)_activityElapsedTimer=setInterval(_updateActiveActivityElapsedTimer,1000);
|
||||
}
|
||||
function _clearActivityElapsedTimer(){
|
||||
if(_activityElapsedTimer){
|
||||
clearInterval(_activityElapsedTimer);
|
||||
_activityElapsedTimer=null;
|
||||
}
|
||||
if(_activityElapsedTimerGroup&&_activityElapsedTimerGroup.isConnected){
|
||||
_activityElapsedTimerGroup.removeAttribute('data-active-turn-elapsed');
|
||||
const durationEl=_activityElapsedTimerGroup.querySelector('.tool-call-group-duration');
|
||||
if(durationEl){durationEl.textContent='';durationEl.style.display='none';}
|
||||
}
|
||||
_activityElapsedTimerGroup=null;
|
||||
}
|
||||
|
||||
const _MOBILE_CONFIG_BASE_LABEL='Workspace, model, reasoning, and context settings';
|
||||
|
||||
@@ -2438,6 +2504,7 @@ function setBusy(v){
|
||||
S.busy=v;
|
||||
updateSendBtn();
|
||||
if(!v){
|
||||
if(typeof _clearActivityElapsedTimer==='function') _clearActivityElapsedTimer();
|
||||
setStatus('');
|
||||
setComposerStatus('');
|
||||
const sid=_queueDrainSid||(S.session&&S.session.session_id);
|
||||
@@ -3727,7 +3794,9 @@ function ensureActivityGroup(inner, opts){
|
||||
if(anchor&&anchor.parentElement===inner) anchor.insertAdjacentElement('afterend', group);
|
||||
else inner.appendChild(group);
|
||||
}
|
||||
if(live) _setActivityElapsedStartedAt(group);
|
||||
_syncToolCallGroupSummary(group);
|
||||
if(live) _startActivityElapsedTimer(group);
|
||||
return group;
|
||||
}
|
||||
function _compressionStateForCurrentSession(){
|
||||
@@ -4807,9 +4876,17 @@ function _syncToolCallGroupSummary(group){
|
||||
}
|
||||
if(list) list.textContent=parts.join(' · ')||'tools / thinking';
|
||||
if(durationEl){
|
||||
const durationText=_formatTurnDuration(group.dataset.turnDuration);
|
||||
durationEl.textContent=durationText?`Done in ${durationText}`:'';
|
||||
durationEl.style.display=durationText?'':'none';
|
||||
if(group.getAttribute('data-live-tool-call-group')==='1'){
|
||||
const activeText=_activityElapsedLabel(group);
|
||||
if(activeText) group.setAttribute('data-active-turn-elapsed',activeText);
|
||||
else group.removeAttribute('data-active-turn-elapsed');
|
||||
durationEl.textContent=activeText?`Working ${activeText}`:'';
|
||||
durationEl.style.display=activeText?'':'none';
|
||||
}else{
|
||||
const durationText=_formatTurnDuration(group.dataset.turnDuration);
|
||||
durationEl.textContent=durationText?`Done in ${durationText}`:'';
|
||||
durationEl.style.display=durationText?'':'none';
|
||||
}
|
||||
}
|
||||
if(badge) badge.textContent=String(total);
|
||||
}
|
||||
@@ -4896,6 +4973,7 @@ function appendLiveToolCard(tc){
|
||||
}
|
||||
|
||||
function clearLiveToolCards(){
|
||||
if(typeof _clearActivityElapsedTimer==='function') _clearActivityElapsedTimer();
|
||||
const inner=_assistantTurnBlocks($('liveAssistantTurn'));
|
||||
if(inner) inner.querySelectorAll('.tool-call-group[data-live-tool-call-group],.tool-card-row[data-live-tid]').forEach(el=>el.remove());
|
||||
// Reset the per-turn user expand intent so the next turn starts at the
|
||||
@@ -5699,7 +5777,10 @@ function removeThinking(){
|
||||
if(blocks) blocks.querySelectorAll('.agent-activity-thinking').forEach(el=>el.remove());
|
||||
if(blocks) blocks.querySelectorAll('.tool-call-group[data-agent-activity-group="1"]').forEach(group=>{
|
||||
_syncToolCallGroupSummary(group);
|
||||
if(!group.querySelector('.tool-card-row,.agent-activity-thinking')) group.remove();
|
||||
if(!group.querySelector('.tool-card-row,.agent-activity-thinking')){
|
||||
if(typeof _clearActivityElapsedTimer==='function') _clearActivityElapsedTimer();
|
||||
group.remove();
|
||||
}
|
||||
});
|
||||
if(turn&&blocks&&!blocks.children.length) turn.remove();
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ from pathlib import Path
|
||||
REPO = Path(__file__).resolve().parent.parent
|
||||
STREAMING_PY = (REPO / "api" / "streaming.py").read_text(encoding="utf-8")
|
||||
MESSAGES_JS = (REPO / "static" / "messages.js").read_text(encoding="utf-8")
|
||||
ROUTES_PY = (REPO / "api" / "routes.py").read_text(encoding="utf-8")
|
||||
UI_JS = (REPO / "static" / "ui.js").read_text(encoding="utf-8")
|
||||
CSS = (REPO / "static" / "style.css").read_text(encoding="utf-8")
|
||||
|
||||
@@ -61,3 +62,29 @@ def test_ui_formats_and_renders_turn_duration_in_footer_and_activity_summary():
|
||||
assert ".msg-duration-inline" in CSS and ".tool-call-group-duration" in CSS, (
|
||||
"Duration UI should have explicit CSS hooks for the footer chip and compact activity summary."
|
||||
)
|
||||
|
||||
|
||||
def test_active_compact_activity_elapsed_timer_uses_persisted_start_time():
|
||||
assert '"pending_started_at": s.pending_started_at' in ROUTES_PY, (
|
||||
"/api/chat/start should return the persisted pending_started_at timestamp "
|
||||
"so the live timer starts from backend/session truth."
|
||||
)
|
||||
assert "startData.pending_started_at" in MESSAGES_JS, (
|
||||
"send() should copy chat-start pending_started_at into S.session before "
|
||||
"attaching the live stream."
|
||||
)
|
||||
assert "function _formatActiveElapsedTimer" in UI_JS and "padStart(2,'0')" in UI_JS, (
|
||||
"ui.js should format the running timer in MM:SS form."
|
||||
)
|
||||
assert "data-turn-started-at" in UI_JS and "data-active-turn-elapsed" in UI_JS, (
|
||||
"Live compact Activity groups need stable start-time and active-elapsed "
|
||||
"hooks for browser QA and reconnect/rerender safety."
|
||||
)
|
||||
assert "Working " in UI_JS, (
|
||||
"The in-progress Activity summary should distinguish the live counter "
|
||||
"from the settled 'Done in …' duration."
|
||||
)
|
||||
assert "setInterval" in UI_JS and "_clearActivityElapsedTimer" in UI_JS, (
|
||||
"The active elapsed label should tick while running and clear its interval "
|
||||
"on terminal/error/session-switch cleanup paths."
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user