Add clear button to conversation filter

This commit is contained in:
george-andraws authored and nesquena-hermes committed 2026-05-30 00:52:29 +00:00
1 parent cbc74e33b8
commit 317d828cff
6 files changed
+101 -4

No files matched your search

+4
View File
@@ -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
View File
@@ -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
View File
@@ -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 -->
+20
View File
@@ -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
View File
@@ -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
+61
View File
@@ -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,
}