diff --git a/public/claude-ide/chat-functions.js b/public/claude-ide/chat-functions.js
index 382ace2f..5d3c3422 100644
--- a/public/claude-ide/chat-functions.js
+++ b/public/claude-ide/chat-functions.js
@@ -5,6 +5,8 @@
let chatSessionId = null;
let chatMessages = [];
let attachedSessionId = null;
+let isGenerating = false; // Track if Claude is currently generating
+let modeSuggestionTimeout = null; // Track mode suggestion auto-hide timer
// Reset all chat state
function resetChatState() {
@@ -19,9 +21,19 @@ function resetChatState() {
async function loadChatView() {
console.log('[loadChatView] Loading chat view...');
+ // Preserve attached session ID if it exists (for auto-session workflow)
+ const preservedSessionId = attachedSessionId;
+
// Reset state on view load to prevent stale session references
resetChatState();
+ // Restore attached session if it was set (e.g., from auto-session initialization)
+ if (preservedSessionId) {
+ console.log('[loadChatView] Restoring attached session:', preservedSessionId);
+ attachedSessionId = preservedSessionId;
+ chatSessionId = preservedSessionId;
+ }
+
// Load chat sessions
try {
console.log('[loadChatView] Fetching sessions...');
@@ -43,7 +55,44 @@ async function loadChatView() {
// ONLY show active sessions - no historical sessions in chat view
// Historical sessions are read-only and can't receive new messages
- const activeSessions = (data.active || []).filter(s => s.status === 'running');
+ let activeSessions = (data.active || []).filter(s => s.status === 'running');
+
+ // Filter by current project if in project context
+ const currentProjectDir = window.currentProjectDir;
+
+ if (currentProjectDir) {
+ console.log('[loadChatView] Filtering sessions for project path:', currentProjectDir);
+
+ // Filter sessions that belong to this project
+ activeSessions = activeSessions.filter(session => {
+ // Check if session's working directory is within current project directory
+ const sessionWorkingDir = session.workingDir || '';
+
+ // Direct match: session working dir starts with project dir
+ const directMatch = sessionWorkingDir.startsWith(currentProjectDir);
+
+ // Metadata match: session metadata project matches
+ const metadataMatch = session.metadata?.project === currentProjectDir;
+
+ // For project sessions, also check if project path is in working dir
+ const pathMatch = sessionWorkingDir.includes(currentProjectDir) || currentProjectDir.includes(sessionWorkingDir);
+
+ const isMatch = directMatch || metadataMatch || pathMatch;
+
+ console.log(`[loadChatView] Session ${session.id}:`, {
+ workingDir: sessionWorkingDir,
+ projectDir: currentProjectDir,
+ directMatch,
+ metadataMatch,
+ pathMatch,
+ isMatch
+ });
+
+ return isMatch;
+ });
+
+ console.log('[loadChatView] Project sessions found:', activeSessions.length, 'out of', (data.active || []).length);
+ }
console.log('Active sessions (can receive messages):', activeSessions.length);
@@ -67,12 +116,26 @@ async function loadChatView() {
`;
}).join('');
} else {
- sessionsListEl.innerHTML = `
-
-
No active sessions
-
-
- `;
+ // Zero-friction entry: Auto-create session in project context
+ if (currentProjectName && window.currentProjectDir) {
+ console.log('[loadChatView] No sessions for project, auto-creating...');
+ sessionsListEl.innerHTML = `
+
+
Creating session for project: ${currentProjectName}
+
+
+ `;
+ // Auto-create session
+ startNewChat();
+ } else {
+ const emptyMessage = `No active sessions
`;
+ sessionsListEl.innerHTML = `
+
+ ${emptyMessage}
+
+
+ `;
+ }
}
console.log('[loadChatView] Chat view loaded successfully');
@@ -100,6 +163,17 @@ async function startNewChat() {
appendSystemMessage('Creating new chat session...');
+ // Determine working directory based on context
+ let workingDir = '/home/uroma/obsidian-vault'; // default
+ let projectName = null;
+
+ // If we're in a project context, use the project directory
+ if (window.currentProjectDir) {
+ workingDir = window.currentProjectDir;
+ projectName = window.currentProjectDir.split('/').pop();
+ console.log('[startNewChat] Creating session for project:', projectName, 'at', workingDir);
+ }
+
// Create new session
try {
console.log('Creating new Claude Code session...');
@@ -107,8 +181,13 @@ async function startNewChat() {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
- workingDir: '/home/uroma/obsidian-vault',
- metadata: { type: 'chat', source: 'web-ide' }
+ workingDir: workingDir,
+ metadata: {
+ type: 'chat',
+ source: 'web-ide',
+ project: projectName,
+ projectPath: window.currentProjectDir || null
+ }
})
});
@@ -123,7 +202,7 @@ async function startNewChat() {
// Update UI
document.getElementById('current-session-id').textContent = data.session.id;
- document.getElementById('chat-title').textContent = 'New Chat';
+ document.getElementById('chat-title').textContent = projectName ? `Project: ${projectName}` : 'New Chat';
// Subscribe to session via WebSocket
subscribeToSession(data.session.id);
@@ -131,15 +210,25 @@ async function startNewChat() {
// Reload sessions list
loadChatView();
- // Show success message
- appendSystemMessage('✅ New chat session started! You can now chat with Claude Code.');
+ // Hide the creation success message after a short delay
+ setTimeout(() => {
+ const loadingMsg = document.querySelector('.chat-system');
+ if (loadingMsg && loadingMsg.textContent.includes('Creating new chat session')) {
+ loadingMsg.remove();
+ }
+ }, 2000);
+
+ // Focus on input
+ const input = document.getElementById('chat-input');
+ if (input) {
+ input.focus();
+ }
} else {
- console.error('Session creation failed:', data);
- appendSystemMessage('❌ Failed to create session: ' + (data.error || 'Unknown error'));
+ throw new Error(data.error || 'Failed to create session');
}
} catch (error) {
- console.error('Error starting new chat:', error);
- appendSystemMessage('❌ Failed to start new chat session: ' + error.message);
+ console.error('Error creating new chat session:', error);
+ appendSystemMessage('❌ Failed to create new chat session: ' + error.message);
}
}
@@ -151,6 +240,11 @@ function attachToSession(sessionId) {
// Update UI
document.getElementById('current-session-id').textContent = sessionId;
+ // Update context panel with session
+ if (typeof contextPanel !== 'undefined' && contextPanel) {
+ contextPanel.setSession(sessionId, 'active');
+ }
+
// Load session messages
loadSessionMessages(sessionId);
@@ -188,9 +282,15 @@ async function loadSessionMessages(sessionId) {
if (data.session) {
clearChatDisplay();
- // Add existing messages from output buffer
+ // Add existing messages from output buffer - restore both user and assistant messages
data.session.outputBuffer.forEach(entry => {
- appendMessage('assistant', entry.content, false);
+ // Check if entry has role information (newer format)
+ if (entry.role) {
+ appendMessage(entry.role, entry.content, false);
+ } else {
+ // Legacy format - assume assistant if no role specified
+ appendMessage('assistant', entry.content, false);
+ }
});
}
} catch (error) {
@@ -198,26 +298,112 @@ async function loadSessionMessages(sessionId) {
}
}
+// Handle Chat Input (modern input handler)
+function handleChatInput(event) {
+ const input = event.target;
+ const wrapper = document.getElementById('chat-input-wrapper');
+ const charCountBadge = document.getElementById('char-count-badge');
+
+ // Update character count badge
+ const charCount = input.value.length;
+ charCountBadge.textContent = charCount + ' chars';
+
+ // Toggle typing state for badge visibility
+ if (charCount > 0) {
+ wrapper.classList.add('typing');
+ } else {
+ wrapper.classList.remove('typing');
+ }
+
+ // Auto-resize textarea
+ input.style.height = 'auto';
+ input.style.height = Math.min(input.scrollHeight, 200) + 'px';
+
+ // Check for terminal command suggestion in Auto/Native modes
+ if ((currentChatMode === 'auto' || currentChatMode === 'native') && charCount > 0) {
+ checkForTerminalCommand(input.value);
+ } else {
+ hideModeSuggestion();
+ }
+}
+
// Handle Chat Key Press
function handleChatKeypress(event) {
const input = document.getElementById('chat-input');
- // Update character count
- const charCount = input.value.length;
- document.getElementById('char-count').textContent = charCount + ' characters';
-
// Send on Enter (but allow Shift+Enter for new line)
if (event.key === 'Enter' && !event.shiftKey) {
event.preventDefault();
sendChatMessage();
}
-
- // Auto-resize textarea
- input.style.height = 'auto';
- input.style.height = Math.min(input.scrollHeight, 150) + 'px';
}
-// Send Chat Message
+// Check for Terminal Command (Task 4: Auto-Suggest Terminal Mode)
+function checkForTerminalCommand(message) {
+ const banner = document.getElementById('mode-suggestion-banner');
+
+ // Don't show suggestion if already in webcontainer mode
+ if (currentChatMode === 'webcontainer') {
+ hideModeSuggestion();
+ return;
+ }
+
+ // Check if message looks like a shell command
+ if (isShellCommand(message)) {
+ showModeSuggestion();
+ } else {
+ hideModeSuggestion();
+ }
+}
+
+// Show Mode Suggestion Banner
+function showModeSuggestion() {
+ const banner = document.getElementById('mode-suggestion-banner');
+ if (banner && banner.style.display === 'none') {
+ banner.style.display = 'flex';
+
+ // Auto-hide after 10 seconds if no action
+ if (modeSuggestionTimeout) {
+ clearTimeout(modeSuggestionTimeout);
+ }
+ modeSuggestionTimeout = setTimeout(() => {
+ hideModeSuggestion();
+ }, 10000);
+ }
+}
+
+// Hide Mode Suggestion Banner
+function hideModeSuggestion() {
+ const banner = document.getElementById('mode-suggestion-banner');
+ if (banner && banner.style.display !== 'none') {
+ banner.classList.add('fade-out');
+ setTimeout(() => {
+ banner.style.display = 'none';
+ banner.classList.remove('fade-out');
+ }, 300);
+
+ if (modeSuggestionTimeout) {
+ clearTimeout(modeSuggestionTimeout);
+ modeSuggestionTimeout = null;
+ }
+ }
+}
+
+// Switch to Terminal Mode
+function switchToTerminalMode() {
+ hideModeSuggestion();
+ setChatMode('webcontainer');
+ appendSystemMessage('✅ Switched to Terminal mode. Your commands will execute in a persistent terminal session.');
+}
+
+// Dismiss Mode Suggestion and Send Anyway
+function dismissModeSuggestion() {
+ hideModeSuggestion();
+ // Proceed with sending the message in current mode
+ sendChatMessage();
+}
+
+// Send Chat Message (Enhanced with smart input parsing)
async function sendChatMessage() {
const input = document.getElementById('chat-input');
const message = input.value.trim();
@@ -229,12 +415,49 @@ async function sendChatMessage() {
return;
}
+ // Hide mode suggestion banner
+ hideModeSuggestion();
+
+ // Parse smart input (file references, commands)
+ let parsed = null;
+ if (typeof smartInput !== 'undefined' && smartInput) {
+ try {
+ parsed = smartInput.parser.parse(message);
+
+ // Update context panel with referenced files
+ if (parsed.files.length > 0 && typeof contextPanel !== 'undefined' && contextPanel) {
+ parsed.files.forEach(filePath => {
+ const fileName = filePath.split('/').pop();
+ const ext = fileName.split('.').pop();
+ const icon = getFileIcon(ext);
+
+ contextPanel.addActiveFile(filePath, fileName, icon);
+ });
+ console.log('Added', parsed.files.length, 'referenced files to context panel');
+ }
+
+ console.log('Parsed input:', parsed);
+ } catch (error) {
+ console.error('Error parsing smart input:', error);
+ }
+ }
+
+ // Use selected mode from buttons, or fall back to parsed mode
+ const selectedMode = currentChatMode || 'auto';
+
// Add user message to chat
appendMessage('user', message);
clearInput();
- // Show streaming indicator
+ // Show streaming indicator and update button state
showStreamingIndicator();
+ setGeneratingState(true);
+
+ // Handle WebContainer mode separately
+ if (selectedMode === 'webcontainer') {
+ await handleWebContainerCommand(message);
+ return;
+ }
try {
// Check WebSocket state
@@ -242,6 +465,7 @@ async function sendChatMessage() {
console.error('WebSocket is null/undefined');
appendSystemMessage('WebSocket not initialized. Please refresh the page.');
hideStreamingIndicator();
+ setGeneratingState(false);
return;
}
@@ -254,6 +478,7 @@ async function sendChatMessage() {
console.error('WebSocket not in OPEN state:', stateName);
appendSystemMessage(`WebSocket not ready (state: ${stateName}). Retrying...`);
hideStreamingIndicator();
+ setGeneratingState(false);
// Trigger reconnection if closed
if (state === WebSocket.CLOSED) {
@@ -265,21 +490,287 @@ async function sendChatMessage() {
return;
}
- // Send command via WebSocket
- window.ws.send(JSON.stringify({
+ // Send command via WebSocket with parsed metadata
+ const payload = {
type: 'command',
sessionId: attachedSessionId,
command: message
- }));
+ };
+
+ // Add metadata if available (files, commands, mode)
+ const hasFiles = parsed && parsed.files.length > 0;
+ const hasCommands = parsed && parsed.commands.length > 0;
+ const modeNotAuto = selectedMode !== 'auto';
+
+ if (hasFiles || hasCommands || modeNotAuto) {
+ payload.metadata = {
+ files: parsed ? parsed.files : [],
+ commands: parsed ? parsed.commands : [],
+ mode: selectedMode,
+ hasFileReferences: parsed ? parsed.hasFileReferences : false
+ };
+ console.log('Sending with metadata:', payload.metadata);
+ }
+
+ window.ws.send(JSON.stringify(payload));
console.log('Sent command via WebSocket:', message.substring(0, 50));
} catch (error) {
console.error('Error sending message:', error);
hideStreamingIndicator();
+ setGeneratingState(false);
appendSystemMessage('Failed to send message: ' + error.message);
}
}
-// Append Message to Chat
+// Task 1: Set Generating State (show/hide stop button)
+function setGeneratingState(generating) {
+ isGenerating = generating;
+
+ const sendButton = document.getElementById('send-button');
+ const stopButton = document.getElementById('stop-button');
+
+ if (generating) {
+ // Show stop button, hide send button
+ sendButton.classList.add('hidden');
+ stopButton.classList.remove('hidden');
+ } else {
+ // Show send button, hide stop button
+ sendButton.classList.remove('hidden');
+ stopButton.classList.add('hidden');
+ }
+}
+
+// Task 1: Stop Generation
+function stopGeneration() {
+ console.log('Stopping generation...');
+
+ if (!window.ws || window.ws.readyState !== WebSocket.OPEN) {
+ appendSystemMessage('⚠️ Cannot stop: WebSocket not connected');
+ return;
+ }
+
+ // Send stop signal via WebSocket
+ window.ws.send(JSON.stringify({
+ type: 'stop',
+ sessionId: attachedSessionId
+ }));
+
+ appendSystemMessage('⏸️ Stopping generation...');
+
+ // Update UI state
+ hideStreamingIndicator();
+ setGeneratingState(false);
+}
+
+// Helper function to get file icon for context panel
+function getFileIcon(ext) {
+ const icons = {
+ 'js': '📜', 'mjs': '📜', 'ts': '📘', 'tsx': '⚛️', 'jsx': '⚛️',
+ 'html': '🌐', 'htm': '🌐', 'css': '🎨', 'scss': '🎨',
+ 'py': '🐍', 'rb': '💎', 'php': '🐘',
+ 'json': '📋', 'xml': '📄',
+ 'md': '📝', 'txt': '📄',
+ 'png': '🖼️', 'jpg': '🖼️', 'jpeg': '🖼️', 'gif': '🖼️', 'svg': '🖼️',
+ 'sh': '🖥️', 'bash': '🖥️', 'zsh': '🖥️',
+ 'yml': '⚙️', 'yaml': '⚙️', 'toml': '⚙️'
+ };
+ return icons[ext] || '📄';
+}
+
+// Chat Mode Management
+let currentChatMode = 'auto';
+
+function setChatMode(mode) {
+ currentChatMode = mode;
+
+ // Update button states
+ document.querySelectorAll('.mode-btn').forEach(btn => {
+ btn.classList.remove('active');
+ if (btn.dataset.mode === mode) {
+ btn.classList.add('active');
+ }
+ });
+
+ // Update context panel with mode
+ if (typeof contextPanel !== 'undefined' && contextPanel) {
+ contextPanel.setMode(mode);
+ }
+
+ // Initialize WebContainer if switching to webcontainer mode
+ if (mode === 'webcontainer') {
+ initializeWebContainer();
+ }
+
+ // Show mode change message
+ const modeNames = {
+ 'auto': '🤖 Auto (best mode will be detected automatically)',
+ 'native': '💻 Native (commands execute directly on your system)',
+ 'webcontainer': '💻 Terminal (executes commands in persistent terminal session)'
+ };
+
+ appendSystemMessage(`Execution mode changed to: ${modeNames[mode]}`);
+ console.log('Chat mode set to:', mode);
+}
+
+// Initialize WebContainer for current session
+async function initializeWebContainer() {
+ try {
+ if (typeof webContainerManager === 'undefined') {
+ appendSystemMessage('⚠️ WebContainer Manager not loaded. Refresh the page.');
+ return;
+ }
+
+ // Check if already initialized for current session
+ const sessionId = attachedSessionId || chatSessionId;
+ if (!sessionId) {
+ appendSystemMessage('⚠️ Please start a chat session first.');
+ return;
+ }
+
+ const status = webContainerManager.getStatus();
+ if (status.initialized && status.currentSession === sessionId) {
+ appendSystemMessage('✅ WebContainer already initialized for this session');
+ return;
+ }
+
+ appendSystemMessage('🔄 Initializing WebContainer environment...');
+
+ await webContainerManager.initialize(sessionId);
+
+ appendSystemMessage('✅ WebContainer ready! Commands will execute in browser sandbox.');
+ } catch (error) {
+ console.error('Failed to initialize WebContainer:', error);
+ appendSystemMessage('❌ Failed to initialize WebContainer: ' + error.message);
+ }
+}
+
+// Detect if message is a shell command
+function isShellCommand(message) {
+ const trimmed = message.trim().toLowerCase();
+
+ // Check for common shell command patterns
+ const commandPatterns = [
+ // Shell built-ins
+ /^(cd|ls|pwd|echo|cat|grep|find|rm|cp|mv|mkdir|rmdir|touch|chmod|chown|ln|head|tail|less|more|sort|uniq|wc|tar|zip|unzip|gzip|gunzip|df|du|ps|top|kill|killall|nohup|bg|fg|jobs|export|unset|env|source|\.|alias|unalias|history|clear|reset)(\s|$)/,
+ // Package managers
+ /^(npm|yarn|pnpm|pip|pip3|conda|brew|apt|apt-get|yum|dnf|pacman|curl|wget)(\s|$)/,
+ // Node.js commands
+ /^(node|npx)(\s|$)/,
+ // Python commands
+ /^(python|python3|pip|pip3|python3-m)(\s|$)/,
+ // Git commands
+ /^git(\s|$)/,
+ // Docker commands
+ /^docker(\s|$)/,
+ // File operations with paths
+ /^[a-z0-9_\-./]+\s*[\|>]/,
+ // Commands with arguments
+ /^[a-z][a-z0-9_\-]*\s+/,
+ // Absolute paths
+ /^\//,
+ // Scripts
+ /^(sh|bash|zsh|fish|powershell|pwsh)(\s|$)/
+ ];
+
+ return commandPatterns.some(pattern => pattern.test(trimmed));
+}
+
+// Handle command execution in Full Stack mode (via Claude CLI session's stdin)
+async function handleWebContainerCommand(message) {
+ const sessionId = attachedSessionId || chatSessionId;
+ if (!sessionId) {
+ appendSystemMessage('⚠️ No active session.');
+ hideStreamingIndicator();
+ setGeneratingState(false);
+ return;
+ }
+
+ // Smart command detection
+ if (!isShellCommand(message)) {
+ hideStreamingIndicator();
+ setGeneratingState(false);
+ appendSystemMessage(`💬 This looks like a conversational message, not a shell command.
+
+Terminal mode executes commands directly. For example:
+• npm install
+• ls -la
+• git status
+• python script.py
+
+Would you like to:
+1. Switch to Chat mode for conversational AI
+2. Rephrase as a shell command`);
+
+ // Auto-switch to Chat mode after brief delay
+ setTimeout(() => {
+ if (currentChatMode === 'webcontainer') {
+ setChatMode('auto');
+ appendSystemMessage('✅ Switched to Chat mode. You can continue your conversation.');
+ }
+ }, 3000);
+ return;
+ }
+
+ try {
+ appendSystemMessage(`💻 Executing in session: ${message}`);
+
+ // Send shell command to the active Claude CLI session
+ const result = await sendShellCommand(sessionId, message);
+
+ hideStreamingIndicator();
+ setGeneratingState(false);
+
+ // Show command output
+ if (result.stdout) {
+ appendMessage('assistant', result.stdout);
+ }
+
+ if (result.stderr) {
+ appendMessage('assistant', `${result.stderr}`);
+ }
+
+ // Show completion status
+ if (result.exitCode === 0) {
+ appendSystemMessage(`✅ Command completed successfully`);
+ } else {
+ appendSystemMessage(`⚠️ Command exited with code ${result.exitCode}`);
+ }
+
+ // Record tool usage
+ if (typeof contextPanel !== 'undefined' && contextPanel) {
+ contextPanel.recordToolUsage('shell_command');
+ }
+
+ } catch (error) {
+ console.error('Shell command execution failed:', error);
+ hideStreamingIndicator();
+ setGeneratingState(false);
+ appendSystemMessage('❌ Failed to execute command: ' + error.message);
+ }
+}
+
+// Fallback: Execute command in native mode via WebSocket
+async function executeNativeCommand(message, sessionId) {
+ try {
+ // Send via WebSocket (backend will execute through native mode)
+ window.ws.send(JSON.stringify({
+ type: 'command',
+ sessionId: sessionId,
+ command: message,
+ metadata: {
+ executionMode: 'native',
+ timestamp: new Date().toISOString()
+ }
+ }));
+
+ appendSystemMessage('✅ Command sent in native execution mode');
+ } catch (error) {
+ console.error('Native execution failed:', error);
+ throw new Error('Failed to execute in native mode: ' + error.message);
+ }
+}
+
+// Append Message to Chat (Enhanced with OpenCode-style rendering)
function appendMessage(role, content, scroll) {
const messagesContainer = document.getElementById('chat-messages');
@@ -295,36 +786,48 @@ function appendMessage(role, content, scroll) {
streaming.remove();
}
- const messageDiv = document.createElement('div');
- messageDiv.className = 'chat-message ' + role;
+ // Use enhanced message system if available, otherwise fall back to basic
+ if (typeof ChatMessage !== 'undefined' && typeof renderEnhancedMessage !== 'undefined') {
+ // Create enhanced message with part-based structure
+ const message = new ChatMessage(role, [
+ new MessagePart('text', content)
+ ]);
- const avatar = document.createElement('div');
- avatar.className = 'chat-message-avatar';
- avatar.textContent = role === 'user' ? '👤' : '🤖';
+ const messageDiv = renderEnhancedMessage(message);
+ messagesContainer.appendChild(messageDiv);
+ } else {
+ // Fallback to basic rendering
+ const messageDiv = document.createElement('div');
+ messageDiv.className = 'chat-message ' + role;
- const contentDiv = document.createElement('div');
- contentDiv.className = 'chat-message-content';
+ const avatar = document.createElement('div');
+ avatar.className = 'chat-message-avatar';
+ avatar.textContent = role === 'user' ? '👤' : '🤖';
- const bubble = document.createElement('div');
- bubble.className = 'chat-message-bubble';
+ const contentDiv = document.createElement('div');
+ contentDiv.className = 'chat-message-content';
- // Format content (handle code blocks, etc.)
- bubble.innerHTML = formatMessage(content);
+ const bubble = document.createElement('div');
+ bubble.className = 'chat-message-bubble';
- const timestamp = document.createElement('div');
- timestamp.className = 'chat-message-timestamp';
- timestamp.textContent = new Date().toLocaleTimeString();
+ // Format content (handle code blocks, etc.)
+ bubble.innerHTML = formatMessage(content);
- contentDiv.appendChild(bubble);
- contentDiv.appendChild(timestamp);
+ const timestamp = document.createElement('div');
+ timestamp.className = 'chat-message-timestamp';
+ timestamp.textContent = new Date().toLocaleTimeString();
- messageDiv.appendChild(avatar);
- messageDiv.appendChild(contentDiv);
+ contentDiv.appendChild(bubble);
+ contentDiv.appendChild(timestamp);
- messagesContainer.appendChild(messageDiv);
+ messageDiv.appendChild(avatar);
+ messageDiv.appendChild(contentDiv);
+
+ messagesContainer.appendChild(messageDiv);
+ }
// Scroll to bottom
- if (scroll) {
+ if (scroll || scroll === undefined) {
messagesContainer.scrollTop = messagesContainer.scrollHeight;
}
@@ -408,6 +911,9 @@ function hideStreamingIndicator() {
if (streaming) {
streaming.remove();
}
+
+ // Also reset generating state
+ setGeneratingState(false);
}
// Clear Chat Display
@@ -438,9 +944,13 @@ function clearChat() {
// Clear Input
function clearInput() {
const input = document.getElementById('chat-input');
+ const wrapper = document.getElementById('chat-input-wrapper');
+ const charCountBadge = document.getElementById('char-count-badge');
+
input.value = '';
input.style.height = 'auto';
- document.getElementById('char-count').textContent = '0 characters';
+ wrapper.classList.remove('typing');
+ charCountBadge.textContent = '0 chars';
}
// Update Token Usage
@@ -475,6 +985,21 @@ function attachFile() {
appendSystemMessage('File attachment feature coming soon! For now, use @filename to reference files.');
}
+// Attach Image (placeholder for now)
+function attachImage() {
+ appendSystemMessage('Image attachment feature coming soon! For now, use @filename to reference image files.');
+}
+
+// Insert Code Snippet (placeholder for now)
+function insertCodeSnippet() {
+ const input = document.getElementById('chat-input');
+ const snippet = '```\n// Your code here\n```\n';
+
+ input.value += snippet;
+ input.focus();
+ handleChatInput({ target: input });
+}
+
// Show Chat Settings (placeholder)
function showChatSettings() {
appendSystemMessage('Chat settings coming soon!');
diff --git a/server.js b/server.js
index 24fb0b3e..7dcd2a7d 100644
--- a/server.js
+++ b/server.js
@@ -75,6 +75,86 @@ function requireAuth(req, res, next) {
// Routes
+// Project URL route - decode base64 path and serve file
+app.get('/p/:base64Path/', (req, res) => {
+ try {
+ const base64Path = req.params.base64Path;
+
+ // Decode base64 path
+ let decodedPath;
+ try {
+ decodedPath = Buffer.from(base64Path, 'base64').toString('utf-8');
+ } catch (error) {
+ console.error('Error decoding base64 path:', error);
+ return res.status(400).send('Invalid base64 path');
+ }
+
+ // Resolve the full path
+ const fullPath = path.resolve(decodedPath);
+
+ // Security check: ensure path is within allowed directories
+ // Allow access to home directory and obsidian vault
+ const allowedPaths = [
+ '/home/uroma',
+ VAULT_PATH
+ ];
+
+ const isAllowed = allowedPaths.some(allowedPath => {
+ return fullPath.startsWith(allowedPath);
+ });
+
+ if (!isAllowed) {
+ console.error('Path outside allowed directories:', fullPath);
+ return res.status(403).send('Access denied');
+ }
+
+ // Check if file exists
+ if (!fs.existsSync(fullPath)) {
+ console.error('File not found:', fullPath);
+ return res.status(404).send('File not found');
+ }
+
+ // Check if it's a file (not a directory)
+ const stats = fs.statSync(fullPath);
+ if (stats.isDirectory()) {
+ // If it's a directory, try to serve index.html or list files
+ const indexPath = path.join(fullPath, 'index.html');
+ if (fs.existsSync(indexPath)) {
+ return res.sendFile(indexPath);
+ } else {
+ return res.status(403).send('Directory access not allowed');
+ }
+ }
+
+ // Determine content type
+ const ext = path.extname(fullPath).toLowerCase();
+ const contentTypes = {
+ '.html': 'text/html',
+ '.htm': 'text/html',
+ '.css': 'text/css',
+ '.js': 'application/javascript',
+ '.json': 'application/json',
+ '.png': 'image/png',
+ '.jpg': 'image/jpeg',
+ '.jpeg': 'image/jpeg',
+ '.gif': 'image/gif',
+ '.svg': 'image/svg+xml',
+ '.ico': 'image/x-icon',
+ '.txt': 'text/plain',
+ '.md': 'text/markdown'
+ };
+
+ const contentType = contentTypes[ext] || 'application/octet-stream';
+
+ // Serve the file
+ res.setHeader('Content-Type', contentType);
+ res.sendFile(fullPath);
+ } catch (error) {
+ console.error('Error serving file:', error);
+ res.status(500).send('Internal server error');
+ }
+});
+
// Sessions landing page (root of /claude/)
app.get('/claude/', (req, res) => {
if (req.session.userId) {
diff --git a/services/claude-service.js b/services/claude-service.js
index b7d78d0b..daf3999d 100644
--- a/services/claude-service.js
+++ b/services/claude-service.js
@@ -59,6 +59,88 @@ class ClaudeCodeService extends EventEmitter {
return session;
}
+ /**
+ * Execute shell command in session (for Full Stack mode)
+ * Spawns a shell process, sends command, captures output
+ */
+ async executeShellCommand(sessionId, command) {
+ const session = this.sessions.get(sessionId);
+
+ if (!session) {
+ throw new Error(`Session ${sessionId} not found`);
+ }
+
+ if (session.status !== 'running') {
+ throw new Error(`Session ${sessionId} is not running`);
+ }
+
+ console.log(`[ClaudeService] Executing shell command in ${sessionId}:`, command);
+
+ // Spawn shell to execute the command
+ const shell = spawn('bash', ['-c', command], {
+ cwd: session.workingDir,
+ stdio: ['ignore', 'pipe', 'pipe'],
+ env: {
+ ...process.env,
+ TERM: 'xterm-256color'
+ }
+ });
+
+ let stdout = '';
+ let stderr = '';
+
+ shell.stdout.on('data', (data) => {
+ const text = data.toString();
+ stdout += text;
+
+ // Add to output buffer
+ session.outputBuffer.push({
+ type: 'shell',
+ timestamp: new Date().toISOString(),
+ content: text
+ });
+
+ // Emit for real-time updates
+ this.emit('session-output', {
+ sessionId,
+ type: 'stdout',
+ content: text
+ });
+ });
+
+ shell.stderr.on('data', (data) => {
+ const text = data.toString();
+ stderr += text;
+
+ session.outputBuffer.push({
+ type: 'stderr',
+ timestamp: new Date().toISOString(),
+ content: text
+ });
+
+ this.emit('session-output', {
+ sessionId,
+ type: 'stderr',
+ content: text
+ });
+ });
+
+ return new Promise((resolve) => {
+ shell.on('close', (code) => {
+ const exitCode = code !== null ? code : -1;
+
+ console.log(`[ClaudeService] Shell command completed with exit code ${exitCode}`);
+
+ resolve({
+ exitCode,
+ stdout,
+ stderr,
+ success: exitCode === 0
+ });
+ });
+ });
+ }
+
/**
* Send command to a session using -p (print) mode
*/
@@ -94,6 +176,14 @@ class ClaudeCodeService extends EventEmitter {
timestamp: new Date().toISOString()
});
+ // Also save user message to outputBuffer for persistence
+ session.outputBuffer.push({
+ type: 'user',
+ role: 'user',
+ timestamp: new Date().toISOString(),
+ content: command
+ });
+
session.lastActivity = new Date().toISOString();
this.emit('command-sent', {