feat: show active elapsed timer in compact activity
This commit is contained in:
parent
0ea3dfbdd1
commit
2c5acb9725
5 changed files with 120 additions and 5 deletions
|
|
@ -5986,7 +5986,11 @@ def _handle_chat_start(handler, body):
|
||||||
daemon=True,
|
daemon=True,
|
||||||
)
|
)
|
||||||
thr.start()
|
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:
|
if normalized_model:
|
||||||
response["effective_model"] = model
|
response["effective_model"] = model
|
||||||
if model_provider:
|
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;
|
streamId=startData.stream_id;
|
||||||
S.activeStreamId = streamId;
|
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){
|
if(S.session&&S.session.session_id===activeSid){
|
||||||
S.session.active_stream_id = streamId;
|
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`;
|
if(h)return`${h}h ${m}m`;
|
||||||
return`${m}m ${s}s`;
|
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';
|
const _MOBILE_CONFIG_BASE_LABEL='Workspace, model, reasoning, and context settings';
|
||||||
|
|
||||||
|
|
@ -2438,6 +2504,7 @@ function setBusy(v){
|
||||||
S.busy=v;
|
S.busy=v;
|
||||||
updateSendBtn();
|
updateSendBtn();
|
||||||
if(!v){
|
if(!v){
|
||||||
|
if(typeof _clearActivityElapsedTimer==='function') _clearActivityElapsedTimer();
|
||||||
setStatus('');
|
setStatus('');
|
||||||
setComposerStatus('');
|
setComposerStatus('');
|
||||||
const sid=_queueDrainSid||(S.session&&S.session.session_id);
|
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);
|
if(anchor&&anchor.parentElement===inner) anchor.insertAdjacentElement('afterend', group);
|
||||||
else inner.appendChild(group);
|
else inner.appendChild(group);
|
||||||
}
|
}
|
||||||
|
if(live) _setActivityElapsedStartedAt(group);
|
||||||
_syncToolCallGroupSummary(group);
|
_syncToolCallGroupSummary(group);
|
||||||
|
if(live) _startActivityElapsedTimer(group);
|
||||||
return group;
|
return group;
|
||||||
}
|
}
|
||||||
function _compressionStateForCurrentSession(){
|
function _compressionStateForCurrentSession(){
|
||||||
|
|
@ -4807,9 +4876,17 @@ function _syncToolCallGroupSummary(group){
|
||||||
}
|
}
|
||||||
if(list) list.textContent=parts.join(' · ')||'tools / thinking';
|
if(list) list.textContent=parts.join(' · ')||'tools / thinking';
|
||||||
if(durationEl){
|
if(durationEl){
|
||||||
const durationText=_formatTurnDuration(group.dataset.turnDuration);
|
if(group.getAttribute('data-live-tool-call-group')==='1'){
|
||||||
durationEl.textContent=durationText?`Done in ${durationText}`:'';
|
const activeText=_activityElapsedLabel(group);
|
||||||
durationEl.style.display=durationText?'':'none';
|
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);
|
if(badge) badge.textContent=String(total);
|
||||||
}
|
}
|
||||||
|
|
@ -4896,6 +4973,7 @@ function appendLiveToolCard(tc){
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearLiveToolCards(){
|
function clearLiveToolCards(){
|
||||||
|
if(typeof _clearActivityElapsedTimer==='function') _clearActivityElapsedTimer();
|
||||||
const inner=_assistantTurnBlocks($('liveAssistantTurn'));
|
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());
|
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
|
// 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('.agent-activity-thinking').forEach(el=>el.remove());
|
||||||
if(blocks) blocks.querySelectorAll('.tool-call-group[data-agent-activity-group="1"]').forEach(group=>{
|
if(blocks) blocks.querySelectorAll('.tool-call-group[data-agent-activity-group="1"]').forEach(group=>{
|
||||||
_syncToolCallGroupSummary(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();
|
if(turn&&blocks&&!blocks.children.length) turn.remove();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ from pathlib import Path
|
||||||
REPO = Path(__file__).resolve().parent.parent
|
REPO = Path(__file__).resolve().parent.parent
|
||||||
STREAMING_PY = (REPO / "api" / "streaming.py").read_text(encoding="utf-8")
|
STREAMING_PY = (REPO / "api" / "streaming.py").read_text(encoding="utf-8")
|
||||||
MESSAGES_JS = (REPO / "static" / "messages.js").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")
|
UI_JS = (REPO / "static" / "ui.js").read_text(encoding="utf-8")
|
||||||
CSS = (REPO / "static" / "style.css").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, (
|
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."
|
"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."
|
||||||
|
)
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue