From 94f17256754486b56b681f22d500216e68d08ca2 Mon Sep 17 00:00:00 2001 From: uroma Date: Tue, 20 Jan 2026 16:26:03 +0000 Subject: [PATCH] feat: OpenCode-style session management implementation --- public/claude-ide/chat-functions.js | 119 ++++++++ public/claude-ide/ide.css | 349 ++++++++++++++++++++++ public/claude-ide/ide.js | 442 ++++++++++++++++++++++------ server.js | 107 ++++--- services/claude-service.js | 8 +- 5 files changed, 892 insertions(+), 133 deletions(-) diff --git a/public/claude-ide/chat-functions.js b/public/claude-ide/chat-functions.js index 5d3c3422..09c42605 100644 --- a/public/claude-ide/chat-functions.js +++ b/public/claude-ide/chat-functions.js @@ -21,6 +21,22 @@ function resetChatState() { async function loadChatView() { console.log('[loadChatView] Loading chat view...'); + // Check if there's a pending session from Sessions view + if (window.pendingSessionId) { + console.log('[loadChatView] Detected pending session:', window.pendingSessionId); + + const sessionId = window.pendingSessionId; + const sessionData = window.pendingSessionData; + + // Clear pending session (consume it) + window.pendingSessionId = null; + window.pendingSessionData = null; + + // Load the session + await loadSessionIntoChat(sessionId, sessionData); + return; + } + // Preserve attached session ID if it exists (for auto-session workflow) const preservedSessionId = attachedSessionId; @@ -153,6 +169,83 @@ async function loadChatView() { } } +/** + * Load a specific session into Chat view + * Called when continuing from Sessions view + */ +async function loadSessionIntoChat(sessionId, sessionData = null) { + try { + appendSystemMessage('πŸ“‚ Loading session...'); + + // If no session data provided, fetch it + if (!sessionData) { + const res = await fetch(`/claude/api/claude/sessions/${sessionId}`); + if (!res.ok) { + throw new Error(`HTTP ${res.status}`); + } + const data = await res.json(); + sessionData = data.session; + } + + if (!sessionData) { + throw new Error('Session not found'); + } + + // Set session IDs + attachedSessionId = sessionId; + chatSessionId = sessionId; + + // Update UI + document.getElementById('current-session-id').textContent = sessionId; + + // Clear chat display + clearChatDisplay(); + + // Load session messages (both user and assistant) + if (sessionData.outputBuffer && sessionData.outputBuffer.length > 0) { + sessionData.outputBuffer.forEach(entry => { + if (entry.role) { + appendMessage(entry.role, entry.content, false); + } else { + // Legacy format - default to assistant + appendMessage('assistant', entry.content, false); + } + }); + } + + // Show success message + const isRunning = sessionData.status === 'running'; + const statusText = isRunning ? 'Active session' : 'Historical session'; + appendSystemMessage(`βœ… Loaded ${statusText} from ${new Date(sessionData.createdAt).toLocaleString()}`); + + if (!isRunning) { + appendSystemMessage('ℹ️ This is a historical session. Messages are read-only.'); + } + + // Update chat history sidebar to highlight this session + if (typeof loadChatHistory === 'function') { + loadChatHistory(); + } + + // Subscribe to session for live updates (if running) + if (isRunning) { + subscribeToSession(sessionId); + } + + // Focus input for active sessions + if (isRunning) { + setTimeout(() => { + const input = document.getElementById('chat-input'); + if (input) input.focus(); + }, 100); + } + + } catch (error) { + console.error('[loadSessionIntoChat] Error:', error); + appendSystemMessage('❌ Failed to load session: ' + error.message); + } +} + // Start New Chat async function startNewChat() { // Reset all state first @@ -675,6 +768,32 @@ function isShellCommand(message) { return commandPatterns.some(pattern => pattern.test(trimmed)); } +// Send shell command to active Claude CLI session +async function sendShellCommand(sessionId, command) { + try { + const response = await fetch('/claude/api/shell-command', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ sessionId, command }) + }); + + if (!response.ok) { + throw new Error(`HTTP ${response.status}: ${await response.text()}`); + } + + const data = await response.json(); + + if (!data.success) { + throw new Error(data.error || 'Command execution failed'); + } + + return data; + } catch (error) { + console.error('[Shell Command] Error:', error); + throw error; + } +} + // Handle command execution in Full Stack mode (via Claude CLI session's stdin) async function handleWebContainerCommand(message) { const sessionId = attachedSessionId || chatSessionId; diff --git a/public/claude-ide/ide.css b/public/claude-ide/ide.css index d771c8a7..dedcd684 100644 --- a/public/claude-ide/ide.css +++ b/public/claude-ide/ide.css @@ -1096,3 +1096,352 @@ body { font-size: 14px; line-height: 1.4; } + +/* ============================================ + SESSIONS VIEW - History Browser Styles + ============================================ */ + +/* Sessions List Container */ +.sessions-list { + overflow-y: auto; + max-height: calc(100vh - 200px); + padding: 12px; +} + +/* Session List Items */ +.session-item { + background: #1a1a1a; + border: 1px solid #333; + border-radius: 8px; + padding: 12px 16px; + margin-bottom: 8px; + cursor: pointer; + transition: all 0.2s ease; +} + +.session-item:hover { + background: #252525; + border-color: #4a9eff; + transform: translateX(4px); +} + +.session-item.historical { + border-left: 3px solid #888; +} + +.session-item.historical:hover { + border-left-color: #4a9eff; +} + +/* Session Header */ +.session-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 8px; +} + +.session-info { + display: flex; + align-items: center; + gap: 8px; +} + +.session-id { + font-family: 'SF Mono', 'Monaco', 'Inconsolata', monospace; + font-size: 13px; + font-weight: 600; + color: #e0e0e0; +} + +/* Session Status Badges */ +.session-status { + display: inline-block; + padding: 4px 8px; + border-radius: 4px; + font-size: 11px; + font-weight: 600; + text-transform: uppercase; +} + +.session-status.running { + background: rgba(81, 207, 102, 0.2); + color: #51cf66; + border: 1px solid rgba(81, 207, 102, 0.3); +} + +.session-status.stopped { + background: rgba(136, 136, 136, 0.2); + color: #888; + border: 1px solid rgba(136, 136, 136, 0.3); +} + +.session-time { + font-size: 12px; + color: #888; +} + +/* Session Meta */ +.session-meta { + font-size: 12px; + color: #aaa; +} + +.session-path { + margin-bottom: 4px; + word-break: break-all; +} + +.session-stats { + display: flex; + gap: 12px; + font-size: 11px; + color: #888; +} + +/* Empty/Error States */ +.empty-state, .error-state { + text-align: center; + padding: 40px 20px; + color: #888; +} + +.empty-icon, .error-icon { + font-size: 48px; + margin-bottom: 16px; +} + +.error-message { + font-size: 13px; + color: #ff6b6b; + margin: 8px 0 16px 0; +} + +/* ============================================ + SESSION DETAIL CARD + ============================================ */ + +.session-detail-card { + background: #1a1a1a; + border-radius: 12px; + padding: 24px; + max-width: 1200px; + margin: 0 auto; +} + +.session-detail-header { + display: flex; + justify-content: space-between; + align-items: flex-start; + margin-bottom: 24px; + padding-bottom: 20px; + border-bottom: 1px solid #333; +} + +.session-title h2 { + margin: 0 0 8px 0; + font-size: 24px; + color: #e0e0e0; +} + +.session-status-badge { + display: inline-block; + padding: 6px 12px; + border-radius: 6px; + font-size: 12px; + font-weight: 600; +} + +.session-status-badge.running { + background: rgba(81, 207, 102, 0.2); + color: #51cf66; +} + +.session-status-badge.stopped { + background: rgba(136, 136, 136, 0.2); + color: #888; +} + +.session-detail-actions { + display: flex; + gap: 8px; + align-items: center; +} + +.session-detail-actions .btn-primary { + background: linear-gradient(135deg, #4a9eff 0%, #a78bfa 100%); + border: none; + padding: 10px 20px; + font-weight: 600; +} + +.session-detail-actions .btn-secondary { + background: #2a2a2a; + border: 1px solid #444; + padding: 10px 16px; +} + +.session-detail-actions .btn-danger { + background: rgba(255, 107, 107, 0.2); + border: 1px solid rgba(255, 107, 107, 0.3); + color: #ff6b6b; + padding: 10px 16px; +} + +.session-detail-actions .btn-danger:hover { + background: rgba(255, 107, 107, 0.3); +} + +.session-detail-meta { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 12px; + margin-bottom: 24px; + padding: 16px; + background: #0d0d0d; + border-radius: 8px; +} + +.meta-row { + display: flex; + gap: 8px; + font-size: 13px; +} + +.meta-label { + color: #888; + font-weight: 600; + min-width: 120px; +} + +.meta-value { + color: #e0e0e0; + word-break: break-all; +} + +.session-context { + margin-bottom: 24px; + padding: 16px; + background: #0d0d0d; + border-radius: 8px; +} + +.session-context h3 { + margin: 0 0 12px 0; + font-size: 14px; + color: #e0e0e0; +} + +.context-bar { + width: 100%; + height: 8px; + background: #333; + border-radius: 4px; + overflow: hidden; + margin-bottom: 8px; +} + +.context-fill { + height: 100%; + background: linear-gradient(90deg, #4a9eff 0%, #a78bfa 100%); + transition: width 0.3s ease; +} + +.context-stats { + display: flex; + justify-content: space-between; + font-size: 12px; + color: #888; +} + +.session-output-preview h3 { + margin: 0 0 16px 0; + font-size: 16px; + color: #e0e0e0; +} + +.output-scroll-area { + max-height: 400px; + overflow-y: auto; + background: #0d0d0d; + border: 1px solid #333; + border-radius: 8px; + padding: 12px; +} + +.output-entry { + margin-bottom: 12px; + padding-bottom: 12px; + border-bottom: 1px solid #252525; +} + +.output-entry:last-child { + border-bottom: none; +} + +.output-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 6px; +} + +.output-type { + display: inline-block; + padding: 2px 8px; + background: #252525; + color: #888; + font-size: 10px; + font-weight: 600; + text-transform: uppercase; + border-radius: 3px; +} + +.output-time { + font-size: 11px; + color: #666; +} + +.output-content { + font-family: 'SF Mono', 'Monaco', 'Inconsolata', monospace; + font-size: 12px; + color: #aaa; + white-space: pre-wrap; + word-break: break-all; + line-height: 1.5; +} + +.no-output { + text-align: center; + padding: 40px; + color: #666; + font-style: italic; +} + +.output-truncated { + text-align: center; + padding: 12px; + color: #4a9eff; + font-size: 13px; +} + +/* Responsive */ +@media (max-width: 768px) { + .session-detail-header { + flex-direction: column; + gap: 16px; + } + + .session-detail-actions { + width: 100%; + flex-direction: column; + } + + .session-detail-actions button { + width: 100%; + } + + .session-detail-meta { + grid-template-columns: 1fr; + } +} diff --git a/public/claude-ide/ide.js b/public/claude-ide/ide.js index 7bda5e00..b94bb831 100644 --- a/public/claude-ide/ide.js +++ b/public/claude-ide/ide.js @@ -346,126 +346,390 @@ function refreshSessions() { // Sessions async function loadSessions() { + const sessionsListEl = document.getElementById('sessions-list'); + try { - const res = await fetch('/claude/api/claude/sessions'); + // Get current project from URL + const urlParams = new URLSearchParams(window.location.search); + const projectPath = urlParams.get('project'); + + // Build API URL with project filter + let apiUrl = '/claude/api/claude/sessions'; + if (projectPath) { + apiUrl += `?project=${encodeURIComponent(projectPath)}`; + console.log('[Sessions] Loading sessions for project:', projectPath); + } + + // Show loading state + sessionsListEl.innerHTML = '
Loading sessions...
'; + + const res = await fetch(apiUrl); + + // Handle HTTP errors + if (!res.ok) { + if (res.status === 401) { + sessionsListEl.innerHTML = ` +
+

⚠️ Session expired

+ +
+ `; + return; + } + throw new Error(`HTTP ${res.status}: ${res.statusText}`); + } + const data = await res.json(); - const sessionsListEl = document.getElementById('sessions-list'); + // Handle API errors + if (data.error) { + throw new Error(data.error); + } + const allSessions = [ - ...(data.active || []), - ...(data.historical || []) + ...(data.active || []).map(s => ({...s, type: 'active'})), + ...(data.historical || []).map(s => ({...s, type: 'historical'})) ]; - if (allSessions.length > 0) { - sessionsListEl.innerHTML = allSessions.map(session => ` -
+ // Sort by last activity (newest first) + allSessions.sort((a, b) => { + const dateA = new Date(a.lastActivity || a.createdAt || a.created_at); + const dateB = new Date(b.lastActivity || b.createdAt || b.created_at); + return dateB - dateA; + }); + + // Empty state + if (allSessions.length === 0) { + const projectName = projectPath ? projectPath.split('/').pop() : 'this project'; + sessionsListEl.innerHTML = ` +
+
πŸ“‚
+

No sessions found for ${escapeHtml(projectName)}

+ +
+ `; + return; + } + + // Render session list + sessionsListEl.innerHTML = allSessions.map(session => { + const isRunning = session.status === 'running' && session.type === 'active'; + const relativeTime = getRelativeTime(session); + const messageCount = session.messageCount || session.metadata?.messageCount || 0; + + return ` +
- ${session.id.substring(0, 20)}... - ${session.status} +
+ ${session.id.substring(0, 12)}... + + ${isRunning ? '🟒 Running' : '⏸️ ' + (session.type === 'historical' ? 'Historical' : 'Stopped')} + +
+
${relativeTime}
- ${session.workingDir}
- ${new Date(session.createdAt).toLocaleString()} +
πŸ“ ${escapeHtml(session.workingDir)}
+
+ πŸ’¬ ${messageCount} messages +
- `).join(''); - } else { - sessionsListEl.innerHTML = '

No sessions

'; - } + `; + }).join(''); + } catch (error) { - console.error('Error loading sessions:', error); + console.error('[loadSessions] Error:', error); + sessionsListEl.innerHTML = ` +
+
⚠️
+

Failed to load sessions

+

${escapeHtml(error.message)}

+ +
+ `; } } -async function viewSession(sessionId) { +function getRelativeTime(session) { + const date = new Date(session.lastActivity || session.createdAt || session.created_at); + const now = new Date(); + const diffMins = Math.floor((now - date) / 60000); + + if (diffMins < 1) return 'Just now'; + if (diffMins < 60) return `${diffMins}m ago`; + if (diffMins < 1440) return `${Math.floor(diffMins/60)}h ago`; + return `${Math.floor(diffMins/1440)}d ago`; +} + +function escapeHtml(text) { + const div = document.createElement('div'); + div.textContent = text; + return div.innerHTML; +} + +async function viewSessionDetails(sessionId) { + const detailEl = document.getElementById('session-detail'); + + try { + // Show loading state + detailEl.innerHTML = '
Loading session details...
'; + + const res = await fetch(`/claude/api/claude/sessions/${sessionId}`); + + // Handle 404 - session not found + if (res.status === 404) { + detailEl.innerHTML = ` +
+
πŸ”
+

Session Not Found

+

The session ${escapeHtml(sessionId)} could not be found.

+ +
+ `; + return; + } + + if (!res.ok) { + throw new Error(`HTTP ${res.status}: ${res.statusText}`); + } + + const data = await res.json(); + + if (data.error) { + throw new Error(data.error); + } + + if (!data.session) { + throw new Error('No session data in response'); + } + + const session = data.session; + const isRunning = session.status === 'running' && session.pid; + const messageCount = session.outputBuffer?.length || 0; + + // Render session detail card + detailEl.innerHTML = ` +
+
+
+

Session ${session.id.substring(0, 12)}...

+ + ${isRunning ? '🟒 Running' : '⏸️ Stopped'} + +
+
+ + + ${isRunning ? ` + + ` : ''} +
+
+ +
+
+ Working Directory: + ${escapeHtml(session.workingDir)} +
+
+ Created: + ${new Date(session.createdAt).toLocaleString()} +
+
+ Last Activity: + ${new Date(session.lastActivity).toLocaleString()} +
+
+ Messages: + ${messageCount} +
+ ${session.pid ? ` +
+ PID: + ${session.pid} +
+ ` : ''} +
+ +
+

Token Usage

+
+
+
+
+ ${(session.context?.totalTokens || 0).toLocaleString()} / ${(session.context?.maxTokens || 200000).toLocaleString()} tokens + ${Math.round((session.context?.totalTokens || 0) / (session.context?.maxTokens || 200000) * 100)}% used +
+
+ +
+

Session Output (${messageCount} entries)

+
+ ${session.outputBuffer?.slice(0, 50).map(entry => ` +
+
+ ${entry.type} + ${new Date(entry.timestamp).toLocaleTimeString()} +
+
${escapeHtml(entry.content.substring(0, 500))}${entry.content.length > 500 ? '...' : ''}
+
+ `).join('') || '

No output yet

'} + ${session.outputBuffer?.length > 50 ? `

...and ${session.outputBuffer.length - 50} more entries

` : ''} +
+
+
+ `; + + currentSession = session; + + } catch (error) { + console.error('[viewSessionDetails] Error:', error); + detailEl.innerHTML = ` +
+
⚠️
+

Failed to Load Session

+

${escapeHtml(error.message)}

+ +
+ `; + } +} + +async function continueSessionInChat(sessionId) { + console.log('[Sessions] Continuing session in Chat:', sessionId); + + try { + showLoadingOverlay('Loading session...'); + + const res = await fetch(`/claude/api/claude/sessions/${sessionId}`); + if (!res.ok) { + throw new Error(`HTTP ${res.status}`); + } + + const data = await res.json(); + if (!data.session) { + throw new Error('Session not found'); + } + + const session = data.session; + + // Check if session is runnable + if (session.status === 'terminated' || session.status === 'stopped') { + hideLoadingOverlay(); + + if (confirm('This session has ended. Do you want to create a new session with the same working directory?')) { + await duplicateSession(sessionId); + } + return; + } + + // Store pending session and switch views + window.pendingSessionId = sessionId; + window.pendingSessionData = session; + + switchView('chat'); + + } catch (error) { + console.error('[continueSessionInChat] Error:', error); + hideLoadingOverlay(); + showToast('❌ Failed to load session: ' + error.message, 'error'); + } +} + +async function duplicateSession(sessionId) { try { const res = await fetch(`/claude/api/claude/sessions/${sessionId}`); const data = await res.json(); - currentSession = data.session; + if (!data.session) { + throw new Error('Session not found'); + } - const detailEl = document.getElementById('session-detail'); - detailEl.innerHTML = ` -
-

${data.session.id}

-

Status: ${data.session.status}

-

PID: ${data.session.pid || 'N/A'}

-

Working Directory: ${data.session.workingDir}

-

Created: ${new Date(data.session.createdAt).toLocaleString()}

-
+ const workingDir = data.session.workingDir; + const projectName = workingDir.split('/').pop(); -

Context Usage

-
-
-
-
- ${data.session.context.totalTokens.toLocaleString()} tokens - ${Math.round(data.session.context.totalTokens / data.session.context.maxTokens * 100)}% used -
+ showLoadingOverlay('Duplicating session...'); -

Session Output

-
- ${data.session.outputBuffer.map(entry => ` -
${escapeHtml(entry.content)}
- `).join('')} -
- - ${data.session.status === 'running' ? ` -
- - -
- ` : ''} - `; - - // Switch to sessions view - switchView('sessions'); - } catch (error) { - console.error('Error viewing session:', error); - alert('Failed to load session'); - } -} - -function handleCommandKeypress(event) { - if (event.key === 'Enter') { - sendCommand(); - } -} - -async function sendCommand() { - const input = document.getElementById('command-input'); - const command = input.value.trim(); - - if (!command || !currentSession) return; - - try { - await fetch(`/claude/api/claude/sessions/${currentSession.id}/command`, { + const createRes = await fetch('/claude/api/claude/sessions', { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ command }) + body: JSON.stringify({ + workingDir, + metadata: { + type: 'chat', + source: 'web-ide', + project: projectName, + duplicatedFrom: sessionId + } + }) }); - input.value = ''; + if (!createRes.ok) { + throw new Error(`HTTP ${createRes.status}`); + } + + const createData = await createRes.json(); + + hideLoadingOverlay(); + showToast('βœ… Session duplicated!', 'success'); + + loadSessions(); + + setTimeout(() => { + if (confirm('Start chatting in the duplicated session?')) { + continueSessionInChat(createData.session.id); + } + }, 500); - // Append command to output - appendOutput({ - type: 'command', - content: `$ ${command}\n` - }); } catch (error) { - console.error('Error sending command:', error); - alert('Failed to send command'); + console.error('[duplicateSession] Error:', error); + hideLoadingOverlay(); + showToast('Failed to duplicate session: ' + error.message, 'error'); } } -function appendOutput(data) { - const outputEl = document.getElementById('session-output'); - if (outputEl) { - const line = document.createElement('div'); - line.className = `output-line ${data.type}`; - line.textContent = data.content; - outputEl.appendChild(line); - outputEl.scrollTop = outputEl.scrollHeight; +async function terminateSession(sessionId) { + if (!confirm('Are you sure you want to terminate this session?')) { + return; + } + + try { + showLoadingOverlay('Terminating session...'); + + const res = await fetch(`/claude/api/claude/sessions/${sessionId}`, { + method: 'DELETE' + }); + + if (!res.ok) { + throw new Error(`HTTP ${res.status}`); + } + + hideLoadingOverlay(); + showToast('βœ… Session terminated', 'success'); + + loadSessions(); + + if (currentSession && currentSession.id === sessionId) { + document.getElementById('session-detail').innerHTML = ` +
+

Session Terminated

+

Select another session from the sidebar

+
+ `; + currentSession = null; + } + + } catch (error) { + console.error('[terminateSession] Error:', error); + hideLoadingOverlay(); + showToast('Failed to terminate session: ' + error.message, 'error'); } } diff --git a/server.js b/server.js index 7dcd2a7d..7e1db2fe 100644 --- a/server.js +++ b/server.js @@ -500,17 +500,38 @@ app.get('/claude/api/recent', requireAuth, (req, res) => { // ============================================ // Session Management +// GET /claude/api/claude/sessions?project=/encoded/path app.get('/claude/api/claude/sessions', requireAuth, (req, res) => { try { - const activeSessions = claudeService.listSessions(); - const historicalSessions = claudeService.loadHistoricalSessions(); + const { project } = req.query; + + let activeSessions = claudeService.listSessions(); + let historicalSessions = claudeService.loadHistoricalSessions(); + + // PROJECT FILTERING + if (project) { + const projectPath = decodeURIComponent(project); + console.log('[SESSIONS] Filtering by project path:', projectPath); + + activeSessions = activeSessions.filter(s => { + const sessionPath = s.workingDir || ''; + return sessionPath.startsWith(projectPath) || sessionPath === projectPath; + }); + + historicalSessions = historicalSessions.filter(s => { + const sessionPath = s.workingDir || ''; + return sessionPath.startsWith(projectPath) || sessionPath === projectPath; + }); + + console.log('[SESSIONS] Filtered to', activeSessions.length, 'active,', historicalSessions.length, 'historical'); + } res.json({ active: activeSessions, historical: historicalSessions }); } catch (error) { - console.error('Error listing sessions:', error); + console.error('[SESSIONS] Error:', error); res.status(500).json({ error: 'Failed to list sessions' }); } }); @@ -1608,46 +1629,46 @@ app.post('/claude/api/terminals/:id/input', requireAuth, (req, res) => { res.status(500).json({ error: 'Failed to send input' }); } }); - -// Get terminal output via HTTP polling (bypasses WebSocket issue) -app.get('/claude/api/terminals/:id/output', requireAuth, (req, res) => { - try { - const sinceIndex = parseInt(req.query.since) || 0; - const result = terminalService.getTerminalOutput(req.params.id, sinceIndex); - - if (result.success) { - res.json(result); - } else { - res.status(404).json({ error: result.error }); - } - } catch (error) { - console.error('Error getting terminal output:', error); - res.status(500).json({ error: 'Failed to get output' }); - } -}); - -// Resize terminal via HTTP -app.post('/claude/api/terminals/:id/resize', requireAuth, (req, res) => { - try { - const { cols, rows } = req.body; - - if (!cols || !rows) { - res.status(400).json({ error: 'Missing cols or rows parameter' }); - return; - } - - const result = terminalService.resizeTerminal(req.params.id, cols, rows); - - if (result.success) { - res.json({ success: true }); - } else { - res.status(404).json({ error: result.error }); - } - } catch (error) { - console.error('Error resizing terminal:', error); - res.status(500).json({ error: 'Failed to resize terminal' }); - } -}); + +// Get terminal output via HTTP polling (bypasses WebSocket issue) +app.get('/claude/api/terminals/:id/output', requireAuth, (req, res) => { + try { + const sinceIndex = parseInt(req.query.since) || 0; + const result = terminalService.getTerminalOutput(req.params.id, sinceIndex); + + if (result.success) { + res.json(result); + } else { + res.status(404).json({ error: result.error }); + } + } catch (error) { + console.error('Error getting terminal output:', error); + res.status(500).json({ error: 'Failed to get output' }); + } +}); + +// Resize terminal via HTTP +app.post('/claude/api/terminals/:id/resize', requireAuth, (req, res) => { + try { + const { cols, rows } = req.body; + + if (!cols || !rows) { + res.status(400).json({ error: 'Missing cols or rows parameter' }); + return; + } + + const result = terminalService.resizeTerminal(req.params.id, cols, rows); + + if (result.success) { + res.json({ success: true }); + } else { + res.status(404).json({ error: result.error }); + } + } catch (error) { + console.error('Error resizing terminal:', error); + res.status(500).json({ error: 'Failed to resize terminal' }); + } +}); // Get recent directories for terminal picker app.get('/claude/api/files/recent-dirs', requireAuth, (req, res) => { diff --git a/services/claude-service.js b/services/claude-service.js index daf3999d..b81811b7 100644 --- a/services/claude-service.js +++ b/services/claude-service.js @@ -446,11 +446,17 @@ class ClaudeCodeService extends EventEmitter { listSessions() { return Array.from(this.sessions.values()).map(session => { const metadata = this.calculateSessionMetadata(session); + + // FIX: Only mark as running if process is actually alive + const isRunning = session.status === 'running' && + session.process && + !session.process.killed; + return { id: session.id, pid: session.pid, workingDir: session.workingDir, - status: session.status, + status: isRunning ? 'running' : 'stopped', createdAt: session.createdAt, lastActivity: session.lastActivity, metadata: session.metadata,