Merge pull request #5436 from nesquena/release/stage-5402
Release — sidebar action menu stays open during chat scroll (#5402, closes #5347)
This commit is contained in:
commit
7ee6a12e44
3 changed files with 54 additions and 0 deletions
|
|
@ -9,6 +9,7 @@
|
|||
|
||||
### Fixed
|
||||
|
||||
- **The sidebar's session action menu no longer closes the moment you scroll the chat.** Opening a session's "…" action menu and then scrolling the conversation used to dismiss the menu immediately, because any scroll event closed it. Scrolls inside the chat transcript are now ignored (the menu stays put), scrolls of the sidebar list itself reposition the menu to keep it anchored to its row, and if a session-list refresh detaches that row the menu closes cleanly instead of floating orphaned. Thanks @nankingjing. (#5402, #5347)
|
||||
- **Transparent Stream no longer flickers or drops rows while the assistant is responding.** With Activity Display set to Transparent Stream, the live response area (text, thinking, and tool rows) used to visibly blink on every streamed update because the renderer tore down and rebuilt every live row each tick, replaying the entrance animation. Live rows are now reconciled by identity across rerenders — matching rows are refreshed in place (preserving the DOM node, its copy button, tool-call data, and expanded/collapsed state), stale rows are removed, and only genuinely new rows animate in. Thanks @rodboev. (#5400, #5367)
|
||||
- **The profile dropdown opens instantly instead of stalling on a cold fetch.** On installs with many profiles (or slow profile metadata), clicking the profile chip used to block on a fresh `/api/profiles` request before the menu appeared, so it felt unresponsive for up to a few seconds. The dropdown now opens immediately from a short-lived cache (rendering a lightweight loading shell when there's no cache yet) and refreshes fresh data in the background; a malformed cache row is validated and purged rather than shown, and a single-profile install keeps its shared cache intact. Thanks @ruizanthony. (#5412)
|
||||
- **Hardened the built-in text-to-speech proxy against a DNS-rebinding attack (SSRF).** When you route text-to-speech through an OpenAI-compatible endpoint, the proxy used to validate the target host's address and then resolve it again to connect — a gap an attacker-controlled DNS server could exploit to answer "public" during the check and "internal" at connect time, redirecting the request (with your API key attached) at a machine on your private network. The proxy now resolves the host once, validates every returned address up front (rejecting any private/loopback/link-local/reserved target), and connects only to those already-vetted addresses — while keeping the original hostname for TLS/SNI so certificate validation is unaffected. Multi-address hosts still fail over correctly across their vetted addresses, environment proxies can't bypass the direct dial, and redirects can't carry your token elsewhere. Thanks @rodboev. (#5407, #5291)
|
||||
|
|
|
|||
|
|
@ -3561,6 +3561,17 @@ function closeSessionActionMenu(){
|
|||
_sessionActionSessionId = null;
|
||||
}
|
||||
|
||||
function _sessionActionMenuShouldIgnoreScrollTarget(target){
|
||||
if(!target || typeof target.closest !== 'function') return false;
|
||||
// #5347: active-chat auto-scroll / manual wheel must not dismiss the sidebar menu.
|
||||
return Boolean(target.closest('#messages, #msgInner, .messages-inner'));
|
||||
}
|
||||
|
||||
function _sessionActionMenuShouldRepositionOnScroll(target){
|
||||
if(!target || typeof target.closest !== 'function') return false;
|
||||
return Boolean(target.closest('#sessionList, .session-list'));
|
||||
}
|
||||
|
||||
function _positionSessionActionMenu(anchorEl){
|
||||
if(!_sessionActionMenu || !anchorEl) return;
|
||||
const rect=anchorEl.getBoundingClientRect();
|
||||
|
|
@ -4036,6 +4047,15 @@ document.addEventListener('click',e=>{
|
|||
document.addEventListener('scroll',e=>{
|
||||
if(!_sessionActionMenu) return;
|
||||
if(_sessionActionMenu.contains(e.target)) return;
|
||||
if(_sessionActionMenuShouldIgnoreScrollTarget(e.target)) return;
|
||||
if(_sessionActionMenuShouldRepositionOnScroll(e.target) && _sessionActionAnchor){
|
||||
if(!_sessionActionAnchor.isConnected){
|
||||
closeSessionActionMenu();
|
||||
return;
|
||||
}
|
||||
_positionSessionActionMenu(_sessionActionAnchor);
|
||||
return;
|
||||
}
|
||||
closeSessionActionMenu();
|
||||
}, true);
|
||||
document.addEventListener('keydown',e=>{
|
||||
|
|
|
|||
33
tests/test_issue5347_sidebar_action_menu_scroll.py
Normal file
33
tests/test_issue5347_sidebar_action_menu_scroll.py
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
"""Regression coverage for #5347: chat scroll must not dismiss sidebar action menu."""
|
||||
from pathlib import Path
|
||||
|
||||
SESSIONS_JS = (Path(__file__).resolve().parent.parent / "static" / "sessions.js").read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def _scroll_listener_block() -> str:
|
||||
marker = "document.addEventListener('scroll',e=>{"
|
||||
start = SESSIONS_JS.find(marker)
|
||||
assert start != -1, "session action menu scroll listener not found"
|
||||
end = SESSIONS_JS.find("}, true);", start)
|
||||
assert end != -1, "session action menu scroll listener did not close"
|
||||
return SESSIONS_JS[start : end + len("}, true);")]
|
||||
|
||||
|
||||
def test_chat_scroll_is_ignored_while_session_action_menu_is_open():
|
||||
"""Streaming/manual chat scroll must not close the sidebar three-dot menu (#5347)."""
|
||||
block = _scroll_listener_block()
|
||||
|
||||
assert "_sessionActionMenuShouldIgnoreScrollTarget(e.target)" in block
|
||||
assert "#messages" in SESSIONS_JS
|
||||
assert "#msgInner" in SESSIONS_JS
|
||||
|
||||
|
||||
def test_session_list_scroll_repositions_instead_of_closing_action_menu():
|
||||
"""Sidebar list scroll should keep the menu aligned with its anchor row."""
|
||||
block = _scroll_listener_block()
|
||||
|
||||
assert "_sessionActionMenuShouldRepositionOnScroll(e.target)" in block
|
||||
assert block.index("_sessionActionMenuShouldRepositionOnScroll(e.target)") < block.index(
|
||||
"closeSessionActionMenu();"
|
||||
)
|
||||
assert "_positionSessionActionMenu(_sessionActionAnchor);" in block
|
||||
Loading…
Reference in a new issue