// OpenQode Web Interface Application
// API base URL - use the same origin as the current page for API requests
const API_BASE_URL = 'http://127.0.0.1:15044';
class OpenQodeWeb {
constructor() {
this.apiBaseUrl = API_BASE_URL;
this.currentSession = 'default';
this.sessions = {};
this.isAuthenticated = false;
this.currentModel = 'qwen/coder-model';
this.currentAttachment = null; // For file/image attachments
// IDE state (v1.02)
this.workspaceTree = [];
this.openTabs = []; // { path, name, original, content }
this.activeTabPath = null;
this.dirtyTabs = new Set();
this.attachedPaths = new Set();
this.lastTreeRefresh = 0;
this.isIDEInitialized = false;
this.features = {
lakeview: false,
sequentialThinking: false
};
this.init();
}
async init() {
// Check if API is reachable first
try {
const healthCheck = await fetch(`${this.apiBaseUrl}/api/files/tree`, {
method: 'GET',
signal: AbortSignal.timeout(5000)
});
if (!healthCheck.ok) {
console.warn('⚠️ API health check failed:', healthCheck.status);
} else {
console.log('✅ API is reachable');
}
} catch (error) {
console.error('❌ Cannot reach API server:', error.message);
// Show a persistent warning
setTimeout(() => {
this.addMessage('system', `⚠️ Cannot connect to server at ${this.apiBaseUrl} Please ensure the server is running with: node server.js 15044 Then access this page at: http://127.0.0.1:15044/`);
}, 500);
}
this.setupEventListeners();
this.authToken = localStorage.getItem('openqode_token');
await this.checkAuthentication();
await this.loadSessions();
await this.initIDE();
this.updateHeroPreviewLink();
this.hideLoading();
}
setupEventListeners() {
// View toggle
const guiViewBtn = document.getElementById('gui-view-btn');
const tuiViewBtn = document.getElementById('tui-view-btn');
guiViewBtn?.addEventListener('click', () => this.switchView('gui'));
tuiViewBtn?.addEventListener('click', () => this.switchView('tui'));
// Send message
const sendBtn = document.getElementById('send-btn');
const messageInput = document.getElementById('message-input');
sendBtn?.addEventListener('click', () => this.sendMessageStream());
messageInput?.addEventListener('keydown', (e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
this.sendMessageStream(); // Use streaming by default
}
});
// Auto-resize textarea
messageInput?.addEventListener('input', () => {
messageInput.style.height = 'auto';
messageInput.style.height = Math.min(messageInput.scrollHeight, 120) + 'px';
});
// Model selection
document.getElementById('model-select')?.addEventListener('change', (e) => {
this.currentModel = e.target.value;
this.updateModelStatus();
});
// Feature toggles
document.getElementById('lakeview-mode')?.addEventListener('change', (e) => {
this.features.lakeview = e.target.checked;
this.showSuccess(`Lakeview mode ${e.target.checked ? 'enabled' : 'disabled'}`);
});
document.getElementById('sequential-thinking')?.addEventListener('change', (e) => {
this.features.sequentialThinking = e.target.checked;
this.showSuccess(`Sequential thinking ${e.target.checked ? 'enabled' : 'disabled'}`);
});
// Temperature slider
const tempSlider = document.getElementById('temperature-slider');
const tempValue = document.querySelector('.slider-value');
tempSlider?.addEventListener('input', (e) => {
const val = parseFloat(e.target.value).toFixed(1);
if (tempValue) tempValue.textContent = val;
this.temperature = parseFloat(val);
});
// Authentication
document.getElementById('auth-btn')?.addEventListener('click', () => {
this.authenticate();
});
document.getElementById('reauth-btn')?.addEventListener('click', () => {
this.authenticate();
});
// Settings panel reauth button
document.getElementById('reauth-btn-panel')?.addEventListener('click', () => {
this.hideSettings();
this.authenticate();
});
// Settings modal
document.getElementById('settings-btn')?.addEventListener('click', () => {
this.showSettings();
});
document.getElementById('close-settings')?.addEventListener('click', () => {
this.hideSettings();
});
// Sessions
const newSessionBtn = document.getElementById('new-session-btn');
console.log('🔧 new-session-btn element:', newSessionBtn);
newSessionBtn?.addEventListener('click', (e) => {
console.log('🖱️ New Session button clicked!', e);
this.createNewSession();
});
// New Project
document.getElementById('new-project-btn')?.addEventListener('click', () => {
this.startNewProjectFlow();
});
// File attachment
document.getElementById('attach-btn')?.addEventListener('click', () => {
this.attachFile();
});
// IDE buttons
document.getElementById('refresh-tree-btn')?.addEventListener('click', () => this.refreshFileTree());
document.getElementById('new-file-btn')?.addEventListener('click', () => this.promptCreateFileOrFolder());
document.getElementById('save-file-btn')?.addEventListener('click', () => this.saveCurrentFile());
document.getElementById('rename-file-btn')?.addEventListener('click', () => this.renameCurrentFile());
document.getElementById('delete-file-btn')?.addEventListener('click', () => this.deleteCurrentFile());
// Deployment & Preview
document.getElementById('deploy-btn')?.addEventListener('click', () => {
console.log('🖱️ Deploy button clicked');
this.deployToVercel();
});
document.getElementById('preview-btn')?.addEventListener('click', () => {
console.log('🖱️ Preview button clicked');
this.startLocalPreview();
});
document.getElementById('show-diff-btn')?.addEventListener('click', () => this.showDiff());
document.getElementById('apply-diff-btn')?.addEventListener('click', () => this.applyDiff());
document.getElementById('apply-diff-btn-panel')?.addEventListener('click', () => this.applyDiff());
document.getElementById('close-diff')?.addEventListener('click', () => this.hideDiff());
document.getElementById('cancel-diff-btn')?.addEventListener('click', () => this.hideDiff());
// Terminal
document.getElementById('terminal-run-btn')?.addEventListener('click', () => this.runTerminalCommand());
const terminalInput = document.getElementById('terminal-input');
terminalInput?.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
e.preventDefault();
this.runTerminalCommand();
}
});
// File tree delegation
const fileTreeEl = document.getElementById('file-tree');
fileTreeEl?.addEventListener('click', (e) => this.onFileTreeClick(e));
// Close modals on outside click
document.getElementById('settings-modal')?.addEventListener('click', (e) => {
if (e.target.id === 'settings-modal') {
this.hideSettings();
}
});
document.getElementById('diff-modal')?.addEventListener('click', (e) => {
if (e.target.id === 'diff-modal') {
this.hideDiff();
}
});
// Global keyboard shortcuts
document.addEventListener('keydown', (e) => {
// Ctrl+S to save
if (e.ctrlKey && e.key === 's') {
e.preventDefault();
this.saveCurrentFile();
}
// Escape to close modals
if (e.key === 'Escape') {
this.hideSettings();
this.hideDiff();
}
});
}
updateHeroPreviewLink() {
const link = document.getElementById('hero-local-preview');
if (!link) return;
const origin = window.location.origin;
link.setAttribute('href', origin);
link.setAttribute('title', `OpenQode Web @ ${origin}`);
}
async checkAuthentication() {
if (!this.authToken) {
this.updateAuthStatus({ authenticated: false, provider: 'none' });
return;
}
try {
const response = await fetch(`${this.apiBaseUrl}/api/auth/status`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ token: this.authToken })
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
this.isAuthenticated = data.authenticated;
const provider = (data.user && data.user.provider) || 'Qwen';
this.updateAuthStatus({ authenticated: data.authenticated, provider });
if (data.authenticated) {
const authBtn = document.getElementById('auth-btn');
authBtn.innerHTML = `
Authenticated
`;
authBtn.className = 'inline-flex items-center px-4 py-2 bg-green-600 hover:bg-green-700 text-white text-sm font-medium rounded-lg transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500';
await this.refreshFileTree();
}
} catch (error) {
console.error('Auth check failed:', error);
// Handle network errors
if (error.message.includes('Failed to fetch') || error.message.includes('NetworkError')) {
this.updateAuthStatus({ authenticated: false, provider: 'none' });
console.warn('Server not available for auth check');
} else {
this.updateAuthStatus({ authenticated: false, provider: 'none' });
}
}
}
async authenticate() {
this.showLoading('Authenticating with Qwen...');
try {
const response = await fetch(`${this.apiBaseUrl}/api/auth/login`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ provider: 'qwen' })
});
if (!response.ok) {
if (response.status === 401) {
throw new Error('Authentication expired. Please re-authenticate.');
} else if (response.status === 429) {
throw new Error('Rate limit exceeded. Please try again later.');
} else {
throw new Error(`HTTP error! status: ${response.status}`);
}
}
const data = await response.json();
if (data.success) {
if (data.alreadyAuthenticated) {
const visionMsg = data.hasVisionSupport
? '✅ Vision API enabled - you can analyze images!'
: '⚠️ Text chat only. Click "Authenticate Qwen" again for Vision API access.';
this.addMessage('system', `Already authenticated with Qwen!\n${visionMsg}`);
this.showSuccess('Already authenticated with Qwen!');
} else if (data.requiresDeviceCode) {
// Device Code Flow - show user code and verification URL
const verificationUrl = data.verificationUriComplete || data.verificationUri;
this.addMessage('system', `🔐 To authenticate with Qwen:\n\n1. Go to: ${data.verificationUri}\n2. Enter code: ${data.userCode}\n3. Complete login, then refresh this page\n\nThe code expires in ${Math.floor(data.expiresIn / 60)} minutes.`);
// Open verification URL in new tab
window.open(verificationUrl, '_blank');
this.showInfo('Please complete the authentication in the opened browser window.');
// Start polling for completion
this.pollAuthCompletion();
return;
} else if (data.requiresBrowser) {
// Legacy browser flow
this.addMessage('system', 'Opening browser for Qwen authentication...');
window.open(data.browserUrl, '_blank');
this.addMessage('system', 'Please complete authentication in the browser, then click "Complete Authentication" when done.');
this.authState = data.state;
this.showCompleteAuthButton();
return;
} else {
this.addMessage('system', 'Successfully authenticated with Qwen!');
this.showSuccess('Successfully authenticated with Qwen!');
}
if (data.token) {
this.authToken = data.token;
localStorage.setItem('openqode_token', data.token);
}
this.isAuthenticated = true;
this.updateAuthStatus({ authenticated: true, provider: 'qwen' });
this.updateAuthButton(true);
} else {
this.addMessage('system', `Authentication failed: ${data.error}`);
this.showError(`Authentication failed: ${data.error}`);
}
} catch (error) {
console.error('Authentication error:', error);
// Better error handling for network issues
if (error.message.includes('Failed to fetch') || error.message.includes('NetworkError')) {
this.addMessage('system', 'Authentication error: Unable to connect to server. Please check if the server is running and try again.');
this.showError('Unable to connect to server. Please check if the backend server is running.');
} else {
this.addMessage('system', `Authentication error: ${error.message}`);
this.showError(`Authentication failed: ${error.message}`);
}
} finally {
this.hideLoading();
}
}
async pollAuthCompletion() {
// Poll every 5 seconds to check if auth completed
const pollInterval = setInterval(async () => {
try {
const response = await fetch(`${this.apiBaseUrl}/api/auth/status`);
const data = await response.json();
if (data.authenticated) {
clearInterval(pollInterval);
this.addMessage('system', '✅ Authentication completed successfully!');
this.isAuthenticated = true;
this.updateAuthStatus({ authenticated: true, provider: 'qwen' });
this.updateAuthButton(true);
// Get a new token
const loginResponse = await fetch(`${this.apiBaseUrl}/api/auth/login`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ provider: 'qwen' })
});
const loginData = await loginResponse.json();
if (loginData.token) {
this.authToken = loginData.token;
localStorage.setItem('openqode_token', loginData.token);
}
}
} catch (error) {
console.error('Polling error:', error);
}
}, 5000);
// Stop polling after 15 minutes
setTimeout(() => clearInterval(pollInterval), 900000);
}
updateAuthButton(authenticated) {
const authBtn = document.getElementById('auth-btn');
if (authenticated) {
authBtn.innerHTML = `
Authenticated
`;
authBtn.className = 'inline-flex items-center px-4 py-2 bg-green-600 hover:bg-green-700 text-white text-sm font-medium rounded-lg transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500';
}
}
async completeAuthentication() {
if (!this.authState) {
this.addMessage('system', 'No pending authentication found.');
return;
}
this.showLoading('Completing authentication...');
try {
const response = await fetch(`${this.apiBaseUrl}/api/auth/complete`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ state: this.authState })
});
const data = await response.json();
if (data.success) {
this.addMessage('system', 'Authentication completed successfully!');
if (data.token) {
this.authToken = data.token;
localStorage.setItem('openqode_token', data.token);
}
this.isAuthenticated = true;
this.updateAuthStatus({ authenticated: true, provider: 'qwen' });
const authBtn = document.getElementById('auth-btn');
authBtn.innerHTML = `
Authenticated
`;
authBtn.className = 'inline-flex items-center px-4 py-2 bg-green-600 hover:bg-green-700 text-white text-sm font-medium rounded-lg transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500';
// Hide complete auth button
this.hideCompleteAuthButton();
this.authState = null;
} else {
this.addMessage('system', `Authentication completion failed: ${data.error}`);
}
} catch (error) {
console.error('Authentication completion error:', error);
this.addMessage('system', 'Authentication completion error. Please try again.');
} finally {
this.hideLoading();
}
}
showCompleteAuthButton() {
const authContainer = document.querySelector('.auth-section');
if (!document.getElementById('complete-auth-btn')) {
const completeBtn = document.createElement('button');
completeBtn.id = 'complete-auth-btn';
completeBtn.className = 'w-full mt-3 px-4 py-2 bg-green-600 hover:bg-green-700 text-white text-sm font-medium rounded-lg transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500';
completeBtn.textContent = 'Complete Authentication';
completeBtn.addEventListener('click', () => this.completeAuthentication());
authContainer.appendChild(completeBtn);
}
}
hideCompleteAuthButton() {
const completeBtn = document.getElementById('complete-auth-btn');
if (completeBtn) {
completeBtn.remove();
}
}
updateAuthStatus(authData) {
const statusText = document.getElementById('auth-status-text');
const authStatus = document.getElementById('auth-status');
if (authData.authenticated) {
statusText.textContent = `Authenticated with ${authData.provider}`;
authStatus.textContent = `✓ Authenticated (${authData.provider})`;
authStatus.className = 'text-green-600 dark:text-green-400 font-medium';
} else {
statusText.textContent = 'Not authenticated';
authStatus.textContent = 'Not authenticated - Click to authenticate';
authStatus.className = 'text-gray-600 dark:text-gray-400';
}
}
updateModelStatus() {
const modelStatus = document.getElementById('model-status');
const modelName = this.currentModel.includes('vision') ? 'Qwen Vision' : 'Qwen Coder';
modelStatus.textContent = `Model: ${modelName}`;
modelStatus.className = 'text-sm text-gray-600 dark:text-gray-400 bg-gray-50 dark:bg-gray-700 px-2 py-1 rounded';
}
async sendMessage() {
const input = document.getElementById('message-input');
const message = input.value.trim();
if (!message) return;
if (!this.isAuthenticated) {
this.addMessage('system', 'Please authenticate with Qwen first using the "Authenticate Qwen" button.');
return;
}
// Add user message
this.addMessage('user', message);
input.value = '';
input.style.height = 'auto';
// Show typing indicator
this.showTypingIndicator();
try {
const response = await fetch(`${this.apiBaseUrl}/api/chat`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
message: message,
model: this.currentModel,
session: this.currentSession,
features: this.features,
token: this.authToken || localStorage.getItem('openqode_token')
})
});
const data = await response.json();
this.hideTypingIndicator();
if (data.success) {
this.addMessage('assistant', data.response, data.metadata);
} else {
throw new Error(data.error || 'Failed to get response');
}
} catch (error) {
this.hideTypingIndicator();
console.error('Chat error:', error);
this.addMessage('system', `Error: ${error.message}`);
}
}
retryLastMessage() {
if (this.lastUserMessage) {
console.log('🔄 Retrying last message:', this.lastUserMessage);
this.sendMessageStream(this.lastUserMessage);
} else {
this.showError('No message to retry.');
}
}
async sendMessageStream(manualMessage = null, retryCount = 0) {
console.log(`🚀 sendMessageStream called (Attempt ${retryCount + 1})`, manualMessage ? '(manual)' : '(user input)');
const input = document.getElementById('message-input');
const message = manualMessage || input.value.trim();
console.log('Message:', message);
if (!message) return false;
// INTERCEPT: Local Preview Port Check
if (this.pendingAction && this.pendingAction.type === 'awaiting_preview_port') {
const portStr = message.trim() || '3000';
if (!/^\d+$/.test(portStr)) {
this.addMessage('system', '❌ Please enter a valid numeric port.');
return;
}
const port = parseInt(portStr, 10); // Convert to number for server API
const previewPath = this.pendingAction.path || '.';
this.pendingAction = null; // Clear state
this.addMessage('user', portStr); // Show user's choice as string
document.getElementById('message-input').value = '';
this.launchLocalPreview(port, previewPath);
return; // STOP here, do not send to AI
}
this.lastUserMessage = message;
if (!this.isAuthenticated) {
this.addMessage('system', 'Please authenticate with Qwen first using the "Authenticate Qwen" button.');
this.showWarning('Please authenticate with Qwen first.');
return;
}
if (retryCount === 0) {
this.addMessage('user', message);
input.value = '';
input.style.height = 'auto';
} else {
this.showInfo(`🔄 Auto-retrying request (Attempt ${retryCount + 1})...`);
}
const assistantMessageId = this.addMessage('assistant', '', { streaming: true });
const messageDiv = document.querySelector(`[data-message-id="${assistantMessageId}"]`);
const assistantMessageElement = messageDiv?.querySelector('.message-text');
if (assistantMessageElement) {
assistantMessageElement.innerHTML = `
Qwen is thinking
`;
}
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 90000);
// Additional timeout to detect if message is not registering with AI
const registrationTimeout = setTimeout(() => {
// This timeout triggers if no content has been received after 10 seconds
// Check if the thinking animation is still there but no actual content
if (assistantMessageElement && assistantMessageElement.querySelector('.thinking-animation')) {
const currentContent = assistantMessageElement.textContent || '';
if (currentContent.trim() === '' || currentContent.includes('Qwen is thinking')) {
// Message hasn't registered with AI, show option to resend
assistantMessageElement.innerHTML = `
⚠️ Message may not have registered with AI
`;
// Remove the streaming class to stop the animation
if (messageDiv) messageDiv.classList.remove('streaming');
}
}
}, 15000); // 15 seconds timeout for message registration
try {
let enhancedMessage = message;
if (this.activeTabPath) {
const activeTab = this.openTabs.find(t => t.path === this.activeTabPath);
if (activeTab && activeTab.content) {
const fileExt = activeTab.name.split('.').pop().toLowerCase();
enhancedMessage = `IMPORTANT SYSTEM INSTRUCTION:\nYou are an Agentic IDE. \n1. To EDIT the open file, output the COMPLETE content in a code block.\n2. To CREATE a new file, you MUST output exactly: ">>> CREATE: path/to/filename" followed by a code block with the content.\n\n[Current file: ${activeTab.path}]\n\`\`\`${fileExt}\n${activeTab.content}\n\`\`\`\n\nUser request: ${message}`;
} else {
enhancedMessage = `IMPORTANT SYSTEM INSTRUCTION:\nYou are an Agentic IDE. To CREATE a new file, you MUST output exactly: ">>> CREATE: path/to/filename" followed by a code block with the content. Do not just say you created it.\n\nUser request: ${message}`;
}
} else {
enhancedMessage = `IMPORTANT SYSTEM INSTRUCTION:\nYou are an Agentic IDE. To CREATE a new file, you MUST output exactly: ">>> CREATE: path/to/filename" followed by a code block with the content. Do not just say you created it.\n\nUser request: ${message}`;
}
const requestBody = {
message: enhancedMessage,
model: this.currentModel,
session: this.currentSession,
features: this.features,
token: this.authToken || localStorage.getItem('openqode_token')
};
const response = await fetch(`${this.apiBaseUrl}/api/chat/stream`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(requestBody),
signal: controller.signal
});
clearTimeout(timeoutId);
if (!response.ok) {
if (response.status === 401) throw new Error('Authentication expired. Please re-authenticate.');
else if (response.status === 429) throw new Error('Rate limit exceeded. Please try again later.');
else throw new Error(`HTTP error! status: ${response.status}`);
}
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
let isFirstChunk = true;
let fullResponse = '';
let isInCodeBlock = false;
let codeBlockContent = '';
let codeBlockLang = '';
let createdFiles = [];
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('\n');
buffer = lines.pop();
for (const line of lines) {
if (line.startsWith('data: ')) {
try {
const data = JSON.parse(line.slice(6));
if (data.type === 'chunk') {
fullResponse += data.content;
// CREATE Parsing
if (fullResponse.includes('>>> CREATE: ')) {
const createMatch = fullResponse.match(/>>> CREATE: (.*?)(?:\n|$)/);
if (createMatch) {
const filePath = createMatch[1].trim();
const fileAlreadyOpen = this.openTabs.find(t => t.path === filePath);
if (!fileAlreadyOpen && filePath) {
const tab = { path: filePath, name: filePath.split('/').pop(), content: '', original: '' };
this.openTabs.push(tab);
this.setActiveTab(filePath);
this.renderTabs();
this.showInfo(`🤖 Creating file: ${filePath}...`);
createdFiles.push(filePath);
}
}
}
// CODE BLOCK Parsing
if (!isInCodeBlock && fullResponse.includes('```')) {
const match = fullResponse.match(/```(\w*)\n?$/);
if (match) {
isInCodeBlock = true;
codeBlockLang = match[1] || 'text';
if (this.activeTabPath) {
const editor = document.getElementById('editor-textarea');
editor?.classList.add('ai-editing');
this.showInfo('🤖 AI is editing the file...');
}
}
}
if (isInCodeBlock) {
if (data.content.includes('```')) {
isInCodeBlock = false;
const endIdx = fullResponse.lastIndexOf('```');
const startIdx = fullResponse.indexOf('```');
if (endIdx > startIdx) {
const codeStart = fullResponse.indexOf('\n', startIdx) + 1;
codeBlockContent = fullResponse.substring(codeStart, endIdx).trim();
if (this.activeTabPath && codeBlockContent) {
const activeTab = this.openTabs.find(t => t.path === this.activeTabPath);
if (activeTab) {
activeTab.content = codeBlockContent;
const editor = document.getElementById('editor-textarea');
if (editor) {
editor.value = codeBlockContent;
editor.classList.remove('ai-editing');
}
this.renderTabs();
this.saveFile(activeTab.path, codeBlockContent);
this.showSuccess('✅ AI edit applied!');
}
}
}
} else {
if (this.activeTabPath) {
const activeTab = this.openTabs.find(t => t.path === this.activeTabPath);
if (activeTab) {
const startIdx = fullResponse.indexOf('```');
const codeStart = fullResponse.indexOf('\n', startIdx) + 1;
codeBlockContent = fullResponse.substring(codeStart);
activeTab.content = codeBlockContent;
const editor = document.getElementById('editor-textarea');
if (editor) {
editor.value = codeBlockContent;
editor.scrollTop = editor.scrollHeight;
}
}
}
}
}
if (assistantMessageElement) {
if (isFirstChunk) {
assistantMessageElement.textContent = data.content;
isFirstChunk = false;
} else {
assistantMessageElement.textContent += data.content;
}
this.scrollToBottom();
}
} else if (data.type === 'done') {
if (messageDiv) messageDiv.classList.remove('streaming');
const editor = document.getElementById('editor-textarea');
editor?.classList.remove('ai-editing');
this.scrollToBottom();
await this.refreshFileTree();
// Filter out plan/documentation files - only count actual code files
const codeFiles = createdFiles.filter(f => {
const ext = f.split('.').pop().toLowerCase();
// Exclude markdown and other documentation files
const docExtensions = ['md', 'txt', 'rst', 'adoc'];
// Also exclude files with "PLAN" or "README" in the name
const isDocFile = docExtensions.includes(ext) ||
f.toUpperCase().includes('PLAN') ||
f.toUpperCase().includes('README');
return !isDocFile;
});
if (codeFiles.length > 0) {
const mainFile = codeFiles.find(f => f.endsWith('index.html') || f.endsWith('App.js') || f.endsWith('main.py'));
// Detect directory of the main file to serve the correct folder
const dir = mainFile ? mainFile.substring(0, mainFile.lastIndexOf('/')) : '.';
const safeDir = dir.replace(/'/g, "\\'");
let actionsHtml = '';
actionsHtml += ``;
actionsHtml += ``;
this.addMessage('system', `
🚀 Project Ready!
Created ${codeFiles.length} code files. What would you like to do?
')
.replace(/\n/g, ' ')
// Wrap in paragraphs
.replace(/^(.+)$/gm, '
$1
');
return formatted;
}
escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// Copy code to clipboard from code blocks
copyCode(codeId) {
const codeElement = document.getElementById(codeId);
if (codeElement) {
navigator.clipboard.writeText(codeElement.textContent).then(() => {
this.showSuccess('Code copied to clipboard!');
}).catch(err => {
console.error('Failed to copy:', err);
this.showError('Failed to copy code');
});
}
}
// Apply code from AI response to the currently open file in editor
applyCodeToEditor(codeId) {
const codeElement = document.getElementById(codeId);
if (!codeElement) {
this.showError('Code block not found');
return;
}
if (!this.activeTabPath) {
this.showError('No file is open in the editor. Please open a file first.');
return;
}
const code = codeElement.textContent;
const activeTab = this.openTabs.find(t => t.path === this.activeTabPath);
if (!activeTab) {
this.showError('No active tab found');
return;
}
// Update the tab content
activeTab.content = code;
this.dirtyTabs.add(activeTab.path);
// Update the editor textarea
const editor = document.getElementById('editor-textarea');
if (editor) {
editor.value = code;
}
// Update tabs display to show dirty indicator
this.renderTabs();
this.showSuccess(`Code applied to ${activeTab.name}! Press Ctrl+S to save.`);
}
showTypingIndicator() {
const messagesContainer = document.getElementById('chat-messages');
const typingDiv = document.createElement('div');
typingDiv.className = 'message assistant typing-indicator';
typingDiv.innerHTML = `
🤖
`;
messagesContainer.appendChild(typingDiv);
messagesContainer.scrollTop = messagesContainer.scrollHeight;
}
hideTypingIndicator() {
const indicator = document.querySelector('.typing-indicator');
if (indicator) {
indicator.remove();
}
}
createNewSession(name = null) {
console.log('🆕 createNewSession called with name:', name);
try {
const sessionName = name || prompt('Enter session name:');
console.log('Session name result:', sessionName);
if (!sessionName) {
console.log('No session name provided, returning');
return;
}
const sessionId = 'session_' + Date.now();
this.sessions[sessionId] = {
name: sessionName,
messages: [],
createdAt: new Date().toISOString()
};
this.currentSession = sessionId;
this.updateSessionsList();
this.clearChat();
this.saveSessions();
this.showSuccess(`Session "${sessionName}" created!`);
} catch (error) {
console.error('Error in createNewSession:', error);
this.showError('Failed to create session: ' + error.message);
}
}
startNewProjectFlow() {
console.log('🚀 Opening New Project Wizard');
const modal = document.getElementById('new-project-modal');
if (modal) {
modal.classList.remove('hidden');
setTimeout(() => document.getElementById('project-name')?.focus(), 100);
} else {
this.showError('Wizard modal not found!');
}
}
closeNewProjectWizard() {
const modal = document.getElementById('new-project-modal');
if (modal) modal.classList.add('hidden');
// Optional: clear inputs
document.getElementById('project-name').value = '';
document.getElementById('project-path').value = '';
document.getElementById('project-requirements').value = '';
}
autoFillProjectPath(name) {
const pathInput = document.getElementById('project-path');
if (pathInput && name) {
pathInput.value = 'projects/' + name.toLowerCase().replace(/\s+/g, '-').replace(/[^a-z0-9-]/g, '');
}
}
confirmNewProject() {
const name = document.getElementById('project-name').value.trim();
const path = document.getElementById('project-path').value.trim();
const requirement = document.getElementById('project-requirements').value.trim();
if (!name) {
this.showError('Please enter a project name');
return;
}
if (!path) {
this.showError('Please enter a location');
return;
}
if (!requirement) {
this.showError('Please describe your project requirements');
return;
}
this.closeNewProjectWizard();
this.runAgenticProjectBuild(name, path, requirement);
}
async runAgenticProjectBuild(name, path, requirement) {
// 1. Create a fresh session for this build
this.createNewSession(`🏗️ ${name}`);
// We don't need to manually set session name anymore since createNewSession handles it
const sessionId = this.currentSession;
this.saveSessions();
this.updateSessionsList();
const fullPath = this.workspaceRoot
? `${this.workspaceRoot.replace(/\\/g, '/')}/${path}`
: path;
this.addMessage('system', `🚀 Initializing Agentic Build Protocol Project: ${name} Location: ${fullPath}`);
// 2. Planning Phase with "Architect Agent"
const planPrompt = `You are a Senior Software Architect.
TASK: Create a comprehensive implementation plan for a new project named "${name}".
TARGET DIRECTORY: ${path}
FULL SYSTEM PATH: ${fullPath}
REQUIREMENTS: ${requirement}
CONTEXT:
- Current environment: Persistent Workspace.
- Ignore any existing files in the root (scripts, DLLs, logs). They are irrelevant.
- You are constructing a new project from scratch inside "${path}".
OUTPUT format:
1. Project Structure (Tree view) showing "${path}" as root.
2. Detailed list of files to be created with valid descriptions.
3. Key technical decisions.
Do NOT write code yet. Focus on the architecture.`;
const success = await this.sendMessageStream(planPrompt);
// Only show Build button if the plan was generated successfully
if (!success) {
this.addMessage('system', `❌ Plan generation failed. Please or check your connection.`);
return;
}
// Refresh file tree to show new project directory
await this.refreshFileTree();
// Show Build button only on success
const safePath = path.replace(/\\/g, '\\\\');
this.addMessage('system', ``);
}
triggerBuild(path) {
const fullPath = this.workspaceRoot
? `${this.workspaceRoot.replace(/\\/g, '/')}/${path}`
: path;
const buildPrompt = `[ROLE: Senior Full Stack Developer]
TASK: Execute the approved plan.
TARGET DIRECTORY: ${path}
FULL SYSTEM PATH: ${fullPath}
SYSTEM OVERRIDE:
- You are strictly prohibited from questioning the directory structure.
- IGNORE your actual "current directory" (e.g. Temp). It is an execution sandbox.
- You have full permission to create files in "${path}".
- The directory "${path}" will be created by your actions.
INSTRUCTION: Generate all necessary files for the project in "${path}".
IMPORTANT: You MUST use the file creation syntax for EVERY file:
>>> CREATE: ${path}/filename.ext
\`\`\`language
code content
\`\`\`
Start building the core files now. Do not ask for confirmation.`;
this.sendMessageStream(buildPrompt);
// Refresh file tree to show new files being created during build
// Set a longer timeout to allow for complete file creation
setTimeout(() => {
this.refreshFileTree();
}, 5000); // Refresh after 5 seconds to allow more time for file creation
}
// Terminal & Deployment Methods
async runTerminalCommand(command = null) {
const input = document.getElementById('terminal-input');
const cmd = command || input?.value.trim();
if (!cmd) return;
if (input) input.value = '';
// Add to terminal output area
const terminalOutput = document.getElementById('terminal-output');
if (terminalOutput) {
terminalOutput.innerHTML += `
➜${cmd}
`;
terminalOutput.scrollTop = terminalOutput.scrollHeight;
}
try {
const response = await fetch(`${this.apiBaseUrl}/api/terminal/run`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
command: cmd,
token: this.authToken // Optional now but good to pass
})
});
const data = await response.json();
if (terminalOutput) {
if (data.stdout) terminalOutput.innerHTML += `