feat: session rename UX overhaul

- Edit button (✎) moved to left of row, separated from delete (×)
- Clicking ✎ hides name/meta/delete and expands input to full row width
- Button changes to ✓ (accent color) while editing
- Enter or ✓ click = save; Escape = cancel without saving
- Removed accidental-save-on-blur behavior
- Edit button: 30% opacity at rest, 75% on row hover, 100% on direct hover
- Touch devices: edit button always at 60% opacity (no hover to reveal it)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Scott Idem
2026-05-05 19:00:39 -04:00
parent 508fb638ad
commit 7a0fbdb659
2 changed files with 92 additions and 38 deletions

View File

@@ -507,27 +507,58 @@
const displayName = s.name || s.session_id; const displayName = s.name || s.session_id;
sessionNames.set(s.session_id, displayName); sessionNames.set(s.session_id, displayName);
const item = makeItem( const item = document.createElement('div');
s.session_id === sessionId ? 'active' : '', item.className = 'session-item' + (s.session_id === sessionId ? ' active' : '');
displayName,
`${s.message_count} msgs · ${timeAgo(s.updated)}`
);
item.addEventListener('click', () => resumeSession(s.session_id));
// Rename button (✎) // ── Edit button (left) ──────────────────────────────────
const renameBtn = document.createElement('button'); const editBtn = document.createElement('button');
renameBtn.className = 'session-rename-btn'; editBtn.className = 'session-edit-btn';
renameBtn.textContent = '✎'; editBtn.textContent = '✎';
renameBtn.title = 'Rename session'; editBtn.title = 'Rename session';
renameBtn.addEventListener('click', async (e) => {
// ── Name label ──────────────────────────────────────────
const labelEl = document.createElement('span');
labelEl.className = 'session-id';
labelEl.textContent = displayName;
// ── Meta (right of name, before delete) ─────────────────
const metaEl = document.createElement('span');
metaEl.className = 'session-meta';
metaEl.textContent = `${s.message_count} msgs · ${timeAgo(s.updated)}`;
// ── Delete button (far right) ────────────────────────────
const delBtn = document.createElement('button');
delBtn.className = 'session-delete-btn';
delBtn.textContent = '×';
delBtn.title = 'Delete session';
item.append(editBtn, labelEl, metaEl, delBtn);
// Click anywhere on the row (not a button) → resume
item.addEventListener('click', (e) => {
if (!e.target.closest('button')) resumeSession(s.session_id);
});
// ── Edit mode ────────────────────────────────────────────
function enterEditMode(e) {
e.stopPropagation(); e.stopPropagation();
const labelEl = item.querySelector('.session-id');
const current = s.name || '';
const input = document.createElement('input'); const input = document.createElement('input');
input.className = 'session-rename-input'; input.className = 'session-rename-input';
input.value = current; input.value = s.name || '';
input.placeholder = s.session_id; input.placeholder = s.session_id;
labelEl.replaceWith(input);
// Hide name, meta, delete — input takes their space
labelEl.hidden = true;
metaEl.hidden = true;
delBtn.hidden = true;
editBtn.textContent = '✓';
editBtn.title = 'Save name';
editBtn.className = 'session-save-btn';
editBtn.onclick = async (e) => { e.stopPropagation(); await commitRename(); };
editBtn.after(input);
input.focus(); input.focus();
input.select(); input.select();
@@ -538,28 +569,33 @@
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: newName }), body: JSON.stringify({ name: newName }),
}); });
const res = await fetch(`/sessions?${_fileParams}`); if (sessionId === s.session_id)
const data = await res.json();
renderPanel(data.sessions);
// Update status bar if this is the active session
if (sessionId === s.session_id) {
sessionEl.textContent = `session: ${newName || s.session_id}`; sessionEl.textContent = `session: ${newName || s.session_id}`;
}
if (newName) showToast('Session renamed', 'success'); if (newName) showToast('Session renamed', 'success');
const res = await fetch(`/sessions?${_fileParams}`);
renderPanel((await res.json()).sessions);
}
function cancelEdit() {
input.remove();
labelEl.hidden = false;
metaEl.hidden = false;
delBtn.hidden = false;
editBtn.textContent = '✎';
editBtn.title = 'Rename session';
editBtn.className = 'session-edit-btn';
editBtn.onclick = enterEditMode;
} }
input.addEventListener('keydown', (e) => { input.addEventListener('keydown', (e) => {
if (e.key === 'Enter') { e.preventDefault(); commitRename(); } if (e.key === 'Enter') { e.preventDefault(); commitRename(); }
if (e.key === 'Escape') { renderPanel(sessions); } if (e.key === 'Escape') { e.preventDefault(); cancelEdit(); }
}); });
input.addEventListener('blur', commitRename); }
});
item.appendChild(renameBtn);
const delBtn = document.createElement('button'); editBtn.onclick = enterEditMode;
delBtn.className = 'session-delete-btn';
delBtn.textContent = '×'; // ── Delete ───────────────────────────────────────────────
delBtn.title = 'Delete session';
delBtn.addEventListener('click', async (e) => { delBtn.addEventListener('click', async (e) => {
e.stopPropagation(); e.stopPropagation();
await fetch(`/sessions/${s.session_id}?${_fileParams}`, { method: 'DELETE' }); await fetch(`/sessions/${s.session_id}?${_fileParams}`, { method: 'DELETE' });
@@ -572,10 +608,8 @@
showToast('Session deleted'); showToast('Session deleted');
} }
const res = await fetch(`/sessions?${_fileParams}`); const res = await fetch(`/sessions?${_fileParams}`);
const data = await res.json(); renderPanel((await res.json()).sessions);
renderPanel(data.sessions);
}); });
item.appendChild(delBtn);
sessionsPanel.appendChild(item); sessionsPanel.appendChild(item);
} }

View File

@@ -320,7 +320,7 @@
} }
.session-delete-btn:hover { color: #e06c75; } .session-delete-btn:hover { color: #e06c75; }
.session-rename-btn { .session-edit-btn {
background: none; background: none;
border: none; border: none;
color: var(--muted); color: var(--muted);
@@ -330,13 +330,30 @@
cursor: pointer; cursor: pointer;
border-radius: 3px; border-radius: 3px;
flex-shrink: 0; flex-shrink: 0;
opacity: 0.4; opacity: 0.3;
transition: opacity 0.15s, color 0.15s; transition: opacity 0.15s, color 0.15s;
min-width: 24px; min-width: 24px;
text-align: center; text-align: center;
} }
.session-item:hover .session-rename-btn { opacity: 1; } .session-item:hover .session-edit-btn { opacity: 0.75; }
.session-rename-btn:hover { color: var(--accent); } .session-edit-btn:hover { color: var(--accent); opacity: 1; }
.session-save-btn {
background: none;
border: none;
color: var(--accent);
font-size: 1rem;
font-weight: bold;
line-height: 1;
padding: 2px 6px;
cursor: pointer;
border-radius: 3px;
flex-shrink: 0;
min-width: 24px;
text-align: center;
transition: opacity 0.15s;
}
.session-save-btn:hover { opacity: 0.75; }
.session-rename-input { .session-rename-input {
flex: 1; flex: 1;
@@ -1609,6 +1626,9 @@
min-width: 36px; min-width: 36px;
min-height: 36px; min-height: 36px;
} }
/* On touch: edit button always fully visible (no hover to reveal it) */
.session-edit-btn { opacity: 0.6; }
} }
@media (max-width: 380px) { @media (max-width: 380px) {