From f935fc4a7f2e5ba5afddd8fa4b9fcc1e6e51488c Mon Sep 17 00:00:00 2001 From: Scott Idem Date: Wed, 18 Mar 2026 19:43:20 -0400 Subject: [PATCH] feat: session delete + touch-friendly message controls MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- cortex/routers/chat.py | 10 +++++++++- cortex/session_store.py | 9 +++++++++ cortex/static/app.js | 21 +++++++++++++++++++++ cortex/static/style.css | 40 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 79 insertions(+), 1 deletion(-) diff --git a/cortex/routers/chat.py b/cortex/routers/chat.py index 5e4d21d..08b12fd 100644 --- a/cortex/routers/chat.py +++ b/cortex/routers/chat.py @@ -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).""" diff --git a/cortex/session_store.py b/cortex/session_store.py index 3bc8529..50e17cd 100644 --- a/cortex/session_store.py +++ b/cortex/session_store.py @@ -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(): diff --git a/cortex/static/app.js b/cortex/static/app.js index 1fa49af..3218c23 100644 --- a/cortex/static/app.js +++ b/cortex/static/app.js @@ -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); } } diff --git a/cortex/static/style.css b/cortex/static/style.css index ff8f172..1ae1ac8 100644 --- a/cortex/static/style.css +++ b/cortex/static/style.css @@ -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; }