clean up & refactor

This commit is contained in:
dobby-d-elf committed 2026-05-21 08:25:04 -06:00
1 parent 13fa0de891
commit ad88c92155
5 files changed
+51 -52

No files matched your search

+28 -27
View File
@@ -111,6 +111,10 @@ const SESSION_SWIPE_DURATION_MS = 500;
const SESSION_SWIPE_REFLOW_LEAD_MS = 220;
const SESSION_REFLOW_TIMEOUT_MS = 420;
const SESSION_LIST_FLIP_TIMEOUT_MS = 460;
const SESSION_LONG_PRESS_DELAY_MS = 400;
const SESSION_ARCHIVE_SWIPE_THRESHOLD_PX = 128;
const SESSION_DELETE_SWIPE_THRESHOLD_PX = 128;
const SESSION_SWIPE_CANCEL_RATIO = 0.75;
function _formatSessionModelWithGateway(s){
if(!s||!s.model)return'';
@@ -1517,7 +1521,25 @@ function _playSessionRowsReflowFromPositions(before, timeoutMs, prefersReducedMo
}
function _sessionPrefersReducedMotion(){
return Boolean(window.matchMedia&&window.matchMedia('(prefers-reduced-motion: reduce)').matches);
try{
return Boolean(window.matchMedia&&window.matchMedia('(prefers-reduced-motion: reduce)').matches);
}catch(_){
return false;
}
}
function _makeSessionSwipeAffordance(side, icon, label){
const affordance=document.createElement('div');
affordance.className='session-swipe-affordance session-swipe-affordance-'+side;
affordance.setAttribute('aria-hidden','true');
const badge=document.createElement('span');
badge.className='session-swipe-badge';
badge.innerHTML=li(icon,18);
const text=document.createElement('span');
text.className='session-swipe-label';
text.textContent=label;
affordance.append(badge,text);
return affordance;
}
const SESSION_VIRTUAL_ROW_HEIGHT = 52;
const SESSION_VIRTUAL_BUFFER_ROWS = 12;
@@ -3616,6 +3638,7 @@ function renderSessionListFromCache(){
el.oncontextmenu=(e)=>{
if(readOnly) return;
e.preventDefault();
if(e.pointerType==='touch'||e.pointerType==='pen') return;
e.stopPropagation();
clearTimeout(_tapTimer);
_tapTimer=null;
@@ -3624,22 +3647,9 @@ function renderSessionListFromCache(){
_openSessionActionMenu(s, actions||el);
};
const _makeSessionSwipeAffordance=(side,icon,label)=>{
const affordance=document.createElement('div');
affordance.className='session-swipe-affordance session-swipe-affordance-'+side;
affordance.setAttribute('aria-hidden','true');
const badge=document.createElement('span');
badge.className='session-swipe-badge';
badge.innerHTML=li(icon,18);
const text=document.createElement('span');
text.className='session-swipe-label';
text.textContent=label;
affordance.append(badge,text);
return affordance;
};
if(!readOnly){
el.append(
_makeSessionSwipeAffordance('right',s.archived?'undo':'archive',s.archived?'Restore':t('session_batch_archive')),
_makeSessionSwipeAffordance('right',s.archived?'undo':'archive',s.archived?t('session_restore'):t('session_batch_archive')),
_makeSessionSwipeAffordance('left','trash-2',t('session_batch_delete')),
);
}
@@ -3663,10 +3673,6 @@ function renderSessionListFromCache(){
let _pointerX=0;
let _pointerY=0;
let _gesturePointerType='';
const _longPressDelay=400;
const _archiveSwipeActionThreshold=128;
const _deleteSwipeActionThreshold=128;
const _swipeCancelRatio=0.75;
const _clearLongPressTimer=()=>{
if(_longPressTimer){clearTimeout(_longPressTimer);_longPressTimer=null;}
if(!_longPressMenuOpened) el.classList.remove('long-pressing');
@@ -3695,7 +3701,7 @@ function renderSessionListFromCache(){
_tapTimer=null;
_lastTapTime=0;
_openSessionActionMenu(s, el);
},_longPressDelay);
},SESSION_LONG_PRESS_DELAY_MS);
};
const _isSessionSwipeTarget=()=>{
return _gesturePointerType!=='mouse'&&!readOnly&&!_renamingSid&&!_sessionSelectMode;
@@ -3769,9 +3775,9 @@ function renderSessionListFromCache(){
};
const _handleSessionSwipe=(signedDx,signedDy)=>{
if(_gestureState==='committed'||!_isSessionSwipeTarget()) return false;
const actionThreshold=signedDx>0?_archiveSwipeActionThreshold:_deleteSwipeActionThreshold;
const actionThreshold=signedDx>0?SESSION_ARCHIVE_SWIPE_THRESHOLD_PX:SESSION_DELETE_SWIPE_THRESHOLD_PX;
if(Math.abs(signedDx)<actionThreshold) return false;
if(Math.abs(signedDy)>Math.abs(signedDx)*_swipeCancelRatio) return false;
if(Math.abs(signedDy)>Math.abs(signedDx)*SESSION_SWIPE_CANCEL_RATIO) return false;
_gestureState='committed';
_clearLongPressTimer();
clearTimeout(_tapTimer);
@@ -3918,11 +3924,6 @@ function renderSessionListFromCache(){
if (_loadingSessionId && _loadingSessionId !== s.session_id) return;
startRename();
};
el.oncontextmenu=(e)=>{
if(e.pointerType==='touch'||e.pointerType==='pen'){
e.preventDefault();
}
};
el.addEventListener('touchstart',(e)=>{
if(_isSessionActionTarget(e.target)) return;
const touch=e.changedTouches&&e.changedTouches[0];
+9
View File
@@ -74,6 +74,15 @@ os.environ['HERMES_BASE_HOME'] = str(TEST_STATE_DIR)
# tests that read/write config.yaml stay inside the isolated test home.
os.environ['HERMES_CONFIG_PATH'] = str(TEST_STATE_DIR / 'config.yaml')
@pytest.fixture(autouse=True)
def _isolate_hermes_config_path():
"""Keep profile/.env side effects from leaking the live config path across tests."""
isolated_config_path = str(TEST_STATE_DIR / 'config.yaml')
os.environ['HERMES_CONFIG_PATH'] = isolated_config_path
yield
os.environ['HERMES_CONFIG_PATH'] = isolated_config_path
# ── Server script: always relative to repo root ───────────────────────────
SERVER_SCRIPT = REPO_ROOT / 'server.py'
if not SERVER_SCRIPT.exists():
@@ -55,14 +55,16 @@ def test_active_session_idle_reconcile_clears_stale_busy_and_inflight_state():
def test_session_list_payload_reconciles_active_idle_state_before_optimistic_merge_and_render():
body = _function_body(SESSIONS_SRC, "function _applySessionListPayload(")
reconcile_pos = body.find("_reconcileActiveSessionIdleStateFromList(sessData.sessions||[])")
filter_pos = body.find("const serverSessions=_optimisticallyRemovedSessionIds.size")
reconcile_pos = body.find("_reconcileActiveSessionIdleStateFromList(serverSessions)")
merge_pos = body.find("_allSessions = _mergeOptimisticFirstTurnSessions")
render_pos = body.find("renderSessionListFromCache()")
assert filter_pos != -1, "payload application must filter optimistic removals before row reconciliation"
assert reconcile_pos != -1, "active-session idle reconciliation must run for refreshed rows"
assert merge_pos != -1, "session rows must still be applied from /api/sessions"
assert render_pos != -1, "payload application must still render from cache"
assert reconcile_pos < merge_pos < render_pos, (
assert filter_pos < reconcile_pos < merge_pos < render_pos, (
"local S.busy/INFLIGHT state must be reconciled against raw server rows "
"before optimistic merging can re-label a stale active session as streaming"
)
@@ -120,7 +120,7 @@ def test_plain_mouse_hover_does_not_mark_session_row_dragging():
"""Pointermove fires during ordinary hover; drag styling must require an active press."""
assert "let _gestureState='idle';" in SESSIONS_JS
assert "_gestureState='pressing';" in SESSIONS_JS
assert "if(_gestureState==='idle') return;" in SESSIONS_JS
assert "if(_gestureState==='idle') return false;" in SESSIONS_JS
assert "_gestureState='idle';" in SESSIONS_JS
assert ".session-item.dragging:hover" in STYLE_CSS
+9 -22
View File
@@ -24,25 +24,17 @@ def test_session_menu_uses_viewport_height_not_fixed_scroll_cap():
assert "max-height:320px" not in session_menu
def test_session_menu_has_subtle_open_animation():
def test_session_menu_respects_motion_preferences():
session_menu = STYLE_CSS[STYLE_CSS.find(".session-action-menu{"):STYLE_CSS.find(".session-action-menu.open")]
assert "will-change:opacity,transform" in session_menu
assert "transform-origin:top right" in session_menu
assert "function _playSessionActionMenuEntrance(menu){" in SESSIONS_JS
assert "typeof menu.animate==='function'" in SESSIONS_JS
assert "{opacity:0, transform:'translate3d(0,-4px,0) scale(.985)'}" in SESSIONS_JS
assert "{duration:450, easing:'cubic-bezier(.2,.8,.2,1)'}" in SESSIONS_JS
assert "menu.classList.add('open-animated')" in SESSIONS_JS
assert ".session-action-menu.open-animated{animation:session-menu-in .45s cubic-bezier(.2,.8,.2,1);}" in STYLE_CSS
assert "@keyframes session-menu-in" in STYLE_CSS
assert "@media (prefers-reduced-motion:reduce)" in STYLE_CSS
assert ".session-action-menu{animation:none;will-change:auto;}" in STYLE_CSS
assert ".session-item,.session-item.dragging,.session-item.session-reflowing,.session-item.swipe-committed,.session-item.swipe-removing{transition:none;}" in STYLE_CSS
assert ".session-item.long-pressing,.session-item.session-swipe-returning{animation:none;}" in STYLE_CSS
def test_mobile_session_menu_opens_from_long_press_and_hides_dots():
assert "_longPressDelay=400" in SESSIONS_JS
assert "const SESSION_LONG_PRESS_DELAY_MS = 400;" in SESSIONS_JS
assert "el.classList.add('long-pressing')" in SESSIONS_JS
assert "if(!_longPressMenuOpened) el.classList.remove('long-pressing')" in SESSIONS_JS
assert "row.classList.remove('menu-open','long-pressing')" in SESSIONS_JS
@@ -58,6 +50,10 @@ def test_mobile_session_menu_opens_from_long_press_and_hides_dots():
def test_open_session_menu_consumes_next_row_activation():
context_menu = _sessions_block("el.oncontextmenu=(e)=>{", "// Use release events")
assert SESSIONS_JS.count("el.oncontextmenu=(e)=>{") == 1
assert "if(e.pointerType==='touch'||e.pointerType==='pen') return;" in context_menu
assert "_openSessionActionMenu(s, actions||el);" in context_menu
assert "if(_sessionActionMenu&&!_sessionActionMenu.contains(target)){" in SESSIONS_JS
assert "closeSessionActionMenu();" in SESSIONS_JS
assert "e.stopPropagation();" in SESSIONS_JS
@@ -84,14 +80,12 @@ def test_open_session_menu_consumes_next_row_activation():
def test_session_swipes_route_archive_restore_and_delete():
assert "_gesturePointerType!=='mouse'" in SESSIONS_JS
assert "_swipeTracking=true" in SESSIONS_JS
assert "_archiveSwipeActionThreshold=128" in SESSIONS_JS
assert "_deleteSwipeActionThreshold=128" in SESSIONS_JS
assert "const SESSION_ARCHIVE_SWIPE_THRESHOLD_PX = 128;" in SESSIONS_JS
assert "const SESSION_DELETE_SWIPE_THRESHOLD_PX = 128;" in SESSIONS_JS
assert "const SESSION_SWIPE_DURATION_MS = 500;" in SESSIONS_JS
assert "const SESSION_SWIPE_REFLOW_LEAD_MS = 220;" in SESSIONS_JS
assert "const committedSwipeDuration=_sessionPrefersReducedMotion()?0:SESSION_SWIPE_DURATION_MS;" in SESSIONS_JS
assert "const committedSwipeReflowDelay=Math.max(0,committedSwipeDuration-SESSION_SWIPE_REFLOW_LEAD_MS);" in SESSIONS_JS
swipe_block = _sessions_block("const _handleSessionSwipe=(signedDx,signedDy)=>{", "const _commitSessionSwipe=()=>{")
assert "const actionThreshold=signedDx>0?_archiveSwipeActionThreshold:_deleteSwipeActionThreshold;" in SESSIONS_JS
assert "const actionThreshold=signedDx>0?SESSION_ARCHIVE_SWIPE_THRESHOLD_PX:SESSION_DELETE_SWIPE_THRESHOLD_PX;" in SESSIONS_JS
assert "if(Math.abs(signedDx)<actionThreshold) return false;" in SESSIONS_JS
assert "const _updateSessionGesture=(clientX,clientY)=>{" in SESSIONS_JS
assert "if(_isSessionSwipeTarget()&&(_swipeTracking||dx>dy)) _paintSessionSwipe(signedDx)" in SESSIONS_JS
@@ -116,8 +110,6 @@ def test_session_swipes_route_archive_restore_and_delete():
assert delete_branch.find("deleteSession(s.session_id,async()=>{") < delete_branch.find("_completeSessionSwipePaint(signedDx);")
assert "showToast('Imported sessions cannot be deleted here.',3000);" in SESSIONS_JS
assert "let _gestureState='idle';" in SESSIONS_JS
assert "const _commitSessionSwipe=()=>{" in SESSIONS_JS
assert "if(_gestureState==='committed'){" in SESSIONS_JS
assert SESSIONS_JS.count("if(e.pointerType==='touch') return;") >= 3
assert "if(_gesturePointerType==='mouse'&&_gestureState!=='idle') _clearPointerDragState();" in SESSIONS_JS
@@ -136,10 +128,8 @@ def test_session_swipe_paint_uses_transform_only_exit():
assert "requestAnimationFrame(()=>requestAnimationFrame(_clearSessionSwipePaint))" in SESSIONS_JS
assert "el.classList.remove('swiping-right','swiping-left','swipe-committed','swipe-removing')" in clear
assert ".session-item.swipe-committed,\n .session-item.swipe-removing{transition:" in STYLE_CSS
assert "transition:background .15s,color .15s,transform .5s cubic-bezier(.22,.61,.36,1),box-shadow .15s ease" in STYLE_CSS
assert "transform:translate3d(calc(-1 * var(--session-swipe-offset,0px)),0,0)" in STYLE_CSS
assert "transform:scale(var(--session-swipe-icon-scale,1))" in STYLE_CSS
assert "min-width:36px" not in STYLE_CSS[STYLE_CSS.find(".session-swipe-affordance{"):STYLE_CSS.find(".session-swipe-affordance-right{")]
swipe_start = STYLE_CSS.find(".session-item.swipe-removing{")
swipe_end = STYLE_CSS.find("}", swipe_start)
assert swipe_start >= 0 and swipe_end > swipe_start
@@ -160,7 +150,6 @@ def test_session_removal_reflows_surviving_rows_smoothly():
assert "const delta=oldTop-row.getBoundingClientRect().top;" in helper
assert "const movingRows=[];" in helper
assert "list.getBoundingClientRect();" in helper
assert helper.count("getBoundingClientRect()") == 2
assert "row.style.transition='none';" in helper
assert "row.classList.add('session-reflowing')" in helper
assert "requestAnimationFrame(()=>requestAnimationFrame(()=>{" in helper
@@ -177,8 +166,6 @@ def test_session_removal_reflows_surviving_rows_smoothly():
assert "if(_showArchived&&!_sessionPrefersReducedMotion()) _sessionSwipeReturnOffsets.set(session.session_id,'0px');" in SESSIONS_JS
assert "const swipeReturnOffset=_sessionSwipeReturnOffsets.get(s.session_id);" in SESSIONS_JS
assert "el.classList.add('session-swipe-returning');" in SESSIONS_JS
assert "@keyframes sessionSwipeReturn" in STYLE_CSS
assert ".session-item.session-swipe-returning{animation:sessionSwipeReturn .5s cubic-bezier(.22,.61,.36,1) both;will-change:transform,opacity;}" in STYLE_CSS
delete_start = SESSIONS_JS.find("async function deleteSession(sid, beforeDelete=null){")
delete_end = SESSIONS_JS.find("// ── Project helpers", delete_start)
assert delete_start >= 0 and delete_end > delete_start