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 = `
+
+ // 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 `
+
- `).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 = `
+
+
+
+
+
+
+
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)
+
+
+
+ `;
+
+ 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 = `
-
+ 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,