From 3cca5380ba907b9cf051ec385d6aabb0917df54c Mon Sep 17 00:00:00 2001 From: zhouyuan Date: Tue, 10 Mar 2026 02:23:10 +0800 Subject: [PATCH] fix: route shell sessions to correct endpoint on tab click selectSession() was always calling /interactive for restored idle sessions regardless of mode. Shell sessions now correctly call /shell. Also add loadHistorySessions and resumeHistorySession to frontend. --- src/web/public/app.js | 115 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 113 insertions(+), 2 deletions(-) diff --git a/src/web/public/app.js b/src/web/public/app.js index f075f34b..6985d9e0 100644 --- a/src/web/public/app.js +++ b/src/web/public/app.js @@ -1131,6 +1131,7 @@ class CodemanApp { if (overlay) { overlay.classList.add('visible'); this.loadTunnelStatus(); + this.loadHistorySessions(); } } @@ -1147,6 +1148,113 @@ class CodemanApp { } } + async loadHistorySessions() { + const container = document.getElementById('historySessions'); + const list = document.getElementById('historyList'); + if (!container || !list) return; + + try { + const res = await fetch('/api/history/sessions'); + const data = await res.json(); + const sessions = data.sessions || []; + if (sessions.length === 0) { + container.style.display = 'none'; + return; + } + + // Deduplicate: keep only the most recent session per workingDir + const byDir = new Map(); + for (const s of sessions) { + if (!byDir.has(s.workingDir)) byDir.set(s.workingDir, []); + byDir.get(s.workingDir).push(s); + } + + // Flatten: show up to 2 most recent per dir, max 12 total + const items = []; + for (const [, group] of byDir) { + items.push(...group.slice(0, 2)); + } + items.sort((a, b) => new Date(b.lastModified) - new Date(a.lastModified)); + const display = items.slice(0, 12); + + // Build DOM safely (no innerHTML with user data) + list.replaceChildren(); + for (const s of display) { + const size = s.sizeBytes < 1024 ? `${s.sizeBytes}B` + : s.sizeBytes < 1048576 ? `${(s.sizeBytes / 1024).toFixed(0)}K` + : `${(s.sizeBytes / 1048576).toFixed(1)}M`; + const date = new Date(s.lastModified); + const timeStr = date.toLocaleDateString('en', { month: 'short', day: 'numeric' }) + + ' ' + date.toLocaleTimeString('en', { hour: '2-digit', minute: '2-digit', hour12: false }); + const shortDir = s.workingDir.replace(/^\/home\/[^/]+\//, '~/'); + + const item = document.createElement('div'); + item.className = 'history-item'; + item.title = s.workingDir; + item.addEventListener('click', () => this.resumeHistorySession(s.sessionId, s.workingDir)); + + const dirSpan = document.createElement('span'); + dirSpan.className = 'history-item-dir'; + dirSpan.textContent = shortDir; + + const metaSpan = document.createElement('span'); + metaSpan.className = 'history-item-meta'; + metaSpan.textContent = timeStr; + + const sizeSpan = document.createElement('span'); + sizeSpan.className = 'history-item-size'; + sizeSpan.textContent = size; + + item.append(dirSpan, metaSpan, sizeSpan); + list.appendChild(item); + } + + container.style.display = ''; + } catch (err) { + console.error('[loadHistorySessions]', err); + container.style.display = 'none'; + } + } + + async resumeHistorySession(sessionId, workingDir) { + try { + this.terminal.clear(); + this.terminal.writeln(`\x1b[1;32m Resuming conversation ${sessionId.slice(0, 8)}...\x1b[0m`); + + // Generate a session name from the working dir + const dirName = workingDir.split('/').pop() || 'session'; + let startNumber = 1; + for (const [, session] of this.sessions) { + const match = session.name && session.name.match(/^w(\d+)-/); + if (match) { + const num = parseInt(match[1]); + if (num >= startNumber) startNumber = num + 1; + } + } + const name = `w${startNumber}-${dirName}`; + + // Create session with resumeSessionId + const createRes = await fetch('/api/sessions', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ workingDir, name, resumeSessionId: sessionId }) + }); + const createData = await createRes.json(); + if (!createData.success) throw new Error(createData.error); + + const newSessionId = createData.session.id; + + // Start interactive + await fetch(`/api/sessions/${newSessionId}/interactive`, { method: 'POST' }); + + this.terminal.writeln(`\x1b[90m Session ${name} ready\x1b[0m`); + await this.selectSession(newSessionId); + this.terminal.focus(); + } catch (err) { + this.terminal.writeln(`\x1b[1;31m Error: ${err.message}\x1b[0m`); + } + } + // ═══════════════════════════════════════════════════════════════ // Terminal Rendering // ═══════════════════════════════════════════════════════════════ @@ -3642,9 +3750,12 @@ class CodemanApp { // Track working directory for path normalization in Project Insights this.currentSessionWorkingDir = session?.workingDir || null; if (session && session.pid === null && session.status === 'idle') { - // This is a restored session - attach to the existing screen + // This is a restored session - attach to the existing screen/shell try { - await fetch(`/api/sessions/${sessionId}/interactive`, { method: 'POST' }); + const endpoint = session.mode === 'shell' + ? `/api/sessions/${sessionId}/shell` + : `/api/sessions/${sessionId}/interactive`; + await fetch(endpoint, { method: 'POST' }); // Update local session state session.status = 'busy'; } catch (err) {