// ── File Explorer Modal ── // Wiederverwendbares Modal zum Durchsuchen des Workspaces. // Nutzung: openFileExplorer({ sessionId, onSelect(path, from, to) }) let _feState = null; function _feApiUrl(action, params = '') { if (_feState && _feState.agentId) { return `/api/workspace/agent/${action}?agentId=${_feState.agentId}${params ? '&' + params : ''}`; } return `/api/workspace/${action}?sessionId=${_feState.sessionId}${params ? '&' + params : ''}`; } function openFileExplorer(opts = {}) { if (_feState) closeFileExplorer(); _feState = { sessionId: opts.sessionId || (!opts.agentId ? getCurrentSessionId() : null), agentId: opts.agentId || null, onSelect: opts.onSelect || null, currentPath: '', history: [], selectedFile: null, initialFile: opts.initialFile || null // z.B. "src/foo.cs" – Datei direkt öffnen }; const overlay = document.createElement('div'); overlay.id = 'feOverlay'; overlay.className = 'fe-overlay'; overlay.addEventListener('click', (e) => { if (e.target === overlay) closeFileExplorer(); }); const modal = document.createElement('div'); modal.className = 'fe-modal'; modal.innerHTML = `
/
Laden...
Datei auswählen
`; overlay.appendChild(modal); document.body.appendChild(overlay); document.getElementById('feBack').addEventListener('click', feGoBack); document.getElementById('feNewDir').addEventListener('click', feCreateDir); document.getElementById('feClose').addEventListener('click', closeFileExplorer); document.getElementById('feSelectBtn').addEventListener('click', feDoSelect); document.getElementById('feSaveBtn').addEventListener('click', feDoSave); // ESC _feState._keyHandler = (e) => { if (e.key === 'Escape') closeFileExplorer(); }; document.addEventListener('keydown', _feState._keyHandler); // Zum Verzeichnis der initialFile navigieren, oder Root if (_feState.initialFile) { const filePath = _feState.initialFile; const dir = filePath.includes('/') ? filePath.substring(0, filePath.lastIndexOf('/')) : ''; feLoadDir(dir); } else { feLoadDir(''); } } function closeFileExplorer() { feDestroyEditor(); const overlay = document.getElementById('feOverlay'); if (overlay) overlay.remove(); if (_feState && _feState._keyHandler) { document.removeEventListener('keydown', _feState._keyHandler); } _feState = null; } async function feLoadDir(path) { if (!_feState) return; _feState.currentPath = path; _feState.selectedFile = null; const pathEl = document.getElementById('fePath'); pathEl.textContent = '/' + (path || ''); const backBtn = document.getElementById('feBack'); backBtn.disabled = _feState.history.length === 0 && path === ''; const sidebar = document.getElementById('feSidebar'); sidebar.innerHTML = '
Laden...
'; // Preview zurücksetzen feDestroyEditor(); const main = document.getElementById('feMain'); main.innerHTML = '
Datei auswählen
'; document.getElementById('feFooter').hidden = true; try { const resp = await fetch(_feApiUrl('tree', `path=${encodeURIComponent(path)}`)); if (!resp.ok) { const err = await resp.json().catch(() => ({})); sidebar.innerHTML = `
${err.error || 'Fehler'}
`; return; } const entries = await resp.json(); sidebar.innerHTML = ''; if (path) { const upItem = document.createElement('div'); upItem.className = 'fe-item fe-dir'; upItem.innerHTML = ' ..'; upItem.addEventListener('click', () => { _feState.history.push(path); const parent = path.includes('/') ? path.substring(0, path.lastIndexOf('/')) : ''; feLoadDir(parent); }); sidebar.appendChild(upItem); } for (const entry of entries) { const item = document.createElement('div'); item.className = 'fe-item'; if (entry.type === 'dir') { item.classList.add('fe-dir'); const label = document.createElement('span'); label.className = 'fe-dir-label'; label.innerHTML = ' ' + esc(entry.name); label.addEventListener('click', () => { _feState.history.push(path); feLoadDir(entry.path); }); item.appendChild(label); const delBtn = document.createElement('button'); delBtn.className = 'fe-btn fe-dir-del'; delBtn.title = 'Ordner löschen (nur wenn leer)'; delBtn.innerHTML = ''; delBtn.addEventListener('click', (e) => { e.stopPropagation(); feDeleteDir(entry.path, entry.name); }); item.appendChild(delBtn); } else { item.classList.add('fe-file'); const icon = entry.isText ? '' : getFileIcon(entry.ext); item.innerHTML = icon + ' ' + esc(entry.name); item.dataset.path = entry.path; item.dataset.isText = entry.isText; item.addEventListener('click', () => feSelectFile(entry, item)); } sidebar.appendChild(item); } if (entries.length === 0) { sidebar.innerHTML = '
Leer
'; } backBtn.disabled = path === ''; // Wenn initialFile gesetzt, Datei automatisch selektieren if (_feState && _feState.initialFile) { const targetPath = _feState.initialFile; _feState.initialFile = null; // Nur einmal triggern // data-path Selektor sicher aufbauen const allFiles = sidebar.querySelectorAll('.fe-file'); let targetItem = null; allFiles.forEach(el => { if (el.dataset.path === targetPath) targetItem = el; }); if (targetItem) { const entry = entries.find(e => e.path === targetPath); if (entry) { feSelectFile(entry, targetItem); } } } } catch (e) { sidebar.innerHTML = '
Netzwerk-Fehler
'; } } function feGoBack() { if (!_feState || _feState.history.length === 0) { if (_feState && _feState.currentPath) feLoadDir(''); return; } const prev = _feState.history.pop(); feLoadDir(prev); } async function feSelectFile(entry, itemEl) { if (!_feState) return; // Active-State setzen document.querySelectorAll('.fe-item.active').forEach(el => el.classList.remove('active')); itemEl.classList.add('active'); const main = document.getElementById('feMain'); const footer = document.getElementById('feFooter'); const info = document.getElementById('feInfo'); // Vorherige Editor-Instanz aufräumen feDestroyEditor(); const imageExts = ['.png', '.jpg', '.jpeg', '.gif', '.webp', '.svg', '.bmp', '.ico']; if (!entry.isText && imageExts.includes((entry.ext || '').toLowerCase())) { const rawUrl = _feApiUrl('raw', `path=${encodeURIComponent(entry.path)}`); main.innerHTML = `
${esc(entry.name)}
`; footer.hidden = true; _feState.selectedFile = null; return; } if (!entry.isText) { main.innerHTML = `
${getFileIcon(entry.ext)} ${esc(entry.name)}
${formatSize(entry.size)} - Vorschau nicht verfügbar
`; footer.hidden = true; _feState.selectedFile = null; return; } main.innerHTML = '
Laden...
'; try { const resp = await fetch(_feApiUrl('file', `path=${encodeURIComponent(entry.path)}`)); if (!resp.ok) { main.innerHTML = '
Fehler beim Laden
'; return; } const data = await resp.json(); _feState.selectedFile = data; const isMarkdown = entry.ext === '.md' || entry.ext === '.markdown'; if (isMarkdown) { feInitEasyMDE(main, data); } else { feInitCodeMirror(main, data, entry.ext); } info.textContent = `${data.path} — ${data.lineCount} Zeilen, ${formatSize(data.size)}`; const saveBtn = document.getElementById('feSaveBtn'); const selectBtn = document.getElementById('feSelectBtn'); saveBtn.hidden = false; saveBtn.disabled = true; selectBtn.hidden = !_feState.onSelect; footer.hidden = false; } catch (e) { main.innerHTML = '
Netzwerk-Fehler
'; } } function feDestroyEditor() { if (_feState && _feState._editor) { if (_feState._editorType === 'easymde') { try { _feState._editor.toTextArea(); } catch (_) {} } // CodeMirror wird durch innerHTML-Reset automatisch verworfen _feState._editor = null; _feState._editorType = null; } } function feInitCodeMirror(container, data, ext) { container.innerHTML = '
'; const host = document.getElementById('feCmHost'); const mode = getCmMode(ext); _feState._originalContent = data.content; const cm = CodeMirror(host, { value: data.content, mode: mode, theme: 'default', lineNumbers: true, readOnly: false, lineWrapping: false, tabSize: 4, indentWithTabs: false, extraKeys: { 'Ctrl-S': () => feDoSave(), 'Cmd-S': () => feDoSave() } }); // Selektion-Tracking für Zeilen-Info cm.on('cursorActivity', () => { if (!_feState || !_feState.selectedFile) return; feUpdateSelectionInfo(cm, data); }); // Änderungs-Tracking cm.on('change', () => { const saveBtn = document.getElementById('feSaveBtn'); if (saveBtn) saveBtn.disabled = cm.getValue() === _feState._originalContent; }); // Refresh nötig wenn CM in einem Container ohne finale Größe initialisiert wird requestAnimationFrame(() => cm.refresh()); _feState._editor = cm; _feState._editorType = 'codemirror'; } function feInitEasyMDE(container, data) { container.innerHTML = '
'; const textarea = document.getElementById('feMdeTextarea'); _feState._originalContent = data.content; const mde = createMDE(textarea, { initialValue: data.content, toolbar: ['bold', 'italic', 'heading', '|', 'quote', 'unordered-list', 'ordered-list', '|', 'link', 'image', 'code', '|', 'preview', 'side-by-side', '|', 'guide'], minHeight: '100%', maxHeight: '100%' }); mde.codemirror.setOption('extraKeys', { 'Ctrl-S': () => feDoSave(), 'Cmd-S': () => feDoSave() }); mde.codemirror.on('cursorActivity', () => { if (!_feState || !_feState.selectedFile) return; feUpdateSelectionInfo(mde.codemirror, data); }); mde.codemirror.on('change', () => { const saveBtn = document.getElementById('feSaveBtn'); if (saveBtn) saveBtn.disabled = mde.value() === _feState._originalContent; }); _feState._editor = mde; _feState._editorType = 'easymde'; } function getCmMode(ext) { const map = { '.js': 'javascript', '.mjs': 'javascript', '.cjs': 'javascript', '.ts': 'javascript', '.jsx': 'javascript', '.tsx': 'javascript', '.json': 'javascript', '.css': 'css', '.scss': 'css', '.less': 'css', '.html': 'htmlmixed', '.htm': 'htmlmixed', '.xml': 'xml', '.svg': 'xml', '.csproj': 'xml', '.sln': 'xml', '.py': 'python', '.sh': 'shell', '.bash': 'shell', '.zsh': 'shell', '.yaml': 'yaml', '.yml': 'yaml', '.cs': 'clike', '.java': 'clike', '.cpp': 'clike', '.c': 'clike', '.h': 'clike' }; return map[ext?.toLowerCase()] || 'null'; } function feGetCm() { if (!_feState || !_feState._editor) return null; if (_feState._editorType === 'easymde') return _feState._editor.codemirror; if (_feState._editorType === 'codemirror') return _feState._editor; return null; } function feGetSelection(cm) { const sel = cm.listSelections()[0]; const anchorLine = sel.anchor.line; const headLine = sel.head.line; const anchorCh = sel.anchor.ch; const headCh = sel.head.ch; // Echte Selektion: unterschiedliche Position (line oder ch) const hasSelection = anchorLine !== headLine || anchorCh !== headCh; if (!hasSelection) return null; const fromLine = Math.min(anchorLine, headLine) + 1; const toLine = Math.max(anchorLine, headLine) + 1; return { from: fromLine, to: toLine }; } function feUpdateSelectionInfo(cm, data) { const info = document.getElementById('feInfo'); if (!info) return; const sel = feGetSelection(cm); if (sel && sel.from !== sel.to) { info.textContent = `${data.path}:${sel.from}-${sel.to} (${sel.to - sel.from + 1} Zeilen ausgewählt)`; } else if (sel) { info.textContent = `${data.path}:${sel.from} (1 Zeile ausgewählt)`; } else { info.textContent = `${data.path} — ${data.lineCount} Zeilen, ${formatSize(data.size)}`; } } function feDoSelect() { if (!_feState || !_feState.selectedFile) return; const data = _feState.selectedFile; let ref = data.path; const cm = feGetCm(); if (cm) { const sel = feGetSelection(cm); if (sel) { ref = sel.from === sel.to ? `${data.path}:${sel.from}` : `${data.path}:${sel.from}-${sel.to}`; } } if (_feState.onSelect) { _feState.onSelect(ref); } closeFileExplorer(); } async function feDoSave() { if (!_feState || !_feState.selectedFile) return; const cm = feGetCm(); if (!cm) return; const saveBtn = document.getElementById('feSaveBtn'); if (saveBtn && saveBtn.disabled) return; const content = cm.getValue(); const path = _feState.selectedFile.path; if (saveBtn) { saveBtn.disabled = true; saveBtn.textContent = 'Speichern...'; } try { const resp = await fetch( _feApiUrl('file', `path=${encodeURIComponent(path)}`), { method: 'PUT', body: content } ); if (!resp.ok) { const err = await resp.json().catch(() => ({})); alert('Fehler beim Speichern: ' + (err.error || resp.status)); if (saveBtn) { saveBtn.disabled = false; saveBtn.textContent = 'Speichern'; } return; } const result = await resp.json(); // State aktualisieren _feState.selectedFile.content = content; _feState.selectedFile.size = result.size; _feState.selectedFile.lineCount = result.lineCount; _feState._originalContent = content; const info = document.getElementById('feInfo'); if (info) info.textContent = `${path} — ${result.lineCount} Zeilen, ${formatSize(result.size)}`; if (saveBtn) { saveBtn.textContent = 'Speichern'; } // bleibt disabled, da gespeicherter Stand == aktueller Stand } catch (e) { alert('Netzwerk-Fehler beim Speichern'); if (saveBtn) { saveBtn.disabled = false; saveBtn.textContent = 'Speichern'; } } } // ── Folder Browser Mode ── // Nutzt /api/workspace/browse mit absolutem Pfad, zum Ordner-Auswählen. function openFolderBrowser(opts = {}) { if (_feState) closeFileExplorer(); _feState = { mode: 'folder', onSelect: opts.onSelect || null, currentAbsPath: opts.startPath || '/', basePath: (opts.basePath || '').replace(/\/+$/, '') || null, history: [] }; const overlay = document.createElement('div'); overlay.id = 'feOverlay'; overlay.className = 'fe-overlay'; overlay.addEventListener('click', (e) => { if (e.target === overlay) closeFileExplorer(); }); const modal = document.createElement('div'); modal.className = 'fe-modal'; modal.innerHTML = `
/
Laden...
`; overlay.appendChild(modal); document.body.appendChild(overlay); document.getElementById('feBack').addEventListener('click', fbGoBack); document.getElementById('feNewDir').addEventListener('click', fbCreateDir); document.getElementById('feClose').addEventListener('click', closeFileExplorer); document.getElementById('feSelectBtn').addEventListener('click', () => { if (_feState && _feState.onSelect) _feState.onSelect(_feState.currentAbsPath); closeFileExplorer(); }); _feState._keyHandler = (e) => { if (e.key === 'Escape') closeFileExplorer(); }; document.addEventListener('keydown', _feState._keyHandler); fbLoadDir(opts.startPath || '/'); } async function fbLoadDir(absPath) { if (!_feState) return; const pathEl = document.getElementById('fePath'); const sidebar = document.getElementById('feSidebar'); const info = document.getElementById('feInfo'); pathEl.textContent = absPath; info.textContent = absPath; sidebar.innerHTML = '
Laden...
'; try { const resp = await fetch(`/api/workspace/browse?path=${encodeURIComponent(absPath)}`); if (!resp.ok) { const err = await resp.json().catch(() => ({})); sidebar.innerHTML = `
${err.error || 'Fehler'}
`; return; } const data = await resp.json(); _feState.currentAbsPath = data.current; pathEl.textContent = data.current; info.textContent = data.current; sidebar.innerHTML = ''; for (const entry of data.entries) { const item = document.createElement('div'); item.className = 'fe-item fe-dir'; item.innerHTML = ' ' + esc(entry.name); item.addEventListener('click', () => { _feState.history.push(_feState.currentAbsPath); fbLoadDir(entry.path); }); sidebar.appendChild(item); } if (data.entries.length === 0) { sidebar.innerHTML = '
Leer
'; } const currentNorm = data.current.replace(/\/+$/, ''); const atBase = _feState.basePath && currentNorm === _feState.basePath; const atRoot = currentNorm === '' || currentNorm === '/'; document.getElementById('feBack').disabled = atBase || (atRoot && _feState.history.length === 0); } catch (e) { sidebar.innerHTML = '
Netzwerk-Fehler
'; } } function fbGoBack() { if (!_feState) return; // Nicht über den basePath hinaus navigieren const currentNorm = _feState.currentAbsPath.replace(/\/+$/, ''); if (_feState.basePath && currentNorm === _feState.basePath) return; if (_feState.history.length > 0) { const prev = _feState.history.pop(); // Prüfe ob der vorherige Pfad über dem basePath liegt if (_feState.basePath && !prev.replace(/\/+$/, '').startsWith(_feState.basePath)) { _feState.history.length = 0; fbLoadDir(_feState.basePath); return; } fbLoadDir(prev); } else { // Kein History-Eintrag → zum Parent navigieren const parent = currentNorm.includes('/') ? currentNorm.substring(0, currentNorm.lastIndexOf('/')) || '/' : '/'; if (parent !== currentNorm) { fbLoadDir(parent); } } } // ── Folder Browser: Create Directory ── async function fbCreateDir() { if (!_feState) return; const name = prompt('Ordnername:'); if (!name || !name.trim()) return; const dirName = name.trim(); if (/[\/\\:*?"<>|]/.test(dirName)) { alert('Ungültiger Ordnername'); return; } const absPath = _feState.currentAbsPath.replace(/\/$/, '') + '/' + dirName; try { const resp = await fetch(`/api/workspace/browse/mkdir?path=${encodeURIComponent(absPath)}`, { method: 'POST' }); if (!resp.ok) { const err = await resp.json().catch(() => ({})); alert(err.error || 'Fehler beim Erstellen'); return; } fbLoadDir(_feState.currentAbsPath); } catch (e) { alert('Netzwerk-Fehler'); } } // ── File Explorer: Create / Delete Directory ── async function feCreateDir() { if (!_feState) return; const name = prompt('Ordnername:'); if (!name || !name.trim()) return; const dirName = name.trim(); if (/[\/\\:*?"<>|]/.test(dirName)) { alert('Ungültiger Ordnername'); return; } const newPath = _feState.currentPath ? _feState.currentPath + '/' + dirName : dirName; try { const url = _feApiUrl('mkdir', `path=${encodeURIComponent(newPath)}`); const resp = await fetch(url, { method: 'POST' }); if (!resp.ok) { const err = await resp.json().catch(() => ({})); alert(err.error || 'Fehler beim Erstellen'); return; } feLoadDir(_feState.currentPath); } catch (e) { alert('Netzwerk-Fehler'); } } async function feDeleteDir(dirPath, dirName) { if (!_feState) return; if (!confirm(`Ordner "${dirName}" löschen?\n(Nur möglich wenn leer)`)) return; try { const url = _feApiUrl('dir', `path=${encodeURIComponent(dirPath)}`); const resp = await fetch(url, { method: 'DELETE' }); if (!resp.ok) { const err = await resp.json().catch(() => ({})); alert(err.error || 'Fehler beim Löschen'); return; } feLoadDir(_feState.currentPath); } catch (e) { alert('Netzwerk-Fehler'); } } // ── Helpers ── function getFileIcon(ext) { const icons = { '.png': '', '.jpg': '', '.jpeg': '', '.gif': '', '.webp': '', '.svg': '', '.pdf': '', '.zip': '', '.gz': '', '.tar': '', '.mp3': '', '.wav': '', '.mp4': '', '.avi': '', '.exe': '', '.dll': '', '.so': '', '.db': '', '.sqlite': '' }; return icons[ext?.toLowerCase()] || ''; } function formatSize(bytes) { if (bytes < 1024) return bytes + ' B'; if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB'; return (bytes / (1024 * 1024)).toFixed(1) + ' MB'; } function escHtml(s) { return s.replace(/&/g, '&').replace(//g, '>'); }