From 944d57eedc73fd9119cd5d46aea2fef982249303 Mon Sep 17 00:00:00 2001 From: sheldon-im Date: Mon, 29 Jun 2026 22:25:45 +0900 Subject: [PATCH] Improve latest assistant response landmark --- static/ui.js | 29 ++++++++++ tests/test_a11y_transcript_landmarks.py | 54 +++++++++++++++++++ tests/test_anchor_fallback_ownership.py | 1 + tests/test_issue4793_dom_recycle_hardening.py | 7 +++ 4 files changed, 91 insertions(+) create mode 100644 tests/test_a11y_transcript_landmarks.py diff --git a/static/ui.js b/static/ui.js index ca6bdb565..6e75da2c3 100644 --- a/static/ui.js +++ b/static/ui.js @@ -453,6 +453,9 @@ const _recycleResetAttrs=[ 'data-transparent-turn-toggle-bound', 'data-anchor-scene-live-owner', 'data-anchor-stream-id', + 'data-latest-assistant-response', + 'role', + 'aria-label', // Defensive reset for legacy/restored shells that may still carry the fallback live-turn marker. 'data-live-assistant-turn', ]; @@ -8634,6 +8637,24 @@ function _createAssistantTurn(tsTitle='', tpsText=''){ row.innerHTML=`${_assistantRoleHtml(tsTitle, tpsText)}
`; return row; } +function _setLatestAssistantTurnLandmark(turn, isLatest){ + if(!turn) return; + const label='Latest Hermes response'; + if(isLatest){ + if(typeof document!=='undefined'){ + document.querySelectorAll('.assistant-turn[data-latest-assistant-response="true"]').forEach(el=>{ + if(el!==turn) _setLatestAssistantTurnLandmark(el, false); + }); + } + turn.setAttribute('role','region'); + turn.setAttribute('aria-label',label); + turn.dataset.latestAssistantResponse='true'; + return; + } + if(turn.getAttribute('role')==='region') turn.removeAttribute('role'); + if(turn.getAttribute('aria-label')===label) turn.removeAttribute('aria-label'); + delete turn.dataset.latestAssistantResponse; +} function _assistantTurnBlocks(turn){ return turn?turn.querySelector('.assistant-turn-blocks'):null; } @@ -12342,6 +12363,13 @@ function renderMessages(options){ }catch(e){} } const transparentToolResultsByTid=_transparentModeActive?_collectToolResultSnippetsByTid(S.messages):{}; + const latestRenderedAssistantRawIdx=(()=>{ + for(let i=renderVisWithIdx.length-1;i>=0;i--){ + const entry=renderVisWithIdx[i]; + if(entry&&entry.m&&entry.m.role==='assistant'&&!entry.m._live) return entry.rawIdx; + } + return -1; + })(); // Windowed render loop replaces the legacy full loop: // for(let vi=0;vi str: + start = source.find(f"function {name}(") + assert start != -1, f"{name}() not found" + end = source.find("\nfunction ", start + 1) + if end == -1: + end = len(source) + return source[start:end] + + +def test_conversation_transcript_is_not_a_named_region(): + match = re.search(r'
]*>', INDEX_HTML) + assert match, "#messages container not found" + tag = match.group(0) + assert 'role="region"' not in tag + assert 'Conversation transcript' not in tag + assert "tabindex" not in tag.lower() + + +def test_latest_assistant_landmark_helper_is_named_but_not_focusable(): + helper = _function_body(UI_JS, "_setLatestAssistantTurnLandmark") + assert "Latest Hermes response" in helper + assert "setAttribute('role','region')" in helper + assert "setAttribute('aria-label',label)" in helper + assert "data-latest-assistant-response" in helper + assert "tabindex" not in helper.lower() + assert "{" in render_messages + assert "if(entry&&entry.m&&entry.m.role==='assistant'&&!entry.m._live) return entry.rawIdx;" in render_messages + assert "_setLatestAssistantTurnLandmark(currentAssistantTurn, !m._live&&rawIdx===latestRenderedAssistantRawIdx);" in render_messages + assert "seg.setAttribute('role','region')" not in render_messages + assert "orderedSeg.setAttribute('role','region')" not in render_messages + + +def test_live_assistant_turn_paths_do_not_own_final_response_landmark(): + restore_live_turn = _function_body(UI_JS, "restoreLiveTurnHtmlForSession") + render_live_scene = _function_body(UI_JS, "renderLiveAnchorActivityScene") + append_thinking = _function_body(UI_JS, "appendThinking") + + assert "_setLatestAssistantTurnLandmark(restored, true);" not in restore_live_turn + assert "_setLatestAssistantTurnLandmark(turn, true);" not in render_live_scene + assert "_setLatestAssistantTurnLandmark(turn, true);" not in append_thinking diff --git a/tests/test_anchor_fallback_ownership.py b/tests/test_anchor_fallback_ownership.py index 041fda845..4bec8d1f1 100644 --- a/tests/test_anchor_fallback_ownership.py +++ b/tests/test_anchor_fallback_ownership.py @@ -604,6 +604,7 @@ def test_render_messages_keeps_anchor_owned_turn_out_of_legacy_activity_rebuilds return turn; }} function _assistantTurnBlocks(turn) {{ return turn ? turn.querySelector('.assistant-turn-blocks') : null; }} + function _setLatestAssistantTurnLandmark() {{}} function _assistantRoleHtml() {{ return ''; }} function _userMessageDomId(rawIdx) {{ return `user-${{rawIdx}}`; }} function _messageSessionIndexForRawIdx(rawIdx) {{ return rawIdx; }} diff --git a/tests/test_issue4793_dom_recycle_hardening.py b/tests/test_issue4793_dom_recycle_hardening.py index 76e32b3d4..b413e4917 100644 --- a/tests/test_issue4793_dom_recycle_hardening.py +++ b/tests/test_issue4793_dom_recycle_hardening.py @@ -89,6 +89,8 @@ const S = { session: { session_id: 'sess-1' } }; const _assistantRoleHtml = (title, tps) => `role:${title}:${tps}`; const isTpsDisplayEnabled = () => true; const _formatTurnTps = value => `tps:${value}`; +const latestRenderedAssistantRawIdx = rawIdx; +function _setLatestAssistantTurnLandmark() {} eval(recycleChunk); console.log(JSON.stringify({ removedAttrs, @@ -110,6 +112,9 @@ console.log(JSON.stringify({ "data-transparent-turn-toggle-bound", "data-anchor-scene-live-owner", "data-anchor-stream-id", + "data-latest-assistant-response", + "role", + "aria-label", "data-live-assistant-turn", } assert set(result["resetAttrs"]) == expected_attrs @@ -155,6 +160,8 @@ const S = { session: { session_id: 'sess-1' } }; const _assistantRoleHtml = (title, tps) => `role:${title}:${tps}`; const isTpsDisplayEnabled = () => true; const _formatTurnTps = value => `tps:${value}`; +const latestRenderedAssistantRawIdx = rawIdx; +function _setLatestAssistantTurnLandmark() {} eval(recycleChunk); console.log(JSON.stringify({ blocksInnerHTML: blocks.innerHTML,