diff --git a/CHANGELOG.md b/CHANGELOG.md index cf78d8c8..49d34e94 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,12 @@ ## [Unreleased] +## [v0.51.394] — 2026-06-13 — Release NG (document-title attention badge for pending prompts, #4121) + +### Added + +- **Desktop/PWA wrappers can now detect approval/clarify attention from the document title without stealing chat-title ownership (#4121).** An active chat tab prepends a `● ` marker only while the current session has a pending approval or clarification prompt; the badge clears on prompt dismissal or session switch. It composes on top of the #4086 title-owner model — `syncTopbar()` still owns the underlying `" — "` format and the badge is layered in front of it. (#4121) + ## [v0.51.393] — 2026-06-13 — Release NF (run /yolo immediately while the agent is busy) ### Fixed diff --git a/static/messages.js b/static/messages.js index 5d2812bf..7dd0d653 100644 --- a/static/messages.js +++ b/static/messages.js @@ -4127,6 +4127,14 @@ function _approvalPromptBelongsToActiveSession(sid) { return !!(sid && _promptActiveSessionId() === sid); } +function activeSessionHasPendingPromptAttention() { + const sid = _promptActiveSessionId(); + return !!(sid && ( + _approvalPendingBySession.has(sid) || + _clarifyPendingBySession.has(sid) + )); +} + function _rememberApprovalPending(pending, pendingCount) { if (!pending) return null; const sid = pending._session_id || _promptActiveSessionId(); @@ -4137,7 +4145,10 @@ function _rememberApprovalPending(pending, pendingCount) { } function _clearApprovalPendingForSession(sid) { - if (sid) _approvalPendingBySession.delete(sid); + if (sid) { + _approvalPendingBySession.delete(sid); + if (typeof syncTopbar === 'function') syncTopbar(); + } } function _hideApprovalCardIfOwner(sid, force=false) { @@ -4205,6 +4216,7 @@ function showApprovalCard(pending, pendingCount) { if (onceBtn && document.activeElement !== $('msg')) { setTimeout(() => onceBtn.focus({preventScroll: true}), 50); } + if (typeof syncTopbar === 'function') syncTopbar(); } function _syncApprovalCollapseButton(card) { @@ -4591,7 +4603,10 @@ function _rememberClarifyPending(pending) { } function _clearClarifyPendingForSession(sid) { - if (sid) _clarifyPendingBySession.delete(sid); + if (sid) { + _clarifyPendingBySession.delete(sid); + if (typeof syncTopbar === 'function') syncTopbar(); + } } function _hideClarifyCardIfOwner(sid, force=false, reason="dismissed") { @@ -4613,8 +4628,15 @@ function showClarifyForSession(sid, pending) { } function _renderPendingPromptsForActiveSession() { + const sid = _promptActiveSessionId(); _renderPendingApprovalForActiveSession(); _renderPendingClarifyForActiveSession(); + if ( + sid && + typeof activeSessionHasPendingPromptAttention === 'function' && + activeSessionHasPendingPromptAttention() + ) return; + if (typeof syncTopbar === 'function') syncTopbar(); } function _ensureClarifyCardDom() { @@ -4967,6 +4989,7 @@ function showClarifyCard(pending) { if (input && !sameClarify && document.activeElement !== $('msg')) { input.focus({preventScroll: true}); } + if (typeof syncTopbar === 'function') syncTopbar(); } async function respondClarify(response) { diff --git a/static/ui.js b/static/ui.js index 44afe003..7a4f27ce 100644 --- a/static/ui.js +++ b/static/ui.js @@ -6275,6 +6275,9 @@ function syncTopbar(){ const sessionTitle=S.session.title||t('untitled'); const _topbarTitle=$('topbarTitle');if(_topbarTitle)_topbarTitle.textContent=sessionTitle; document.title=sessionTitle+' \u2014 '+assistantDisplayName(); + if(typeof activeSessionHasPendingPromptAttention==='function'&&activeSessionHasPendingPromptAttention()){ + document.title='● '+document.title; + } const _topbarMeta=$('topbarMeta'); if(_topbarMeta){ let sourceLabel=(S.session&&(S.session.source_label||S.session.source_tag||S.session.raw_source))||''; diff --git a/tests/test_1694_prompt_ownership.py b/tests/test_1694_prompt_ownership.py index 833d4abf..251e151b 100644 --- a/tests/test_1694_prompt_ownership.py +++ b/tests/test_1694_prompt_ownership.py @@ -62,8 +62,10 @@ def test_approval_card_render_is_gated_to_active_session_and_cached(): body = _function_body(MESSAGES_JS, "showApprovalCard") assert "_rememberApprovalPending(" in body assert "_approvalPromptBelongsToActiveSession(sid)" in body + assert "syncTopbar()" in body assert "return;" in body assert "let _approvalPendingBySession" in MESSAGES_JS + assert "function activeSessionHasPendingPromptAttention()" in MESSAGES_JS assert "function _renderPendingPromptsForActiveSession()" in MESSAGES_JS @@ -71,6 +73,7 @@ def test_clarify_card_render_is_gated_to_active_session_and_cached(): body = _function_body(MESSAGES_JS, "showClarifyCard") assert "_rememberClarifyPending(" in body assert "_clarifyPromptBelongsToActiveSession(sid)" in body + assert "syncTopbar()" in body assert "return;" in body assert "let _clarifyPendingBySession" in MESSAGES_JS assert "function _renderPendingPromptsForActiveSession()" in MESSAGES_JS @@ -100,7 +103,16 @@ def test_load_session_rerenders_cached_prompt_for_new_active_session(): def test_prompt_rerender_hides_previous_session_cards_without_clearing_cache(): approval_body = _function_body(MESSAGES_JS, "_renderPendingApprovalForActiveSession") clarify_body = _function_body(MESSAGES_JS, "_renderPendingClarifyForActiveSession") + rerender_body = _function_body(MESSAGES_JS, "_renderPendingPromptsForActiveSession") + clear_approval_body = _function_body(MESSAGES_JS, "_clearApprovalPendingForSession") + clear_clarify_body = _function_body(MESSAGES_JS, "_clearClarifyPendingForSession") assert "_approvalSessionId && _approvalSessionId !== sid" in approval_body assert "hideApprovalCard(true)" in approval_body assert "_clarifySessionId && _clarifySessionId !== sid" in clarify_body assert "hideClarifyCard(true, 'session')" in clarify_body + assert "syncTopbar()" in rerender_body + assert "activeSessionHasPendingPromptAttention()" in rerender_body + assert "if (sid) {" in clear_approval_body + assert "syncTopbar()" in clear_approval_body + assert "if (sid) {" in clear_clarify_body + assert "syncTopbar()" in clear_clarify_body diff --git a/tests/test_topbar_lazy_message_count.py b/tests/test_topbar_lazy_message_count.py index c75819d0..ff37c2e9 100644 --- a/tests/test_topbar_lazy_message_count.py +++ b/tests/test_topbar_lazy_message_count.py @@ -24,6 +24,9 @@ def test_sync_topbar_does_not_count_only_loaded_tail_messages(): assert "const metaText=_topbarMessageMetaText();" in block assert "t('n_messages',vis.length)" not in block assert "S.messages.filter(m=>m&&m.role&&m.role!=='tool')" not in block + assert "document.title=sessionTitle+' \\u2014 '+assistantDisplayName();" in block + assert "document.title='● '+document.title;" in block + assert "const pendingPrefix=(typeof activeSessionHasPendingPromptAttention==='function'&&activeSessionHasPendingPromptAttention())?'● ':'';" not in block sessions_js = (ROOT / "static" / "sessions.js").read_text() fn = sessions_js[