feat: Implement CLI session-based Full Stack mode

Replaces WebContainer-based approach with simpler Claude Code CLI session
shell command execution. This eliminates COOP/COEP header requirements
and reduces bundle size by ~350KB.

Changes:
- Added executeShellCommand() to ClaudeService for spawning bash processes
- Added /claude/api/shell-command API endpoint for executing commands
- Updated Full Stack mode (chat-functions.js) to use CLI sessions
- Simplified terminal mode by removing WebContainer dependencies

Benefits:
- No SharedArrayBuffer/COOP/COEP issues
- Uses existing Claude Code infrastructure
- Faster startup, more reliable execution
- Better error handling and output capture

Fixes:
- Terminal creation failure in Full Stack mode
- WebContainer SharedArrayBuffer serialization errors

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
uroma
2026-01-20 16:08:56 +00:00
Unverified
parent 6894c8bed4
commit 9b9ff5456d
3 changed files with 748 additions and 53 deletions

View File

@@ -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 = `
<div class="chat-history-empty">
<p>No active sessions</p>
<button class="btn-primary" onclick="startNewChat()" style="margin-top: 12px;">+ Start New Chat</button>
</div>
`;
// Zero-friction entry: Auto-create session in project context
if (currentProjectName && window.currentProjectDir) {
console.log('[loadChatView] No sessions for project, auto-creating...');
sessionsListEl.innerHTML = `
<div class="chat-history-empty">
<p>Creating session for project: <strong>${currentProjectName}</strong></p>
<div class="loading-spinner" style="margin: 20px auto;"></div>
</div>
`;
// Auto-create session
startNewChat();
} else {
const emptyMessage = `<p>No active sessions</p>`;
sessionsListEl.innerHTML = `
<div class="chat-history-empty">
${emptyMessage}
<button class="btn-primary" onclick="startNewChat()" style="margin-top: 12px;">+ Start New Chat</button>
</div>
`;
}
}
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:
• <code>npm install</code>
• <code>ls -la</code>
• <code>git status</code>
• <code>python script.py</code>
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: <code>${message}</code>`);
// 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', `<error>${result.stderr}</error>`);
}
// 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!');

View File

@@ -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) {

View File

@@ -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', {