Files
SuperCharged-Claude-Code-Up…/public/claude-ide/components/monaco-editor-monaco-editor.js-1769008703817.js
uroma 55aafbae9a Fix project isolation: Make loadChatHistory respect active project sessions
- Modified loadChatHistory() to check for active project before fetching all sessions
- When active project exists, use project.sessions instead of fetching from API
- Added detailed console logging to debug session filtering
- This prevents ALL sessions from appearing in every project's sidebar

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-22 14:43:05 +00:00

661 lines
22 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Monaco Editor Component
* VS Code's editor in the browser with tab system
*
* Features:
* - Tab-based multi-file editing
* - Syntax highlighting for 100+ languages
* - Auto-save on Ctrl+S
* - Dirty state indicators
* - Mobile responsive (CodeMirror fallback on touch devices)
*/
class MonacoEditor {
constructor(containerId) {
this.container = document.getElementById(containerId);
if (!this.container) {
console.error('[MonacoEditor] Container not found:', containerId);
return;
}
this.editors = new Map(); // tabId -> editor instance
this.models = new Map(); // tabId -> model instance
this.tabs = [];
this.activeTab = null;
this.monaco = null;
this.isMobile = this.detectMobile();
this.initialized = false;
}
detectMobile() {
// Check for actual mobile device (not just touch-enabled laptop)
const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
// Also check screen width as additional heuristic
const isSmallScreen = window.innerWidth < 768;
return isMobile || isSmallScreen;
}
async initialize() {
if (this.initialized) return;
if (this.isMobile) {
// Use CodeMirror for mobile (touch-friendly)
console.log('[MonacoEditor] Mobile detected, using fallback');
this.initializeFallback();
return;
}
try {
// Wrap AMD loader in promise
await new Promise((resolve, reject) => {
// Configure Monaco loader
require.config({
paths: {
'vs': 'https://unpkg.com/monaco-editor@0.45.0/min/vs'
}
});
// Load Monaco
require(['vs/editor/editor.main'], (monaco) => {
this.monaco = monaco;
this.setupContainer();
this.setupKeyboardShortcuts();
this.loadPersistedTabs();
this.initialized = true;
console.log('[MonacoEditor] Initialized successfully');
resolve();
}, (error) => {
console.error('[MonacoEditor] AMD loader error:', error);
reject(error);
});
});
} catch (error) {
console.error('[MonacoEditor] Failed to initialize:', error);
this.initializeFallback();
this.initialized = true;
}
}
setupContainer() {
this.container.innerHTML = `
<div class="monaco-editor-container">
<div class="editor-tabs-wrapper">
<div class="editor-tabs" id="editor-tabs"></div>
<div class="editor-tabs-actions">
<button class="btn-icon" id="btn-save-current" title="Save (Ctrl+S)" style="display: none;">💾</button>
<button class="btn-icon" id="btn-save-all" title="Save All (Ctrl+Shift+S)">💾</button>
<button class="btn-icon" id="btn-close-all" title="Close All">✕</button>
</div>
</div>
<div class="editor-content-wrapper">
<div class="editor-content" id="editor-content">
<div class="editor-placeholder">
<div class="placeholder-icon">📄</div>
<h2>No file open</h2>
<p>Select a file from the sidebar to start editing</p>
<p style="font-size: 0.9em; opacity: 0.7; margin-top: 8px;">Files are automatically editable</p>
</div>
</div>
</div>
<div class="editor-statusbar">
<span class="statusbar-item" id="statusbar-cursor">Ln 1, Col 1</span>
<span class="statusbar-item" id="statusbar-language">Plain Text</span>
<span class="statusbar-item" id="statusbar-file">No file</span>
<span class="statusbar-item" id="statusbar-editable" style="display: none;">✓ Editable</span>
</div>
</div>
`;
// Event listeners
const saveCurrentBtn = this.container.querySelector('#btn-save-current');
if (saveCurrentBtn) {
saveCurrentBtn.addEventListener('click', () => this.saveCurrentFile());
}
const saveAllBtn = this.container.querySelector('#btn-save-all');
if (saveAllBtn) {
saveAllBtn.addEventListener('click', () => this.saveAllFiles());
}
const closeAllBtn = this.container.querySelector('#btn-close-all');
if (closeAllBtn) {
closeAllBtn.addEventListener('click', () => this.closeAllTabs());
}
}
setupKeyboardShortcuts() {
// Ctrl+S to save
document.addEventListener('keydown', (e) => {
if ((e.ctrlKey || e.metaKey) && e.key === 's') {
e.preventDefault();
this.saveCurrentFile();
}
// Ctrl+W to close tab
if ((e.ctrlKey || e.metaKey) && e.key === 'w') {
e.preventDefault();
this.closeCurrentTab();
}
});
}
getLanguageFromFile(filePath) {
const ext = filePath.split('.').pop().toLowerCase();
const languageMap = {
'js': 'javascript',
'jsx': 'javascript',
'ts': 'typescript',
'tsx': 'typescript',
'py': 'python',
'html': 'html',
'htm': 'html',
'css': 'css',
'scss': 'scss',
'sass': 'scss',
'json': 'json',
'md': 'markdown',
'markdown': 'markdown',
'xml': 'xml',
'yaml': 'yaml',
'yml': 'yaml',
'sql': 'sql',
'sh': 'shell',
'bash': 'shell',
'zsh': 'shell',
'txt': 'plaintext'
};
return languageMap[ext] || 'plaintext';
}
async openFile(filePath, content) {
if (!this.initialized && !this.isMobile) {
await this.initialize();
}
if (this.isMobile) {
this.openFileFallback(filePath, content);
return;
}
// Check if already open
const existingTab = this.tabs.find(tab => tab.path === filePath);
if (existingTab) {
this.activateTab(existingTab.id);
return;
}
// Create new tab
const tabId = `tab-${Date.now()}`;
const tab = {
id: tabId,
path: filePath,
name: filePath.split('/').pop(),
dirty: false,
originalContent: content || ''
};
this.tabs.push(tab);
// Create Monaco model
const language = this.getLanguageFromFile(filePath);
const model = this.monaco.editor.createModel(content || '', language, monaco.Uri.parse(filePath));
this.models.set(tabId, model);
// Create editor instance
const contentArea = this.container.querySelector('#editor-content');
// Remove placeholder
const placeholder = contentArea.querySelector('.editor-placeholder');
if (placeholder) placeholder.remove();
// Create editor container
const editorContainer = document.createElement('div');
editorContainer.className = 'monaco-editor-instance';
editorContainer.style.display = 'none';
contentArea.appendChild(editorContainer);
// Create editor
const editor = this.monaco.editor.create(editorContainer, {
model: model,
theme: 'vs-dark',
automaticLayout: true,
fontSize: 14,
fontFamily: "'Fira Code', 'JetBrains Mono', 'SF Mono', 'Menlo', 'Consolas', monaco",
lineNumbers: 'on',
minimap: { enabled: true },
scrollBeyondLastLine: false,
wordWrap: 'off',
tabSize: 4,
renderWhitespace: 'selection',
cursorStyle: 'line',
folding: true,
bracketPairColorization: { enabled: true },
guides: {
indentation: true,
bracketPairs: true
}
});
// Track cursor position
editor.onDidChangeCursorPosition((e) => {
this.updateCursorPosition(e.position);
});
// Track content changes
model.onDidChangeContent(() => {
this.markDirty(tabId);
});
this.editors.set(tabId, editor);
// Activate the new tab
this.activateTab(tabId);
// Persist tabs
this.saveTabsToStorage();
return tabId;
}
activateTab(tabId) {
if (!this.editors.has(tabId)) {
console.error('[MonacoEditor] Tab not found:', tabId);
return;
}
// Hide all editors
this.editors.forEach((editor, id) => {
const container = editor.getDomNode();
if (container) {
container.style.display = id === tabId ? 'block' : 'none';
}
});
this.activeTab = tabId;
this.renderTabs();
this.updateStatusbar(tabId);
// Show save button for current file and editable indicator
const tab = this.tabs.find(t => t.id === tabId);
const saveCurrentBtn = this.container.querySelector('#btn-save-current');
const editableIndicator = this.container.querySelector('#statusbar-editable');
if (saveCurrentBtn) {
saveCurrentBtn.style.display = 'inline-flex';
saveCurrentBtn.title = `Save ${tab?.name || 'file'} (Ctrl+S)`;
}
if (editableIndicator) {
editableIndicator.style.display = 'inline-flex';
editableIndicator.textContent = tab?.dirty ? '● Unsaved changes' : '✓ Editable';
editableIndicator.style.color = tab?.dirty ? '#f48771' : '#4ec9b0';
}
// Focus the active editor and ensure it's not read-only
const editor = this.editors.get(tabId);
if (editor) {
editor.focus();
editor.updateOptions({ readOnly: false });
}
}
closeTab(tabId) {
const tab = this.tabs.find(t => t.id === tabId);
if (!tab) return;
// Check for unsaved changes
if (tab.dirty) {
const shouldSave = confirm(`Save changes to ${tab.name} before closing?`);
if (shouldSave) {
this.saveFile(tabId);
}
}
// Dispose editor and model
const editor = this.editors.get(tabId);
if (editor) {
editor.dispose();
this.editors.delete(tabId);
}
const model = this.models.get(tabId);
if (model) {
model.dispose();
this.models.delete(tabId);
}
// Remove tab from list
this.tabs = this.tabs.filter(t => t.id !== tabId);
// If we closed the active tab, activate another one
if (this.activeTab === tabId) {
if (this.tabs.length > 0) {
this.activateTab(this.tabs[0].id);
} else {
this.activeTab = null;
this.showPlaceholder();
}
}
this.renderTabs();
this.saveTabsToStorage();
}
closeCurrentTab() {
if (this.activeTab) {
this.closeTab(this.activeTab);
}
}
closeAllTabs() {
if (this.tabs.length === 0) return;
const hasUnsaved = this.tabs.some(t => t.dirty);
if (hasUnsaved) {
const shouldSaveAll = confirm('Some files have unsaved changes. Save all before closing?');
if (shouldSaveAll) {
this.saveAllFiles();
}
}
// Dispose all editors and models
this.editors.forEach(editor => editor.dispose());
this.models.forEach(model => model.dispose());
this.editors.clear();
this.models.clear();
this.tabs = [];
this.activeTab = null;
this.renderTabs();
this.showPlaceholder();
this.saveTabsToStorage();
}
async saveFile(tabId) {
const tab = this.tabs.find(t => t.id === tabId);
if (!tab) return;
const model = this.models.get(tabId);
if (!model) return;
const content = model.getValue();
try {
const response = await fetch(`/claude/api/file/${encodeURIComponent(tab.path)}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ content })
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const data = await response.json();
if (data.error) {
throw new Error(data.error);
}
// Update tab state
tab.dirty = false;
tab.originalContent = content;
this.renderTabs();
// Show success toast
if (typeof showToast === 'function') {
showToast(`✅ Saved ${tab.name}`, 'success', 2000);
}
return true;
} catch (error) {
console.error('[MonacoEditor] Error saving file:', error);
if (typeof showToast === 'function') {
showToast(`❌ Failed to save ${tab.name}: ${error.message}`, 'error', 3000);
}
return false;
}
}
async saveCurrentFile() {
if (this.activeTab) {
await this.saveFile(this.activeTab);
}
}
async saveAllFiles() {
const dirtyTabs = this.tabs.filter(t => t.dirty);
if (dirtyTabs.length === 0) {
if (typeof showToast === 'function') {
showToast('No unsaved changes', 'info', 2000);
}
return;
}
let saved = 0;
let failed = 0;
for (const tab of dirtyTabs) {
const result = await this.saveFile(tab.id);
if (result) {
saved++;
} else {
failed++;
}
}
if (typeof showToast === 'function') {
if (failed === 0) {
showToast(`✅ Saved ${saved} file${saved > 1 ? 's' : ''}`, 'success', 2000);
} else {
showToast(`⚠️ Saved ${saved} file${saved > 1 ? 's' : ''}, ${failed} failed`, 'warning', 3000);
}
}
}
markDirty(tabId) {
const tab = this.tabs.find(t => t.id === tabId);
if (tab && !tab.dirty) {
tab.dirty = true;
this.renderTabs();
}
}
updateCursorPosition(position) {
const cursorEl = this.container.querySelector('#statusbar-cursor');
if (cursorEl && position) {
cursorEl.textContent = `Ln ${position.lineNumber}, Col ${position.column}`;
}
}
updateStatusbar(tabId) {
const tab = this.tabs.find(t => t.id === tabId);
if (!tab) return;
const fileEl = this.container.querySelector('#statusbar-file');
const langEl = this.container.querySelector('#statusbar-language');
if (fileEl) {
fileEl.textContent = tab.path;
}
if (langEl) {
const language = this.getLanguageFromFile(tab.path);
langEl.textContent = language.charAt(0).toUpperCase() + language.slice(1);
}
}
renderTabs() {
const tabsContainer = this.container.querySelector('#editor-tabs');
if (!tabsContainer) return;
if (this.tabs.length === 0) {
tabsContainer.innerHTML = '';
return;
}
tabsContainer.innerHTML = this.tabs.map(tab => `
<div class="editor-tab ${tab.id === this.activeTab ? 'active' : ''} ${tab.dirty ? 'dirty' : ''}"
data-tab-id="${tab.id}"
title="${this.escapeHtml(tab.path)}">
<span class="tab-name">${this.escapeHtml(tab.name)}</span>
${tab.dirty ? '<span class="tab-dirty-indicator">●</span>' : ''}
<button class="tab-close" title="Close tab">×</button>
</div>
`).join('');
// Tab click handlers
tabsContainer.querySelectorAll('.editor-tab').forEach(tabEl => {
tabEl.addEventListener('click', (e) => {
if (!e.target.classList.contains('tab-close')) {
this.activateTab(tabEl.dataset.tabId);
}
});
const closeBtn = tabEl.querySelector('.tab-close');
if (closeBtn) {
closeBtn.addEventListener('click', (e) => {
e.stopPropagation();
this.closeTab(tabEl.dataset.tabId);
});
}
});
}
showPlaceholder() {
const contentArea = this.container.querySelector('#editor-content');
if (contentArea) {
contentArea.innerHTML = `
<div class="editor-placeholder">
<div class="placeholder-icon">📄</div>
<h2>No file open</h2>
<p>Select a file from the sidebar to start editing</p>
</div>
`;
}
}
saveTabsToStorage() {
const tabsData = this.tabs.map(tab => ({
path: tab.path,
name: tab.name,
dirty: tab.dirty,
active: tab.id === this.activeTab
}));
try {
sessionStorage.setItem('monaco-tabs', JSON.stringify(tabsData));
} catch (e) {
console.error('[MonacoEditor] Failed to save tabs:', e);
}
}
loadPersistedTabs() {
try {
const saved = sessionStorage.getItem('monaco-tabs');
if (saved) {
const tabsData = JSON.parse(saved);
console.log('[MonacoEditor] Restoring tabs:', tabsData);
// Note: Files will need to be reloaded from server
// This just restores the tab list structure
}
} catch (e) {
console.error('[MonacoEditor] Failed to load tabs:', e);
}
}
escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// Fallback for mobile devices
initializeFallback() {
this.setupContainer();
this.isMobile = true;
this.initialized = true;
// Add message about mobile limitation
const contentArea = this.container.querySelector('#editor-content');
if (contentArea) {
contentArea.innerHTML = `
<div class="editor-placeholder">
<div class="placeholder-icon">📱</div>
<h2>Mobile View</h2>
<p>Full code editing coming soon to mobile!</p>
<p>For now, please use a desktop or tablet device.</p>
</div>
`;
}
}
openFileFallback(filePath, content) {
// Mobile fallback - show read-only content
const contentArea = this.container.querySelector('#editor-content');
if (contentArea) {
const language = this.getLanguageFromFile(filePath);
contentArea.innerHTML = `
<div class="mobile-file-view">
<div class="file-header">
<h3>${this.escapeHtml(filePath)}</h3>
<span class="language-badge">${language}</span>
</div>
<pre class="code-content"><code>${this.escapeHtml(content || '')}</code></pre>
</div>
`;
}
}
destroy() {
// Dispose all editors and models
this.editors.forEach(editor => editor.dispose());
this.models.forEach(model => model.dispose());
this.editors.clear();
this.models.clear();
this.tabs = [];
this.activeTab = null;
}
}
// Global instance
let monacoEditor = null;
// Initialize when DOM is ready
async function initMonacoEditor() {
monacoEditor = new MonacoEditor('file-editor');
await monacoEditor.initialize();
return monacoEditor;
}
// Export to window
if (typeof window !== 'undefined') {
window.MonacoEditor = MonacoEditor;
// Auto-initialize
async function autoInit() {
try {
const editor = await initMonacoEditor();
window.monacoEditor = editor;
console.log('[MonacoEditor] Auto-initialization complete');
} catch (error) {
console.error('[MonacoEditor] Auto-initialization failed:', error);
window.monacoEditor = null;
}
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => autoInit());
} else {
autoInit();
}
}
// Export for use in other scripts
if (typeof module !== 'undefined' && module.exports) {
module.exports = { MonacoEditor };
}