feat: session delete + touch-friendly message controls

Session delete:
- DELETE /sessions/{session_id} endpoint (chat.py + session_store.py)
- × button on each session item in the panel (hover-reveal on desktop)
- Clears UI if the active session is deleted

Touch accessibility:
- @media (hover: none) rule makes msg-actions always visible on touch devices
- msg-act-btn tap targets enlarged to 36px min-height, readable font size
- session-delete-btn also always visible and finger-sized on touch

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Scott Idem
2026-03-18 19:43:20 -04:00
parent e6e76e7e4c
commit f935fc4a7f
4 changed files with 79 additions and 1 deletions

View File

@@ -6,7 +6,7 @@ from pydantic import BaseModel
from context_loader import load_context
from llm_client import complete
from session_logger import log_turn
from session_store import load as load_session, save as save_session, list_all, generate_session_id
from session_store import load as load_session, save as save_session, list_all, generate_session_id, delete as delete_session
from config import settings
import event_bus
@@ -143,6 +143,14 @@ async def list_sessions() -> dict:
return {"sessions": list_all()}
@router.delete("/sessions/{session_id}")
async def delete_session_endpoint(session_id: str) -> dict:
found = delete_session(session_id)
if not found:
raise HTTPException(status_code=404, detail=f"Session {session_id} not found")
return {"ok": True, "session_id": session_id}
@router.put("/history/{session_id}")
async def replace_history(session_id: str, req: HistoryUpdate) -> dict:
"""Replace the full message list for a session (used by edit/delete UI)."""

View File

@@ -69,6 +69,15 @@ def save(session_id: str, messages: list[dict]) -> None:
}, indent=2))
def delete(session_id: str) -> bool:
"""Delete a session file. Returns True if it existed and was deleted."""
path = _path(session_id)
if not path.exists():
return False
path.unlink()
return True
def list_all() -> list[dict]:
d = settings.sessions_path()
if not d.exists():

View File

@@ -183,6 +183,27 @@
`${s.message_count} msgs · ${timeAgo(s.updated)}`
);
item.addEventListener('click', () => resumeSession(s.session_id));
const delBtn = document.createElement('button');
delBtn.className = 'session-delete-btn';
delBtn.textContent = '×';
delBtn.title = 'Delete session';
delBtn.addEventListener('click', async (e) => {
e.stopPropagation();
await fetch(`/sessions/${s.session_id}`, { method: 'DELETE' });
if (sessionId === s.session_id) {
sessionId = null;
currentHistory = [];
messagesEl.innerHTML = '';
sessionEl.textContent = '';
addMessage('system', 'Session deleted');
}
const res = await fetch('/sessions');
const data = await res.json();
renderPanel(data.sessions);
});
item.appendChild(delBtn);
sessionsPanel.appendChild(item);
}
}

View File

@@ -176,6 +176,23 @@
.session-item:hover { background: var(--bg); }
.session-item.new { color: var(--accent); justify-content: center; }
.session-delete-btn {
background: none;
border: none;
color: var(--muted);
font-size: 1.1rem;
line-height: 1;
padding: 2px 4px;
cursor: pointer;
border-radius: 3px;
flex-shrink: 0;
margin-left: 4px;
opacity: 0;
transition: opacity 0.15s, color 0.15s;
}
.session-item:hover .session-delete-btn { opacity: 1; }
.session-delete-btn:hover { color: #e06c75; }
.session-id {
font-family: monospace;
font-size: 0.85rem;
@@ -1029,6 +1046,29 @@
#note-type-btn { padding: 6px 10px; }
}
/* ── Touch devices — no hover capability ─────────────────── */
/* Always show message controls; make tap targets finger-sized */
@media (hover: none) {
.msg-actions {
opacity: 1;
padding: 4px 2px 2px;
}
.msg-act-btn {
font-size: 0.78rem;
padding: 6px 12px;
min-height: 36px;
}
.session-delete-btn {
opacity: 1;
font-size: 1.3rem;
padding: 4px 8px;
min-width: 36px;
min-height: 36px;
}
}
@media (max-width: 380px) {
header .name { font-size: 1rem; }
.header-emoji { font-size: 1.3rem; }