Add clear button to conversation filter
This commit is contained in:
1 parent
cbc74e33b8
commit
317d828cff
6 files changed
+101
-4
No files matched your search
@@ -3,6 +3,10 @@
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
|
||||
- Conversation filtering now shows a clear button inside the search field whenever text is present, letting users clear the filter with one click.
|
||||
|
||||
## [v0.51.161] — 2026-05-29 — Release EG (stage-batch43 — 3-PR live-display fixes: jump-to-question on intermediate assistant messages + per-turn usage badge persistence + stale unread/compression-timer/tool-card dedup)
|
||||
|
||||
### Fixed
|
||||
|
||||
+6
-1
@@ -1224,7 +1224,10 @@ document.addEventListener('keydown',async e=>{
|
||||
closeWsDropdown();
|
||||
// Clear session search
|
||||
const ss=$('sessionSearch');
|
||||
if(ss&&ss.value){ss.value='';filterSessions();}
|
||||
if(ss&&ss.value){
|
||||
if(typeof clearSessionSearch==='function') clearSessionSearch(false);
|
||||
else { ss.value=''; filterSessions(); }
|
||||
}
|
||||
// Cancel any active message edit
|
||||
const editArea=document.querySelector('.msg-edit-area');
|
||||
if(editArea){
|
||||
@@ -1754,6 +1757,7 @@ function applyBotName(){
|
||||
// separately below by a `pageshow` listener — the async IIFE here does NOT
|
||||
// re-run when the browser restores the page from bfcache.
|
||||
const _srch = document.getElementById('sessionSearch'); if (_srch) _srch.value = '';
|
||||
if (typeof syncSessionSearchClear === 'function') syncSessionSearchClear();
|
||||
// Initialize reasoning chip on boot (fixes #1103 — chip hidden until session load)
|
||||
if(typeof fetchReasoningChip==='function') fetchReasoningChip();
|
||||
if(typeof refreshProviderQuotaIndicator==='function') refreshProviderQuotaIndicator();
|
||||
@@ -1858,6 +1862,7 @@ window.addEventListener('pageshow', async (event) => {
|
||||
if (!event.persisted) return; // fresh loads are handled by the IIFE above
|
||||
const _srch = document.getElementById('sessionSearch');
|
||||
if (_srch) _srch.value = '';
|
||||
if (typeof syncSessionSearchClear === 'function') syncSessionSearchClear();
|
||||
// Close any dropdowns/popovers that were open when the user navigated away.
|
||||
// bfcache freezes DOM state, so a dropdown left open remains open on restore.
|
||||
if (typeof closeModelDropdown === 'function') try { closeModelDropdown(); } catch (_) {}
|
||||
|
||||
+1
-1
@@ -195,7 +195,7 @@
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="session-search sidebar-search"><svg class="sidebar-search-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="11" cy="11" r="8"/><path d="M21 21l-4.35-4.35"/></svg><input id="sessionSearch" placeholder="Filter conversations..." data-i18n-placeholder="filter_conversations" oninput="filterSessions()" autocomplete="off"></div>
|
||||
<div class="session-search sidebar-search"><div class="session-search-field"><svg class="sidebar-search-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="11" cy="11" r="8"/><path d="M21 21l-4.35-4.35"/></svg><input id="sessionSearch" placeholder="Filter conversations..." data-i18n-placeholder="filter_conversations" oninput="filterSessions()" autocomplete="off"><button type="button" id="sessionSearchClear" class="session-search-clear has-tooltip has-tooltip--bottom-right" aria-label="Clear conversation filter" data-tooltip="Clear conversation filter" onclick="clearSessionSearch()" hidden><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg></button></div></div>
|
||||
<div class="session-list" id="sessionList"></div>
|
||||
</div>
|
||||
<!-- Tasks (cron) panel -->
|
||||
|
||||
@@ -2666,9 +2666,29 @@ function _sessionSearchContentPreview(session, query){
|
||||
return preview||'';
|
||||
}
|
||||
|
||||
function syncSessionSearchClear(){
|
||||
const input=$('sessionSearch');
|
||||
const clear=$('sessionSearchClear');
|
||||
if(!input||!clear) return;
|
||||
clear.hidden=!Boolean(input.value);
|
||||
}
|
||||
|
||||
function clearSessionSearch(focusInput=true){
|
||||
const input=$('sessionSearch');
|
||||
if(!input) return;
|
||||
if(input.value){
|
||||
input.value='';
|
||||
filterSessions();
|
||||
}else{
|
||||
syncSessionSearchClear();
|
||||
}
|
||||
if(focusInput) input.focus();
|
||||
}
|
||||
|
||||
function filterSessions(){
|
||||
// Immediate client-side title filter (no flicker)
|
||||
// Debounced content search via API for message text
|
||||
syncSessionSearchClear();
|
||||
const q = ($('sessionSearch').value || '').trim();
|
||||
if(q!==_lastSessionSearchQuery){
|
||||
_lastSessionSearchQuery=q;
|
||||
|
||||
+9
-2
@@ -701,11 +701,18 @@
|
||||
.new-chat-btn{width:100%;padding:9px 12px;border-radius:9px;background:var(--accent-bg);border:1px solid var(--accent-bg-strong);color:var(--accent-text);font-size:13px;cursor:pointer;display:flex;align-items:center;gap:8px;transition:all .15s;margin-bottom:8px;font-weight:500;}
|
||||
.new-chat-btn:hover{background:var(--accent-bg-strong);border-color:var(--accent);}
|
||||
.session-list{flex:1;overflow-y:auto;padding:0 8px 8px;min-height:0;overscroll-behavior-y:contain;touch-action:pan-y;overflow-anchor:none;}
|
||||
.sidebar-search{position:relative;padding:8px 12px;flex-shrink:0;}
|
||||
.sidebar-search{padding:8px 12px;flex-shrink:0;}
|
||||
.session-search-field{position:relative;display:flex;align-items:center;width:100%;}
|
||||
.sidebar-search input{width:100%;background:var(--bg);border:1px solid var(--border);border-radius:8px;color:var(--text);padding:7px 10px 7px 32px;font-size:13px;outline:none;transition:border-color .15s,box-shadow .15s,background .15s;box-sizing:border-box;}
|
||||
.session-search input{padding-right:34px;}
|
||||
.sidebar-search input:focus{border-color:var(--accent);box-shadow:0 0 0 3px var(--accent-bg);}
|
||||
.sidebar-search input::placeholder{color:var(--muted);opacity:.7;}
|
||||
.sidebar-search-icon{position:absolute;left:22px;top:50%;transform:translateY(-50%);width:14px;height:14px;color:var(--muted);opacity:.7;pointer-events:none;}
|
||||
.sidebar-search-icon{position:absolute;left:10px;top:50%;transform:translateY(-50%);width:14px;height:14px;color:var(--muted);opacity:.7;pointer-events:none;}
|
||||
.session-search .session-search-clear{position:absolute;right:6px;top:50%;transform:translateY(-50%);z-index:1;width:24px;height:24px;border:0;border-radius:6px;background:transparent;color:var(--muted);display:flex;align-items:center;justify-content:center;padding:0;cursor:pointer;opacity:.78;}
|
||||
.session-search-clear[hidden]{display:none;}
|
||||
.session-search-clear svg{width:14px;height:14px;}
|
||||
.session-search-clear:hover,.session-search-clear:focus-visible{background:var(--hover);color:var(--text);opacity:1;}
|
||||
.session-search-clear:focus-visible{outline:2px solid var(--accent);outline-offset:1px;}
|
||||
/* Inline session title edit */
|
||||
.session-title-input{flex:1;background:var(--surface);border:1px solid var(--accent);border-radius:6px;color:var(--text);padding:3px 8px;font-size:13px;outline:none;min-width:0;box-shadow:0 0 0 2px var(--accent-bg-strong);font-family:inherit;}
|
||||
/* padding-right was 86px to reserve space for the absolute-positioned
|
||||
|
||||
@@ -7,6 +7,7 @@ from pathlib import Path
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
SESSIONS_JS = ROOT / "static" / "sessions.js"
|
||||
STYLE_CSS = ROOT / "static" / "style.css"
|
||||
INDEX_HTML = ROOT / "static" / "index.html"
|
||||
|
||||
|
||||
def _run_js_ranges(cases):
|
||||
@@ -85,3 +86,63 @@ def test_sidebar_search_rendering_uses_safe_dom_helpers():
|
||||
assert "if(($('sessionSearch').value||'').trim()) _hideSearchPreviewsAfterSelect=true;" in src
|
||||
assert ".session-search-preview" in css
|
||||
assert "-webkit-line-clamp:2" in css
|
||||
|
||||
|
||||
def test_session_search_has_accessible_clear_button():
|
||||
html = INDEX_HTML.read_text(encoding="utf-8")
|
||||
assert '<div class="session-search sidebar-search"><div class="session-search-field">' in html
|
||||
clear = re.search(r'<button[^>]*id="sessionSearchClear"[^>]*>', html)
|
||||
assert clear, "#sessionSearchClear button not found beside #sessionSearch"
|
||||
tag = clear.group(0)
|
||||
assert 'type="button"' in tag
|
||||
assert 'hidden' in tag
|
||||
assert 'onclick="clearSessionSearch()"' in tag
|
||||
assert 'aria-label="Clear conversation filter"' in tag
|
||||
|
||||
|
||||
def test_session_search_clear_button_styles_do_not_shift_input_width():
|
||||
css = STYLE_CSS.read_text(encoding="utf-8")
|
||||
assert ".session-search-field{position:relative;display:flex;align-items:center;width:100%;}" in css
|
||||
assert ".session-search input{padding-right:34px;}" in css
|
||||
assert ".sidebar-search-icon{position:absolute;left:10px;top:50%;transform:translateY(-50%);" in css
|
||||
assert ".session-search .session-search-clear{position:absolute;" in css
|
||||
assert "right:6px;top:50%;transform:translateY(-50%)" in css
|
||||
assert "z-index:1" in css
|
||||
assert ".session-search-clear[hidden]{display:none;}" in css
|
||||
assert ".session-search-clear:focus-visible" in css
|
||||
|
||||
|
||||
def test_session_search_clear_sync_and_click_behaviour():
|
||||
src = SESSIONS_JS.read_text(encoding="utf-8")
|
||||
start = src.index("function syncSessionSearchClear")
|
||||
end = src.index("function filterSessions", start)
|
||||
helper = src[start:end]
|
||||
script = helper + r"""
|
||||
const input = { value: 'Psalm', focused: false, focus(){ this.focused = true; } };
|
||||
const clear = { hidden: true };
|
||||
let filtered = 0;
|
||||
function $(id){ return id === 'sessionSearch' ? input : clear; }
|
||||
function filterSessions(){ filtered += 1; syncSessionSearchClear(); }
|
||||
syncSessionSearchClear();
|
||||
const visibleAfterText = clear.hidden === false;
|
||||
clearSessionSearch();
|
||||
const clearedOnClick = input.value === '' && clear.hidden === true && filtered === 1 && input.focused === true;
|
||||
input.value = 'again';
|
||||
input.focused = false;
|
||||
clearSessionSearch(false);
|
||||
const preserveFocus = input.value === '' && clear.hidden === true && filtered === 2 && input.focused === false;
|
||||
console.log(JSON.stringify({visibleAfterText, clearedOnClick, preserveFocus}));
|
||||
"""
|
||||
completed = subprocess.run(
|
||||
["node", "-e", script],
|
||||
check=True,
|
||||
text=True,
|
||||
capture_output=True,
|
||||
cwd=ROOT,
|
||||
)
|
||||
result = json.loads(completed.stdout)
|
||||
assert result == {
|
||||
"visibleAfterText": True,
|
||||
"clearedOnClick": True,
|
||||
"preserveFocus": True,
|
||||
}
|
||||
Reference in new issue
Block a user