From 528537a043aa4962b5c481e3cd1c057cf0097708 Mon Sep 17 00:00:00 2001 From: DerekfromMadison Date: Sat, 18 Apr 2026 10:03:38 -0500 Subject: [PATCH 1/2] feat(frontend): inline rename for session names Double-click the session name in the drawer or tab bar to rename in place; the gear menu's Rename item and the mux session pencil button use the same inline input. Replaces the window.prompt flow with a single keystroke-friendly editor (Enter to commit, Escape to cancel, blur to save). --- src/web/public/app.js | 90 +++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 86 insertions(+), 4 deletions(-) diff --git a/src/web/public/app.js b/src/web/public/app.js index d5b9bf8b..943675f7 100644 --- a/src/web/public/app.js +++ b/src/web/public/app.js @@ -8956,7 +8956,7 @@ class CodemanApp { ${mode === 'shell' ? '' : mode === 'opencode' ? '' : ''} - ${escapeHtml(name)} + ${escapeHtml(name)} ${showFolder ? `\u{1F4C1} ${escapeHtml(folderName)}` : ''} @@ -11380,6 +11380,24 @@ class CodemanApp { menu.appendChild(sep); } + // Rename item + const renameItem = document.createElement('div'); + renameItem.className = 'session-ctx-item'; + renameItem.setAttribute('role', 'menuitem'); + renameItem.tabIndex = 0; + const renameIcon = document.createElement('span'); + renameIcon.className = 'session-ctx-icon'; + renameIcon.textContent = '\u270E'; + const renameLabel = document.createElement('span'); + renameLabel.textContent = 'Rename'; + renameItem.appendChild(renameIcon); + renameItem.appendChild(renameLabel); + renameItem.addEventListener('click', () => { + this.closeSessionContextMenu(); + this.startInlineRename(sessionId); + }); + menu.appendChild(renameItem); + // Remove item const removeItem = document.createElement('div'); removeItem.className = 'session-ctx-item session-ctx-item-danger'; @@ -13205,7 +13223,7 @@ class CodemanApp { const session = this.sessions.get(sessionId); if (!session) return; - const tabName = document.querySelector(`.tab-name[data-session-id="${sessionId}"]`); + const tabName = document.querySelector(`.tab-name[data-session-id="${sessionId}"], .drawer-session-name[data-session-id="${sessionId}"]`); if (!tabName) return; const currentName = this.getSessionName(session); @@ -20349,11 +20367,13 @@ class CodemanApp { modelHtml = `${modelShort}`; } + const displayName = escapeHtml(muxSession.name || muxSession.muxName); + const sidAttr = escapeHtml(muxSession.sessionId); html += `
${statusLabel}
-
${modelHtml} ${escapeHtml(muxSession.name || muxSession.muxName)}
+
${modelHtml} ${displayName}
${tokenHtml} ${costHtml} @@ -20363,7 +20383,8 @@ class CodemanApp {
- + +
`; @@ -20372,6 +20393,60 @@ class CodemanApp { body.innerHTML = html; } + startMuxSessionRename(sessionId) { + const muxSession = this.muxSessions.find(s => s.sessionId === sessionId); + if (!muxSession) return; + const nameSpan = document.querySelector(`.mux-session-name[data-session-id="${sessionId}"]`); + if (!nameSpan) return; + const current = muxSession.name || muxSession.muxName || ''; + const originalText = nameSpan.textContent; + const input = document.createElement('input'); + input.type = 'text'; + input.value = muxSession.name || ''; + input.placeholder = current; + input.className = 'mux-rename-input'; + input.style.cssText = 'font: inherit; padding: 2px 4px; background: var(--bg-input); border: 1px solid var(--accent); border-radius: 3px; color: var(--text); outline: none;'; + nameSpan.textContent = ''; + nameSpan.appendChild(input); + input.focus(); + input.select(); + + let done = false; + const finish = async (commit) => { + if (done) return; + done = true; + const next = input.value.trim(); + if (!commit || !next || next === current) { + nameSpan.textContent = originalText; + return; + } + try { + const res = await fetch(`/api/sessions/${encodeURIComponent(sessionId)}/name`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ name: next }) + }); + if (!res.ok) throw new Error(`HTTP ${res.status}`); + const data = await res.json(); + const finalName = (data && data.name) || next; + muxSession.name = finalName; + const sess = this.sessions.get(sessionId); + if (sess) sess.name = finalName; + this.renderMuxSessions(); + if (typeof this.renderSessionTabs === 'function') this.renderSessionTabs(); + } catch (err) { + nameSpan.textContent = originalText; + this.showToast(`Rename failed: ${err.message}`, 'error'); + } + }; + + input.addEventListener('blur', () => finish(true)); + input.addEventListener('keydown', (e) => { + if (e.key === 'Enter') { e.preventDefault(); input.blur(); } + else if (e.key === 'Escape') { finish(false); } + }); + } + renderMonitorSubagents() { const body = document.getElementById('monitorSubagentsBody'); const stats = document.getElementById('monitorSubagentStats'); @@ -21820,7 +21895,14 @@ const SessionDrawer = { const name = document.createElement('span'); name.className = 'drawer-session-name'; + name.dataset.sessionId = s.id; + name.title = 'Double-click to rename'; name.textContent = app.getSessionName(s); + name.addEventListener('click', e => e.stopPropagation()); + name.addEventListener('dblclick', e => { + e.stopPropagation(); + app.startInlineRename(s.id); + }); const badge = document.createElement('span'); badge.className = 'session-mode-badge'; From 00e20fe4434fab619e0704ff556a73b7cd638cc1 Mon Sep 17 00:00:00 2001 From: DerekfromMadison Date: Sat, 18 Apr 2026 22:12:34 -0500 Subject: [PATCH 2/2] fix(frontend): widen drawer rename target to the whole row Single-click on the name span was being swallowed by the dblclick handler, breaking session selection. Move dblclick from the name span to the row, and use a drawer-local inline-rename helper that swaps the name node for an input (Enter commits, Escape cancels, blur saves). --- src/web/public/app.js | 72 ++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 67 insertions(+), 5 deletions(-) diff --git a/src/web/public/app.js b/src/web/public/app.js index 943675f7..413e346a 100644 --- a/src/web/public/app.js +++ b/src/web/public/app.js @@ -21878,6 +21878,64 @@ const SessionDrawer = { return String(str ?? ''); }, + _startInlineRename(row, nameEl, session) { + if (row.querySelector('.drawer-rename-input')) return; + const currentDisplay = app.getSessionName(session); + const input = document.createElement('input'); + input.type = 'text'; + input.value = session.name || ''; + input.placeholder = currentDisplay; + input.className = 'drawer-rename-input'; + input.style.cssText = 'flex:1;min-width:0;font:inherit;padding:2px 4px;background:var(--bg-input);border:1px solid var(--accent);border-radius:3px;color:var(--text);outline:none'; + + nameEl.replaceWith(input); + input.focus(); + input.select(); + + let done = false; + const finish = async (commit) => { + if (done) return; + done = true; + const next = input.value.trim(); + const restore = () => { + if (input.parentNode) input.replaceWith(nameEl); + }; + if (!commit || !next || next === (session.name || '')) { + restore(); + return; + } + try { + const res = await fetch(`/api/sessions/${encodeURIComponent(session.id)}/name`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ name: next }), + }); + const data = await res.json().catch(() => ({})); + if (!res.ok || data.success === false) { + app.showToast?.(`Rename failed: ${data.error || res.statusText}`, 'error'); + restore(); + return; + } + session.name = next; + nameEl.textContent = app.getSessionName(session); + restore(); + if (typeof app.renderSessionTabs === 'function') app.renderSessionTabs(); + if (typeof app.renderMuxSessions === 'function') app.renderMuxSessions(); + } catch (err) { + app.showToast?.(`Rename failed: ${err.message}`, 'error'); + restore(); + } + }; + + input.addEventListener('click', (e) => e.stopPropagation()); + input.addEventListener('dblclick', (e) => e.stopPropagation()); + input.addEventListener('blur', () => finish(true)); + input.addEventListener('keydown', (e) => { + if (e.key === 'Enter') { e.preventDefault(); finish(true); } + else if (e.key === 'Escape') { e.preventDefault(); finish(false); } + }); + }, + _renderSessionRow(s) { const isActive = s.id === app.activeSessionId; const isRunning = s.status === 'running' || s.status === 'active' || s.status === 'busy'; @@ -21898,11 +21956,6 @@ const SessionDrawer = { name.dataset.sessionId = s.id; name.title = 'Double-click to rename'; name.textContent = app.getSessionName(s); - name.addEventListener('click', e => e.stopPropagation()); - name.addEventListener('dblclick', e => { - e.stopPropagation(); - app.startInlineRename(s.id); - }); const badge = document.createElement('span'); badge.className = 'session-mode-badge'; @@ -21944,6 +21997,15 @@ const SessionDrawer = { } }); + row.addEventListener('dblclick', (e) => { + e.preventDefault(); + e.stopPropagation(); + if (typeof window.getSelection === 'function') { + try { window.getSelection().removeAllRanges(); } catch(_) {} + } + SessionDrawer._startInlineRename(row, name, s); + }); + return row; },