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:
@@ -6,7 +6,7 @@ from pydantic import BaseModel
|
|||||||
from context_loader import load_context
|
from context_loader import load_context
|
||||||
from llm_client import complete
|
from llm_client import complete
|
||||||
from session_logger import log_turn
|
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
|
from config import settings
|
||||||
import event_bus
|
import event_bus
|
||||||
|
|
||||||
@@ -143,6 +143,14 @@ async def list_sessions() -> dict:
|
|||||||
return {"sessions": list_all()}
|
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}")
|
@router.put("/history/{session_id}")
|
||||||
async def replace_history(session_id: str, req: HistoryUpdate) -> dict:
|
async def replace_history(session_id: str, req: HistoryUpdate) -> dict:
|
||||||
"""Replace the full message list for a session (used by edit/delete UI)."""
|
"""Replace the full message list for a session (used by edit/delete UI)."""
|
||||||
|
|||||||
@@ -69,6 +69,15 @@ def save(session_id: str, messages: list[dict]) -> None:
|
|||||||
}, indent=2))
|
}, 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]:
|
def list_all() -> list[dict]:
|
||||||
d = settings.sessions_path()
|
d = settings.sessions_path()
|
||||||
if not d.exists():
|
if not d.exists():
|
||||||
|
|||||||
@@ -183,6 +183,27 @@
|
|||||||
`${s.message_count} msgs · ${timeAgo(s.updated)}`
|
`${s.message_count} msgs · ${timeAgo(s.updated)}`
|
||||||
);
|
);
|
||||||
item.addEventListener('click', () => resumeSession(s.session_id));
|
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);
|
sessionsPanel.appendChild(item);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -176,6 +176,23 @@
|
|||||||
.session-item:hover { background: var(--bg); }
|
.session-item:hover { background: var(--bg); }
|
||||||
.session-item.new { color: var(--accent); justify-content: center; }
|
.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 {
|
.session-id {
|
||||||
font-family: monospace;
|
font-family: monospace;
|
||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
@@ -1029,6 +1046,29 @@
|
|||||||
#note-type-btn { padding: 6px 10px; }
|
#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) {
|
@media (max-width: 380px) {
|
||||||
header .name { font-size: 1rem; }
|
header .name { font-size: 1rem; }
|
||||||
.header-emoji { font-size: 1.3rem; }
|
.header-emoji { font-size: 1.3rem; }
|
||||||
|
|||||||
Reference in New Issue
Block a user