
(() => {
  let editorTree = null;
  let editorOpenFile = '';
  let editorLoadedText = '';
  const editorCollapsedTree = new Set();
  let editorBooted = false;

  let editorCollapsedMode = false;
  let editorOutlineMode = 'section';
  const EDITOR_OUTLINE_MODES = ['section', 'standard', 'indent'];
  const EDITOR_OUTLINE_LABELS = { section: 'Section Titles', standard: 'Standard', indent: 'Indentation' };
  let editorCollapsedBlocks = [];
  let editorCollapsedSelection = null;
  let editorCollapsedAnchor = null;
  let editorExpandedCollapsedBlock = null;
  let editorLastRootRequest = '';
  let editorContextFilePath = '';
  let editorContextFolderPath = '';
  let editorFontSize = 19;
  let editorUndoStack = [];
  let editorRedoStack = [];
  let editorUndoSnapshot = '';
  let editorFindResults = [];
  let editorProjectFindResults = [];
  const editorProjectExcludedResults = new Set();
  let editorFindHighlightQuery = '';
  const editorFindOptions = {
    file: true,
    peers: true,
    subfolders: false,
    matchCase: true,
    wholeWord: false,
    fileFilterMode: 'include',
    folderFilterMode: 'exclude',
    filetypeFilterMode: 'include',
    files: new Set(),
    folders: new Set(),
    extensions: new Set()
  };


  function byId(id) {
    return document.getElementById(id);
  }

  let editorRuntimeRootLabel = 'workspace';

  function getEditorRootRequest() {
    return '';
  }

  function getEditorRootLabel() {
    return editorRuntimeRootLabel || 'workspace';
  }

  function setEditorStatus(text) {
    const el = byId('editor-file-status');
    if (el) el.textContent = text;
  }

  function currentEditorText() {
    return byId('editor-textarea')?.value || '';
  }

  function editorIsDirty() {
    return !!editorOpenFile && currentEditorText() !== editorLoadedText;
  }

  function escapeCodeHtml(text) {
    return String(text ?? '')
      .replace(/&/g, '&amp;')
      .replace(/</g, '&lt;')
      .replace(/>/g, '&gt;')
      .replace(/"/g, '&quot;');
  }

  function escapeRegExp(text) {
    return String(text ?? '').replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
  }

  function escapeEditorAttr(text) {
    return String(text ?? '')
      .replace(/&/g, '&amp;')
      .replace(/"/g, '&quot;')
      .replace(/</g, '&lt;')
      .replace(/>/g, '&gt;');
  }

  function editorIsWordChar(ch) {
    return /[A-Za-z0-9_$]/.test(String(ch || ''));
  }

  function editorWholeWordAllowed(source, index, length) {
    if (!editorFindOptions.wholeWord) return true;
    return !editorIsWordChar(source[index - 1]) && !editorIsWordChar(source[index + length]);
  }

  function findEditorMatchIndexes(text, needle) {
    const source = String(text || '');
    const query = String(needle || '');
    if (!query) return [];

    const haystack = editorFindOptions.matchCase ? source : source.toLowerCase();
    const target = editorFindOptions.matchCase ? query : query.toLowerCase();
    const indexes = [];
    let searchIndex = 0;

    while (searchIndex <= haystack.length) {
      const index = haystack.indexOf(target, searchIndex);
      if (index < 0) break;
      if (editorWholeWordAllowed(source, index, query.length)) indexes.push(index);
      searchIndex = index + Math.max(target.length, 1);
    }

    return indexes;
  }

  function markEditorFindHtml(escapedText) {
    const query = String(editorFindHighlightQuery || '');
    if (!query) return escapedText;
    const escapedQuery = escapeCodeHtml(query);
    if (!escapedQuery) return escapedText;

    const haystack = editorFindOptions.matchCase ? escapedText : escapedText.toLowerCase();
    const target = editorFindOptions.matchCase ? escapedQuery : escapedQuery.toLowerCase();
    let out = '';
    let cursor = 0;

    while (cursor <= escapedText.length) {
      const index = haystack.indexOf(target, cursor);
      if (index < 0) break;
      out += escapedText.slice(cursor, index);
      out += `<mark class="editor-find-match-mark">${escapedText.slice(index, index + escapedQuery.length)}</mark>`;
      cursor = index + escapedQuery.length;
    }

    return out + escapedText.slice(cursor);
  }

  const EDITOR_BLOCKED_EXTENSIONS = new Set([
    '.png', '.jpg', '.jpeg', '.gif', '.webp', '.bmp', '.ico', '.svg',
    '.mp4', '.mov', '.avi', '.mkv', '.webm', '.mp3', '.wav', '.ogg',
    '.zip', '.rar', '.7z', '.tar', '.gz', '.exe', '.dll', '.bin', '.dat',
    '.pdf', '.woff', '.woff2', '.ttf', '.otf'
  ]);

  function editorFileExtension(filePath) {
    const clean = String(filePath || '').split('?')[0].split('#')[0].toLowerCase();
    const name = clean.split('/').pop() || '';
    const dot = name.lastIndexOf('.');
    return dot >= 0 ? name.slice(dot) : '';
  }

  function isEditorEditableFile(filePath) {
    const ext = editorFileExtension(filePath);
    if (!ext) return true;
    return !EDITOR_BLOCKED_EXTENSIONS.has(ext);
  }

  function showEditorNonEditableFile(filePath) {
    editorOpenFile = '';
    editorLoadedText = '';
    editorCollapsedMode = false;
    editorCollapsedSelection = null;
    editorCollapsedAnchor = null;
    editorExpandedCollapsedBlock = null;
    const textarea = byId('editor-textarea');
    if (textarea) textarea.value = '';
    const code = byId('editor-highlight-code');
    if (code) code.textContent = `Not editable in Verbatim editor:\n${filePath}\n\nBinary and media files are blocked to prevent freezing.`;
    setEditorStatus('Not editable · binary/media file blocked');
    toast('Binary/media file blocked from editor');
    renderEditorTree();
    refreshEditorHeader();
  }

  function detectEditorLanguage(filePath) {
    const p = String(filePath || '').toLowerCase();
    if (p.endsWith('.ts') || p.endsWith('.tsx') || p.endsWith('.js') || p.endsWith('.jsx')) return 'script';
    if (p.endsWith('.json')) return 'json';
    if (p.endsWith('.html') || p.endsWith('.htm')) return 'html';
    if (p.endsWith('.css')) return 'css';
    return 'plain';
  }

  function glyphize(value) {
    const escaped = escapeCodeHtml(value);
    if (editorFindHighlightQuery) return markEditorFindHtml(escaped);
    return escaped.replace(/\b([A-F][xo]\d*)\b/g, '<span class="tok-glyph">$1</span>');
  }

  function wrapEditorToken(className, value) {
    return `<span class="${className}">${glyphize(value)}</span>`;
  }

  function highlightEditorCode(text, language) {
    const source = String(text ?? '');

    const wrap = wrapEditorToken;

    const paint = (pattern, classify) => {
      let out = '';
      let last = 0;
      pattern.lastIndex = 0;
      let match;

      while ((match = pattern.exec(source))) {
        if (match.index > last) out += glyphize(source.slice(last, match.index));
        const value = match[0];
        out += wrap(classify(value, match), value);
        last = match.index + value.length;
        if (!value.length) pattern.lastIndex += 1;
      }

      if (last < source.length) out += glyphize(source.slice(last));
      return out || ' ';
    };

    if (language === 'html') {
      return highlightHtmlCode(source);
    }

    if (language === 'script') {
      const scriptPattern = /\/\/.*?$|\/\*[\s\S]*?\*\/|(["'`])(?:\\.|(?!\1)[\s\S])*\1|\b(const|let|var|function|return|if|else|for|while|switch|case|break|continue|try|catch|finally|throw|new|await|async|import|from|export|default|class|extends|implements|type|interface)\b|\b(string|number|boolean|void|any|unknown|never|object|Promise|Record|Array)\b|\b\d+(?:\.\d+)?\b|[{}()[\];,.]/gm;
      return paint(scriptPattern, value => {
        if (value.startsWith('//') || value.startsWith('/*')) return 'tok-comment';
        if (/^["'`]/.test(value)) return 'tok-string';
        if (/^(string|number|boolean|void|any|unknown|never|object|Promise|Record|Array)$/.test(value)) return 'tok-type';
        if (/^\d/.test(value)) return 'tok-number';
        if (/^[{}()[\];,.]$/.test(value)) return 'tok-punct';
        return 'tok-keyword';
      });
    }

    if (language === 'json') {
      const jsonPattern = /"(?:\\.|[^"\\])*"(?=\s*:)|"(?:\\.|[^"\\])*"|\b(true|false|null)\b|\b\d+(?:\.\d+)?\b|[{}\[\],:]/g;
      return paint(jsonPattern, value => {
        if (/^"/.test(value) && /"$/.test(value)) return 'tok-string';
        if (/^(true|false|null)$/.test(value)) return 'tok-keyword';
        if (/^\d/.test(value)) return 'tok-number';
        return 'tok-punct';
      });
    }

    if (language === 'css') {
      const cssPattern = /\/\*[\s\S]*?\*\/|"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|#[a-fA-F0-9]{3,8}\b|\b\d+(?:\.\d+)?(?:px|rem|em|vh|vw|%)?\b|[{}:;,.()]/g;
      return paint(cssPattern, value => {
        if (value.startsWith('/*')) return 'tok-comment';
        if (/^["']/.test(value)) return 'tok-string';
        if (/^#/.test(value) || /^\d/.test(value)) return 'tok-number';
        return 'tok-punct';
      });
    }

    return glyphize(source) || ' ';
  }
  function highlightHtmlCode(text) {
    const source = String(text ?? '');
    const tagPattern = /<!--[\s\S]*?-->|<\/?[a-zA-Z][^>]*?>/g;
    let out = '';
    let last = 0;
    let match;

    while ((match = tagPattern.exec(source))) {
      if (match.index > last) out += glyphize(source.slice(last, match.index));
      const value = match[0];
      out += value.startsWith('<!--') ? wrapEditorToken('tok-comment', value) : highlightHtmlTag(value);
      last = match.index + value.length;
    }

    if (last < source.length) out += glyphize(source.slice(last));
    return out || ' ';
  }

  function highlightHtmlTag(tag) {
    const source = String(tag ?? '');
    const partPattern = /<\/?|\/?>|=|([A-Za-z_:][-A-Za-z0-9_:.]*)(?=\s*=)|(["'])(?:\\.|(?!\2)[\s\S])*\2|[A-Za-z][A-Za-z0-9:-]*/g;
    let out = '';
    let last = 0;
    let match;

    while ((match = partPattern.exec(source))) {
      if (match.index > last) out += glyphize(source.slice(last, match.index));
      const value = match[0];
      if (/^<\/?$|^\/?>$/.test(value) || value === '=') out += wrapEditorToken('tok-punct', value);
      else if (/^["']/.test(value)) out += wrapEditorToken('tok-string', value);
      else if (match[1]) out += wrapEditorToken('tok-attr', value);
      else out += wrapEditorToken('tok-tag', value);
      last = match.index + value.length;
      if (!value.length) partPattern.lastIndex += 1;
    }

    if (last < source.length) out += glyphize(source.slice(last));
    return out;
  }

  function syncEditorHighlight() {
    const textarea = byId('editor-textarea');
    const highlight = byId('editor-highlight');
    const code = byId('editor-highlight-code');
    if (!textarea || !highlight || !code || editorCollapsedMode) return;
    refreshEditorFindOptions();
    const bar = byId('editor-findbar');
    editorFindHighlightQuery = bar && !bar.hidden ? String(byId('editor-find-input')?.value || '') : '';
    code.innerHTML = highlightEditorCode(textarea.value, detectEditorLanguage(editorOpenFile));
    highlight.scrollTop = textarea.scrollTop;
    highlight.scrollLeft = textarea.scrollLeft;
  }

  function setEditorTextareaValue(text, opts = {}) {
    const textarea = byId('editor-textarea');
    if (!textarea) return;
    if (!opts.skipUndo) pushEditorUndoSnapshot();
    textarea.value = String(text ?? '');
    refreshEditorHeader();
    updateCollapsedBlocks();
    if (editorCollapsedMode) renderCollapsedView();
  }

  function pushEditorUndoSnapshot() {
    const text = currentEditorText();
    if (editorUndoStack[editorUndoStack.length - 1] === text) return;
    editorUndoStack.push(text);
    if (editorUndoStack.length > 100) editorUndoStack.shift();
    editorRedoStack = [];
  }

  function markEditorUndoBoundary() {
    const text = currentEditorText();
    if (editorUndoSnapshot !== text) {
      editorUndoStack.push(editorUndoSnapshot);
      if (editorUndoStack.length > 100) editorUndoStack.shift();
      editorRedoStack = [];
      editorUndoSnapshot = text;
    }
  }

  function undoEditorChange() {
    const textarea = byId('editor-textarea');
    if (!textarea || !editorUndoStack.length) return;
    const current = textarea.value;
    const previous = editorUndoStack.pop();
    editorRedoStack.push(current);
    textarea.value = previous;
    editorUndoSnapshot = previous;
    refreshEditorHeader();
    updateCollapsedBlocks();
    if (editorCollapsedMode) renderCollapsedView();
    toast('Undo');
  }

  function redoEditorChange() {
    const textarea = byId('editor-textarea');
    if (!textarea || !editorRedoStack.length) return;
    const current = textarea.value;
    const next = editorRedoStack.pop();
    editorUndoStack.push(current);
    textarea.value = next;
    editorUndoSnapshot = next;
    refreshEditorHeader();
    updateCollapsedBlocks();
    if (editorCollapsedMode) renderCollapsedView();
    toast('Redo');
  }

  function adjustEditorFontSize(delta) {
    editorFontSize = Math.max(9, Math.min(28, editorFontSize + delta));
    const stage = byId('editor-stage');
    const textarea = byId('editor-textarea');
    const highlight = byId('editor-highlight');
    const collapsed = byId('editor-collapsed-view');
    [stage, textarea, highlight, collapsed].forEach(el => {
      if (el) el.style.fontSize = `${editorFontSize}px`;
    });
    localStorage.setItem('nodarama:editorFontSize', String(editorFontSize));
  }

  function restoreEditorFontSize() {
    const saved = Number(localStorage.getItem('nodarama:editorFontSize'));
    if (Number.isFinite(saved)) editorFontSize = Math.max(9, Math.min(28, saved));
    adjustEditorFontSize(0);
  }

  function setCollapsedMode(next) {
    editorCollapsedMode = Boolean(next && editorCollapsedBlocks.length);
    const stage = byId('editor-stage');
    if (stage) stage.classList.toggle('collapsed-mode', editorCollapsedMode);
    const collapsed = byId('editor-collapsed-view');
    if (collapsed) collapsed.hidden = !editorCollapsedMode;
    if (!editorCollapsedMode) syncEditorHighlight();
    refreshEditorHeader();
  }

  function refreshEditorHeader() {
    const rootEl = byId('editor-root-label');
    const pathEl = byId('editor-file-path');
    if (rootEl) rootEl.textContent = getEditorRootLabel();
    if (pathEl) pathEl.textContent = editorOpenFile || 'No file selected';

    if (!editorOpenFile) setEditorStatus('No file selected');
    else if (editorCollapsedMode) setEditorStatus(`Collapsed outline · ${editorCollapsedBlocks.length} editable blocks`);
    else if (editorIsDirty()) setEditorStatus('Unsaved changes');
    else setEditorStatus('Saved');

    syncEditorHighlight();
  }

  function normalizeEditorPath(value) {
    const raw = String(value || '').trim().replace(/\\/g, '/');
    if (!raw) return '';
    if (raw.startsWith('./') || raw.startsWith('/') || /^[A-Za-z]:\//.test(raw)) return raw;
    if (raw.startsWith('projects/')) return `./${raw}`;
    return raw;
  }

  function canonicalEditorFileSegment(value) {
    return String(value || '')
      .trim()
      .toLowerCase()
      .replace(/[^a-z0-9._-]+/g, '_')
      .replace(/^_+|_+$/g, '');
  }

  function buildCanonicalEditorNewFilePath(rawPath) {
    const input = String(rawPath || '').trim().replace(/\\/g, '/');
    if (!input || input.startsWith('/') || /^[A-Za-z]:\//.test(input) || input.split('/').includes('..')) return '';

    const relative = input
      .replace(/^\.\//, '')
      .split('/')
      .map(canonicalEditorFileSegment)
      .filter(Boolean)
      .join('/');

    if (!relative) return '';

    const root = getEditorRootRequest().replace(/[\\/]+$/, '');
    return normalizeEditorPath(`${root}/${relative}`);
  }

  function normalizeEditorChildNode(parentPath, child) {
    const parent = normalizeEditorPath(parentPath).replace(/\/$/, '');
    const childPath = normalizeEditorPath(child?.path || '');
    const childName = String(child?.name || '').trim();
    let nextPath = childPath;

    if (!nextPath && childName) nextPath = `${parent}/${childName}`;
    else if (childName && nextPath === childName) nextPath = `${parent}/${childName}`;
    else if (nextPath && !nextPath.startsWith('./') && !nextPath.startsWith('/') && !/^[A-Za-z]:\//.test(nextPath)) nextPath = normalizeEditorPath(nextPath);

    return { ...child, path: nextPath };
  }

  function renderEditorTreeNode(node) {
    const wrap = document.createElement('div');
    wrap.className = 'editor-tree-node' + (editorCollapsedTree.has(node.path) ? ' collapsed' : '');

    const row = document.createElement('div');
    row.className = 'editor-tree-row' + (node.path === editorOpenFile ? ' active' : '');

    const caret = document.createElement('span');
    caret.className = 'editor-tree-caret';
    caret.textContent = node.type === 'dir' ? (editorCollapsedTree.has(node.path) ? '+' : '-') : '';

    const type = document.createElement('span');
    type.className = 'editor-tree-type';
    type.textContent = node.type === 'dir' ? '□' : '·';

    const name = document.createElement('span');
    name.className = 'editor-tree-name';
    name.textContent = node.name;

    row.appendChild(caret);
    row.appendChild(type);
    row.appendChild(name);
    wrap.appendChild(row);

    if (node.type === 'dir') {
      row.addEventListener('contextmenu', event => openEditorContextMenu(event, '', node.path));

      caret.addEventListener('click', async event => {
        event.stopPropagation();
        const isCollapsed = editorCollapsedTree.has(node.path);

        if (node.lazy && isCollapsed) {
          const data = await api('/read/tree', 'POST', { root: node.path });
          if (!data?.ok || !data.root) {
            console.warn('[editor-tree] lazy folder failed', node.path, data);
            toast(`Unable to load folder: ${node.name || node.path}`);
            return;
          }
          node.children = Array.isArray(data.root.children)
            ? data.root.children.map(child => {
                const normalized = normalizeEditorChildNode(node.path, child);
                return {
                  ...normalized,
                  lazy: normalized?.type === 'dir' && (!Array.isArray(normalized.children) || !normalized.children.length)
                };
              })
            : [];

          node.lazy = false;
          for (const child of node.children || []) {
            if (child?.type === 'dir') editorCollapsedTree.add(child.path);
          }
        }

        if (isCollapsed) editorCollapsedTree.delete(node.path);
        else editorCollapsedTree.add(node.path);
        renderEditorTree();
      });

      const children = document.createElement('div');
      children.className = 'editor-tree-children';
      for (const child of node.children || []) children.appendChild(renderEditorTreeNode(child));
      wrap.appendChild(children);
    } else {
      row.addEventListener('click', () => openEditorFile(node.path));
      row.addEventListener('contextmenu', event => openEditorContextMenu(event, node.path));
    }

    return wrap;
  }

  function renderEditorTree() {
    const tree = byId('editor-tree');
    if (!tree) return;
    tree.innerHTML = '';

    if (!editorTree) {
      tree.innerHTML = '<div style="color:var(--text3);padding:6px">Load the project tree to begin.</div>';
      return;
    }

    if (editorTree.path === '__editor_projects__') {
      const header = document.createElement('div');
      header.className = 'editor-tree-root-heading';
      header.textContent = 'Projects';
      tree.appendChild(header);
      for (const child of editorTree.children || []) tree.appendChild(renderEditorTreeNode(child));
      return;
    }

    tree.appendChild(renderEditorTreeNode(editorTree));
  }

  function openEditorContextMenu(event, filePath = '', folderPath = '', opts = {}) {
    event?.preventDefault?.();
    event?.stopPropagation?.();

    const existing = byId('editor-context-menu');
    if (existing) existing.remove();

    editorContextFilePath = normalizeEditorPath(filePath || '');
    editorContextFolderPath = normalizeEditorPath(folderPath || '');

    const menu = document.createElement('div');
    menu.id = 'editor-context-menu';
    menu.style.position = 'fixed';
    menu.style.left = `${Math.max(8, event?.clientX || 8)}px`;
    menu.style.top = `${Math.max(8, event?.clientY || 8)}px`;
    menu.style.zIndex = '12000';
    menu.style.minWidth = '190px';
    menu.style.padding = '6px';
    menu.style.border = '1px solid rgba(120,170,220,.42)';
    menu.style.borderRadius = '8px';
    menu.style.background = 'rgba(15,18,28,.96)';
    menu.style.boxShadow = '0 14px 34px rgba(0,0,0,.42)';
    menu.style.color = 'var(--text,#eef)';

    const addItem = (label, fn) => {
      const btn = document.createElement('button');
      btn.type = 'button';
      btn.textContent = label;
      btn.style.display = 'block';
      btn.style.width = '100%';
      btn.style.margin = '2px 0';
      btn.style.padding = '7px 8px';
      btn.style.textAlign = 'left';
      btn.style.border = '1px solid rgba(255,255,255,.12)';
      btn.style.borderRadius = '6px';
      btn.style.background = 'rgba(255,255,255,.05)';
      btn.style.color = 'inherit';
      btn.addEventListener('click', async clickEvent => {
        clickEvent.preventDefault();
        clickEvent.stopPropagation();
        menu.remove();
        await fn?.();
      });
      menu.appendChild(btn);
    };

    if (opts.findResult) {
      addItem('GOTO FILE/CODE', () => gotoEditorFindResult(opts.findResult));
    } else if (editorContextFilePath) {
      addItem('GOTO FILE/CODE', () => openEditorFile(editorContextFilePath));
    } else if (editorContextFolderPath) {
      addItem(`Folder: ${editorContextFolderPath}`, () => editorToast(editorContextFolderPath));
    } else {
      addItem('Editor context', () => editorToast('Editor context'));
    }

    document.body.appendChild(menu);
    const close = closeEvent => {
      if (!menu.contains(closeEvent.target)) {
        menu.remove();
        document.removeEventListener('mousedown', close, true);
      }
    };
    setTimeout(() => document.addEventListener('mousedown', close, true), 0);
  }

  function getLineOffsets(text) {
    const offsets = [0];
    for (let i = 0; i < text.length; i++) if (text[i] === '\n') offsets.push(i + 1);
    return offsets;
  }

  function indexToLineNumber(offsets, index) {
    let low = 0;
    let high = offsets.length - 1;
    while (low <= high) {
      const mid = Math.floor((low + high) / 2);
      if (offsets[mid] <= index) low = mid + 1;
      else high = mid - 1;
    }
    return Math.max(1, high + 1);
  }

  function scanBlockEnd(text, openIndex) {
    let depth = 0;
    let quote = '';
    let inLineComment = false;
    let inBlockComment = false;
    let escaped = false;

    for (let i = openIndex; i < text.length; i++) {
      const ch = text[i];
      const next = text[i + 1];

      if (inLineComment) {
        if (ch === '\n') inLineComment = false;
        continue;
      }
      if (inBlockComment) {
        if (ch === '*' && next === '/') {
          inBlockComment = false;
          i++;
        }
        continue;
      }
      if (quote) {
        if (escaped) {
          escaped = false;
          continue;
        }
        if (ch === '\\') {
          escaped = true;
          continue;
        }
        if (ch === quote) quote = '';
        continue;
      }
      if (ch === '/' && next === '/') {
        inLineComment = true;
        i++;
        continue;
      }
      if (ch === '/' && next === '*') {
        inBlockComment = true;
        i++;
        continue;
      }
      if (ch === '"' || ch === '\'' || ch === '`') {
        quote = ch;
        continue;
      }
      if (ch === '{') depth++;
      else if (ch === '}') {
        depth--;
        if (depth === 0) {
          let end = i + 1;
          while (end < text.length && /[;\s]/.test(text[end])) end++;
          return end;
        }
      }
    }
    return -1;
  }

  function makeCollapsedBlock(signature, startIndex, endIndex, offsets, extra = {}) {
    return {
      index: 0,
      startIndex,
      endIndex,
      startLine: indexToLineNumber(offsets, startIndex),
      signature,
      ...extra
    };
  }

  function finalizeCollapsedBlocks(blocks) {
    return blocks
      .filter(block => block && Number.isFinite(block.startIndex) && Number.isFinite(block.endIndex) && block.endIndex > block.startIndex)
      .sort((a, b) => a.startIndex - b.startIndex)
      .map((block, index) => ({ ...block, index }));
  }

  function collapsedLineHasContent(text, startIndex, endIndex) {
    return String(text || '').slice(startIndex, endIndex).trim().length > 0;
  }

  function nameScriptBlock(line) {
    const clean = String(line || '').trim();
    const functionName = clean.match(/(?:export\s+)?(?:async\s+)?function\s+([A-Za-z_$][\w$]*)/)?.[1];
    if (functionName) return `function/${functionName}`;
    const className = clean.match(/(?:export\s+)?class\s+([A-Za-z_$][\w$]*)/)?.[1];
    if (className) return `class/${className}`;
    const arrowName = clean.match(/(?:export\s+)?(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=/)?.[1];
    if (arrowName) return `function/${arrowName}`;
    return '(script)';
  }

  function analyzeScriptBlocks(text, filePath) {
    const source = String(text || '');
    const lines = source.split('\n');
    const offsets = getLineOffsets(source);
    const named = [];
    const patterns = [
      /^\s*export\s+async\s+function\s+[A-Za-z_$][\w$]*/,
      /^\s*export\s+function\s+[A-Za-z_$][\w$]*/,
      /^\s*async\s+function\s+[A-Za-z_$][\w$]*/,
      /^\s*function\s+[A-Za-z_$][\w$]*/,
      /^\s*export\s+class\s+[A-Za-z_$][\w$]*/,
      /^\s*class\s+[A-Za-z_$][\w$]*/,
      /^\s*export\s+(const|let|var)\s+[A-Za-z_$][\w$]*\s*=\s*(async\s*)?(?:\([^)]*\)|[A-Za-z_$][\w$]*)\s*=>/,
      /^\s*(const|let|var)\s+[A-Za-z_$][\w$]*\s*=\s*(async\s*)?(?:\([^)]*\)|[A-Za-z_$][\w$]*)\s*=>/
    ];

    let nextAllowedLine = 0;
    for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) {
      if (lineIndex < nextAllowedLine) continue;
      const line = lines[lineIndex];
      if (/^\s/.test(line)) continue;
      if (!patterns.some(pattern => pattern.test(line))) continue;

      const startIndex = offsets[lineIndex];
      const braceIndex = source.indexOf('{', startIndex);
      if (braceIndex < 0) continue;
      const endIndex = scanBlockEnd(source, braceIndex);
      if (endIndex < 0) continue;

      nextAllowedLine = indexToLineNumber(offsets, endIndex) - 1;
      named.push(makeCollapsedBlock(nameScriptBlock(line), startIndex, endIndex, offsets));
    }

    if (!named.length) {
      return source.trim() ? finalizeCollapsedBlocks([makeCollapsedBlock('section/script', 0, source.length, offsets)]) : [];
    }

    const blocks = [];
    const first = named[0];
    const last = named[named.length - 1];

    if (collapsedLineHasContent(source, 0, first.startIndex)) {
      blocks.push(makeCollapsedBlock('section/imports', 0, first.startIndex, offsets));
    }

    named.forEach((block, i) => {
      blocks.push(block);
      const next = named[i + 1];
      const gapStart = block.endIndex;
      const gapEnd = next ? next.startIndex : last.endIndex;
      if (next && collapsedLineHasContent(source, gapStart, gapEnd)) {
        blocks.push(makeCollapsedBlock('section/script', gapStart, gapEnd, offsets));
      }
    });
    if (collapsedLineHasContent(source, last.endIndex, source.length)) {
      blocks.push(makeCollapsedBlock('section/trailing', last.endIndex, source.length, offsets));
    }

    return finalizeCollapsedBlocks(blocks);
  }

  function compactSelectorLabel(selector) {
    return String(selector || '')
      .replace(/\/\*[\s\S]*?\*\//g, '')
      .replace(/\s+/g, ' ')
      .trim()
      .slice(0, 120) || 'CSS Block';
  }

  function analyzeCssBlocks(text) {
    const source = String(text || '');
    const offsets = getLineOffsets(source);
    const blocks = [];
    const named = [];
    let searchIndex = 0;

    while (searchIndex < source.length) {
      const openIndex = source.indexOf('{', searchIndex);
      if (openIndex < 0) break;
      const lineStart = source.lastIndexOf('\n', openIndex) + 1;
      const selectorStart = Math.max(source.lastIndexOf('}', openIndex) + 1, lineStart);
      const selector = source.slice(selectorStart, openIndex).trim();
      const endIndex = scanBlockEnd(source, openIndex);
      if (!selector || endIndex < 0) { searchIndex = openIndex + 1; continue; }
      named.push(makeCollapsedBlock(compactSelectorLabel(selector), selectorStart, endIndex, offsets));
      searchIndex = endIndex;
    }

    if (!named.length) {
      return source.trim() ? finalizeCollapsedBlocks([makeCollapsedBlock('section/script', 0, source.length, offsets)]) : [];
    }

    const first = named[0];
    const last = named[named.length - 1];
    if (collapsedLineHasContent(source, 0, first.startIndex)) blocks.push(makeCollapsedBlock('section/imports', 0, first.startIndex, offsets));
    blocks.push(...named);
    if (collapsedLineHasContent(source, last.endIndex, source.length)) blocks.push(makeCollapsedBlock('section/trailing', last.endIndex, source.length, offsets));
    return finalizeCollapsedBlocks(blocks);
  }
  function htmlElementLabel(rawTag) {
    const tagText = String(rawTag || '');
    const tag = tagText.match(/^<\s*([a-zA-Z0-9:-]+)/)?.[1] || 'element';
    const id = tagText.match(/\sid\s*=\s*["']([^"']+)["']/i)?.[1];
    const className = tagText.match(/\sclass\s*=\s*["']([^"']+)["']/i)?.[1];
    if (id) return `section/${id}`;
    if (className) return `section/${String(className).trim().split(/\s+/)[0]}`;
    return `section/${tag}`;
  }

  function analyzeHtmlBlocks(text) {
    const source = String(text || '');
    const offsets = getLineOffsets(source);
    const tagPattern = /<\/?[a-zA-Z][^>]*?>/g;
    const stack = [];
    const blocks = [];
    const voidTags = new Set(['area','base','br','col','embed','hr','img','input','link','meta','param','source','track','wbr']);
    let match;

    while ((match = tagPattern.exec(source))) {
      const raw = match[0];
      if (/^<\//.test(raw)) {
        const closing = raw.match(/^<\/\s*([a-zA-Z0-9:-]+)/)?.[1]?.toLowerCase();
        for (let i = stack.length - 1; i >= 0; i--) {
          if (stack[i].tag === closing) {
            const item = stack.splice(i, 1)[0];
            const endIndex = tagPattern.lastIndex;
            if (item.depth <= 3) blocks.push(makeCollapsedBlock(item.label, item.startIndex, endIndex, offsets));
            break;
          }
        }
        continue;
      }

      const tag = raw.match(/^<\s*([a-zA-Z0-9:-]+)/)?.[1]?.toLowerCase();
      if (!tag || raw.endsWith('/>') || voidTags.has(tag)) continue;
      stack.push({ tag, startIndex: match.index, depth: stack.length + 1, label: htmlElementLabel(raw) });
    }

    for (const item of stack) {
      if (item.depth <= 3) blocks.push(makeCollapsedBlock(item.label, item.startIndex, source.length, offsets));
    }

    return finalizeCollapsedBlocks(blocks.length ? blocks : (source.trim() ? [makeCollapsedBlock('section/script', 0, source.length, offsets)] : []));
  }

  function cleanEditorSectionTitle(line) {
    const raw = String(line || '').trim();
    if (!raw) return '';
    const cleaned = raw
      .replace(/^\/\/\s?/, '')
      .replace(/^#\s?/, '')
      .replace(/^\/\*+\s?/, '')
      .replace(/^\*\s?/, '')
      .replace(/\s?\*+\/$/, '')
      .replace(/^<!--\s?/, '')
      .replace(/\s?-->$/, '')
      .replace(/^[-=]{3,}\s?/, '')
      .replace(/\s?[-=]{3,}$/, '')
      .trim();
    return cleaned.slice(0, 96);
  }

  function isEditorSectionTitleLine(line) {
    const raw = String(line || '');
    const trimmed = raw.trim();
    if (!trimmed) return false;
    if (/^(\/\/|#|\/\*+|\*|<!--)\s*\S/.test(trimmed)) return !!cleanEditorSectionTitle(trimmed);
    if (/^[-=]{3,}\s*\S/.test(trimmed)) return true;
    return false;
  }

  function analyzeSectionTitleBlocks(text) {
    const source = String(text || '');
    const lines = source.split('\n');
    const offsets = getLineOffsets(source);
    const anchors = source.trim() ? [{ lineIndex: 0, startIndex: 0, label: 'start' }] : [];
    let codeLinesSinceAnchor = 0;
    let previousCandidateWasTitle = false;

    const isRealCodeLine = line => {
      const trimmed = String(line || '').trim();
      if (!trimmed) return false;
      if (isEditorSectionTitleLine(line)) return false;
      if (/^(\/\/|#|\/\*+|\*|<!--|-->|\*\/)/.test(trimmed)) return false;
      return true;
    };

    lines.forEach((line, lineIndex) => {
      const isTitle = isEditorSectionTitleLine(line);

      if (isTitle) {
        const label = cleanEditorSectionTitle(line) || `line ${lineIndex + 1}`;
        if (!previousCandidateWasTitle && codeLinesSinceAnchor >= 5) {
          anchors.push({ lineIndex, startIndex: offsets[lineIndex], label });
          codeLinesSinceAnchor = 0;
        }
        previousCandidateWasTitle = true;
        return;
      }

      previousCandidateWasTitle = false;
      if (isRealCodeLine(line)) codeLinesSinceAnchor += 1;
    });

    if (anchors.length < 2) return [];

    const blocks = anchors.map((anchor, i) => {
      const next = anchors[i + 1];
      return makeCollapsedBlock(`section/${anchor.label}`, anchor.startIndex, next ? next.startIndex : source.length, offsets);
    });

    return finalizeCollapsedBlocks(blocks);
  }

  function analyzeIndentBlocks(text) {
    const source = String(text || '');
    const lines = source.split('\n');
    const offsets = getLineOffsets(source);
    const anchors = [];

    lines.forEach((line, lineIndex) => {
      if (!line.trim()) return;
      if (/^\s/.test(line)) return;
      anchors.push({ lineIndex, startIndex: offsets[lineIndex], label: line.trim().slice(0, 96) });
    });

    if (anchors.length < 2) return [];

    const blocks = anchors.map((anchor, i) => {
      const next = anchors[i + 1];
      return makeCollapsedBlock(`indent/${anchor.label}`, anchor.startIndex, next ? next.startIndex : source.length, offsets);
    });

    return finalizeCollapsedBlocks(blocks);
  }

  function analyzeTopLevelBlocks(text, filePath) {
    const language = detectEditorLanguage(filePath);
    const standardBlocks = () => {
      if (language === 'script') return analyzeScriptBlocks(text, filePath);
      if (language === 'css') return analyzeCssBlocks(text);
      if (language === 'html') return analyzeHtmlBlocks(text);
      const source = String(text || '');
      const offsets = getLineOffsets(source);
      return source.trim() ? finalizeCollapsedBlocks([makeCollapsedBlock('section/script', 0, source.length, offsets)]) : [];
    };

    if (editorOutlineMode === 'section') {
      const sectionBlocks = analyzeSectionTitleBlocks(text);
      return sectionBlocks.length ? sectionBlocks : standardBlocks();
    }

    if (editorOutlineMode === 'indent') {
      const indentBlocks = analyzeIndentBlocks(text);
      return indentBlocks.length ? indentBlocks : standardBlocks();
    }

    return standardBlocks();
  }

  function updateCollapsedBlocks() {
    editorCollapsedBlocks = analyzeTopLevelBlocks(currentEditorText(), editorOpenFile);
    if (!editorCollapsedBlocks.length) {
      editorExpandedCollapsedBlock = null;
      editorCollapsedSelection = null;
      editorCollapsedAnchor = null;
      setCollapsedMode(false);
    } else if (editorCollapsedSelection?.start >= editorCollapsedBlocks.length) {
      editorCollapsedSelection = { start: editorCollapsedBlocks.length - 1, end: editorCollapsedBlocks.length - 1 };
      editorCollapsedAnchor = editorCollapsedBlocks.length - 1;
    }
    refreshSectionMenu();
  }

  function syncEditorSectionControl() {
    const select = byId('editor-section-select');
    if (!select) return;
    const selected = editorCollapsedSelection?.start;
    select.value = selected == null ? '' : String(selected);
  }

  function setEditorBlockSelection(index, opts = {}) {
    if (!editorCollapsedBlocks.length) return;
    const next = Math.max(0, Math.min(editorCollapsedBlocks.length - 1, Number(index) || 0));

    if (opts.extend && editorCollapsedAnchor != null) {
      editorCollapsedSelection = { start: editorCollapsedAnchor, end: next };
    } else {
      editorCollapsedAnchor = next;
      editorCollapsedSelection = { start: next, end: next };
    }

    syncEditorSectionControl();
    if (editorCollapsedMode) renderCollapsedView();
    if (opts.jump) jumpToEditorBlock(next);
  }

  function replaceCollapsedBlockSource(block, nextText) {
    if (!block) return;
    const textarea = byId('editor-textarea');
    if (!textarea) return;

    pushEditorUndoSnapshot();

    const current = textarea.value;
    let replacement = String(nextText ?? '');

    if (Array.isArray(block.segments) && block.segments.length) {
      toast('This generated runtime section is read-only because it spans separated gaps');
      return;
    }

    textarea.value = current.slice(0, block.startIndex) + replacement + current.slice(block.endIndex);
    editorUndoSnapshot = textarea.value;
    updateCollapsedBlocks();
    refreshEditorHeader();
  }

  function sizeCollapsedEditor(source) {
    if (!source) return;
    const lineHeight = Math.max(14, Number.parseFloat(getComputedStyle(source).lineHeight) || editorFontSize * 1.55);
    const lines = String(source.value || '').split('\n').length;
    const padding = 24;
    const max = Math.max(80, Math.floor(window.innerHeight * 0.30));
    const next = Math.min(max, Math.max(42, Math.ceil(lines * lineHeight + padding)));
    source.style.height = `${next}px`;
    source.style.overflowY = lines * lineHeight + padding > max ? 'auto' : 'hidden';
  }

  function toggleCollapsedBlock(blockIndex) {
    const current = editorCollapsedSelection && editorCollapsedSelection.start === blockIndex && editorCollapsedSelection.end === blockIndex;
    if (current) {
      editorCollapsedSelection = null;
      editorCollapsedAnchor = null;
      syncEditorSectionControl();
      renderCollapsedView();
      return;
    }
    setEditorBlockSelection(blockIndex);
  }

  function renderCollapsedView() {
    const host = byId('editor-collapsed-view');
    if (!host) return;
    host.innerHTML = '';
    if (!editorCollapsedBlocks.length) {
      host.innerHTML = '<div class="editor-collapsed-empty">No top-level function, class, export, section, or HTML blocks found in this file.</div>';
      return;
    }

    const start = editorCollapsedSelection ? Math.min(editorCollapsedSelection.start, editorCollapsedSelection.end) : -1;
    const end = editorCollapsedSelection ? Math.max(editorCollapsedSelection.start, editorCollapsedSelection.end) : -1;
    const text = currentEditorText();

    editorCollapsedBlocks.forEach(block => {
      const row = document.createElement('div');
      row.className = 'editor-collapsed-item editable';
      if (block.index >= start && block.index <= end) row.classList.add('selected');
      row.dataset.index = String(block.index);

      const header = document.createElement('div');
      header.className = 'editor-collapsed-header';

      const line = document.createElement('span');
      line.className = 'editor-collapsed-line';
      line.textContent = String(block.startLine);

      const signature = document.createElement('button');
      signature.className = 'editor-collapsed-signature';
      signature.textContent = block.signature;
      header.addEventListener('click', event => {
        if (event.target === copy || event.target === source) return;
        toggleCollapsedBlock(block.index);
      });
      signature.addEventListener('click', event => {
        event.stopPropagation();
        toggleCollapsedBlock(block.index);
      });

      const copy = document.createElement('button');
      copy.className = 'editor-collapsed-copy';
      copy.textContent = 'Copy';
      copy.addEventListener('click', async event => {
        event.stopPropagation();
        const source = Array.isArray(block.segments) && block.segments.length
          ? block.segments.map(segment => text.slice(segment.startIndex, segment.endIndex)).join('\n')
          : text.slice(block.startIndex, block.endIndex);
        await navigator.clipboard.writeText(source);
        toast('Section copied');
      });

      header.appendChild(line);
      header.appendChild(signature);
      header.appendChild(copy);
      row.appendChild(header);

      const source = document.createElement('textarea');
      source.className = 'editor-collapsed-source editor-collapsed-editor';
      source.spellcheck = false;
      source.value = Array.isArray(block.segments) && block.segments.length
        ? block.segments.map(segment => text.slice(segment.startIndex, segment.endIndex)).join('\n')
        : text.slice(block.startIndex, block.endIndex);
      source.readOnly = Array.isArray(block.segments) && block.segments.length;
      source.addEventListener('focus', () => {
        const index = Number(row.dataset.index);
        const alreadySelected = editorCollapsedSelection?.start === index && editorCollapsedSelection?.end === index;
        if (!alreadySelected) setEditorBlockSelection(index);
      });
      source.addEventListener('input', () => {
        sizeCollapsedEditor(source);
        replaceCollapsedBlockSource(editorCollapsedBlocks[Number(row.dataset.index)], source.value);
      });
      row.appendChild(source);
      sizeCollapsedEditor(source);

      row.addEventListener('contextmenu', event => openEditorContextMenu(event));
      host.appendChild(row);
    });
  }

  function getCollapsedSourceSelection() {
    if (!editorCollapsedSelection || !editorCollapsedBlocks.length) return '';
    const start = Math.min(editorCollapsedSelection.start, editorCollapsedSelection.end);
    const end = Math.max(editorCollapsedSelection.start, editorCollapsedSelection.end);
    const text = currentEditorText();
    const selected = editorCollapsedBlocks.slice(start, end + 1);
    return selected.map(block => {
      if (Array.isArray(block.segments) && block.segments.length) {
        return block.segments.map(segment => text.slice(segment.startIndex, segment.endIndex)).join('\n');
      }
      return text.slice(block.startIndex, block.endIndex);
    }).join('\n');
  }

  function getCurrentEditorSelectionText() {
    const active = document.activeElement;
    if (active?.classList?.contains('editor-collapsed-editor')) {
      return active.value.slice(active.selectionStart, active.selectionEnd);
    }

    const textarea = byId('editor-textarea');
    if (!textarea) return '';
    if (editorCollapsedMode) return getCollapsedSourceSelection();
    return textarea.value.slice(textarea.selectionStart, textarea.selectionEnd);
  }

  async function copyEditorSelection(useFullFile = false) {
    const text = useFullFile ? currentEditorText() : (getCurrentEditorSelectionText() || currentEditorText());
    if (!text) {
      toast('Nothing to copy');
      return;
    }
    await navigator.clipboard.writeText(text);
    toast(useFullFile ? 'Full file copied' : 'Copied');
  }

  function insertIntoTextarea(text) {
    const textarea = byId('editor-textarea');
    if (!textarea) return;
    pushEditorUndoSnapshot();
    const start = textarea.selectionStart;
    const before = textarea.value.slice(0, start);
    const after = textarea.value.slice(textarea.selectionEnd);
    textarea.value = before + text + after;
    const next = start + text.length;
    textarea.setSelectionRange(next, next);
    textarea.focus();
    editorUndoSnapshot = textarea.value;
    refreshEditorHeader();
    updateCollapsedBlocks();
    if (editorCollapsedMode) renderCollapsedView();
  }

  async function cutEditorSelection() {
    const active = document.activeElement;
    if (editorCollapsedMode && active?.classList?.contains('editor-collapsed-editor')) {
      const selected = active.value.slice(active.selectionStart, active.selectionEnd);
      if (!selected) {
        toast('Select text first');
        return;
      }
      await navigator.clipboard.writeText(selected);
      active.setRangeText('', active.selectionStart, active.selectionEnd, 'start');
      replaceCollapsedBlockSource(editorCollapsedBlocks[Number(active.closest('[data-index]')?.dataset.index)], active.value);
      toast('Cut');
      return;
    }

    if (editorCollapsedMode) {
      toast('Use an editable collapsed section to cut text');
      return;
    }

    const textarea = byId('editor-textarea');
    if (!textarea) return;
    const selected = textarea.value.slice(textarea.selectionStart, textarea.selectionEnd);
    if (!selected) {
      toast('Select text first');
      return;
    }
    await navigator.clipboard.writeText(selected);
    pushEditorUndoSnapshot();
    textarea.setRangeText('', textarea.selectionStart, textarea.selectionEnd, 'start');
    editorUndoSnapshot = textarea.value;
    refreshEditorHeader();
    updateCollapsedBlocks();
    toast('Cut');
  }

  async function pasteIntoEditor() {
    const text = await navigator.clipboard.readText();
    if (!text) return;

    const active = document.activeElement;
    if (editorCollapsedMode && active?.classList?.contains('editor-collapsed-editor')) {
      active.setRangeText(text, active.selectionStart, active.selectionEnd, 'end');
      replaceCollapsedBlockSource(editorCollapsedBlocks[Number(active.closest('[data-index]')?.dataset.index)], active.value);
      toast('Pasted');
      return;
    }

    if (editorCollapsedMode) {
      toast('Select an editable collapsed section to paste');
      return;
    }

    insertIntoTextarea(text);
    toast('Pasted');
  }

  async function runEditorSystemFileAction(action, filePath, payload) {
    const data = await api('/editor/system-action', 'POST', { action, filePath, payload });
    if (!data?.ok) throw new Error(data?.message || data?.error || `${action} failed`);
    return data;
  }

  function getEditorFileName(filePath) {
    return String(filePath || '').replace(/\\/g, '/').split('/').pop() || '';
  }

  function joinEditorPathParts(folderPath, fileName) {
    const folder = normalizeEditorPath(folderPath).replace(/[\\/]+$/, '');
    const name = canonicalEditorFileSegment(fileName);
    return normalizeEditorPath(`${folder}/${name}`);
  }

  function collectEditorFolderOptions(node, out = []) {
    if (!node) return out;
    if (node.type === 'dir' && node.path !== '__editor_projects__') out.push({ name: node.name, path: node.path });
    for (const child of node.children || []) collectEditorFolderOptions(child, out);
    return out;
  }

  function ensureEditorDestinationPicker() {
    let modal = byId('editor-destination-picker');
    if (modal) return modal;

    modal = document.createElement('div');
    modal.id = 'editor-destination-picker';
    modal.hidden = true;
    modal.style.position = 'fixed';
    modal.style.inset = '0';
    modal.style.zIndex = '9999';
    modal.style.background = 'rgba(0,0,0,.45)';
    modal.innerHTML = [
      '<div class="editor-destination-card" style="position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);width:min(560px,92vw);max-height:82vh;overflow:auto;background:var(--panel,#111);color:var(--text,#eee);border:1px solid var(--line,#444);border-radius:10px;padding:14px;box-shadow:0 18px 50px rgba(0,0,0,.45)">',
      '<div style="font-weight:700;margin-bottom:8px" id="editor-destination-title">Choose destination</div>',
      '<div style="font-size:12px;color:var(--text3,#aaa);margin-bottom:8px" id="editor-destination-source"></div>',
      '<label style="display:block;font-size:12px;margin:8px 0 4px">File name</label>',
      '<input id="editor-destination-name" style="width:100%;box-sizing:border-box;margin-bottom:10px" />',
      '<div style="font-size:12px;color:var(--text3,#aaa);margin-bottom:4px">Click a destination folder</div>',
      '<div id="editor-destination-folders" style="max-height:360px;overflow:auto;border:1px solid var(--line,#444);border-radius:8px;padding:6px"></div>',
      '<div style="display:flex;gap:8px;justify-content:flex-end;margin-top:12px">',
      '<button type="button" id="editor-destination-cancel">Cancel</button>',
      '<button type="button" id="editor-destination-apply">Apply</button>',
      '</div>',
      '</div>'
    ].join('');
    document.body.appendChild(modal);
    return modal;
  }

  async function api(url, method = 'GET', payload = null) {
    const options = { method, headers: { 'Content-Type': 'application/json' } };
    if (payload != null) options.body = JSON.stringify(payload);
    const res = await fetch(url, options);
    let data = null;
    try { data = await res.json(); } catch { data = { ok: false, error: 'invalid_json_response' }; }
    if (!res.ok && data?.ok !== false) data = { ok: false, error: `http_${res.status}` };
    return data;
  }

  function editorToast(message) {
    if (typeof window.toast === 'function') {
      window.toast(message);
      return;
    }
    const el = byId('toast');
    if (el) {
      el.textContent = String(message || '');
      el.classList.add('show');
      setTimeout(() => el.classList.remove('show'), 1800);
      return;
    }
    console.info('[Editor]', message);
  }

  function normalizeEditorTreeNode(node) {
    if (!node || typeof node !== 'object') return null;
    const next = {
      name: String(node.name || node.path || 'workspace'),
      path: normalizeEditorPath(node.path || '.'),
      type: node.type === 'file' ? 'file' : 'dir',
      children: Array.isArray(node.children) ? node.children.map(normalizeEditorTreeNode).filter(Boolean) : []
    };
    next.lazy = next.type === 'dir' && (!Array.isArray(node.children) || !node.children.length);
    return next;
  }

  async function loadEditorTree() {
    const treeHost = byId('editor-tree');
    if (treeHost) treeHost.innerHTML = '<div style="color:var(--text3);padding:6px">Loading files…</div>';
    setEditorStatus('Loading file tree…');

    const data = await api('/read/tree', 'POST', { root: getEditorRootRequest() || '.' });
    if (!data?.ok || !data.root) {
      editorTree = null;
      renderEditorTree();
      setEditorStatus('File tree failed to load');
      editorToast(data?.message || data?.error || 'Unable to load file tree');
      return null;
    }

    editorRuntimeRootLabel = data.activeProject || data.projectRoot || 'workspace';
    editorTree = normalizeEditorTreeNode(data.root);
    editorCollapsedTree.clear();
    const collapseRootFolders = node => {
      if (!node) return;
      if (node.type === 'dir' && node.path !== editorTree?.path && node.path !== '__editor_projects__') editorCollapsedTree.add(node.path);
      for (const child of node.children || []) collapseRootFolders(child);
    };
    collapseRootFolders(editorTree);
    renderEditorTree();
    refreshEditorHeader();
    setEditorStatus('File tree loaded');
    return editorTree;
  }

  async function openEditorFile(filePath) {
    const cleanPath = normalizeEditorPath(filePath);
    if (!cleanPath) return;
    if (cleanPath !== editorOpenFile && editorIsDirty()) {
      const proceed = await confirmEditorExit();
      if (!proceed) return;
    }
    if (!isEditorEditableFile(cleanPath)) {
      showEditorNonEditableFile(cleanPath);
      return;
    }

    setEditorStatus('Opening file…');
    const data = await api('/read/file', 'POST', { path: cleanPath });
    const file = Array.isArray(data?.files) ? data.files[0] : null;
    if (!data?.ok || !file?.ok || file.type !== 'file') {
      setEditorStatus('File open failed');
      editorToast(file?.error || data?.message || data?.error || 'Unable to open file');
      return;
    }

    editorOpenFile = cleanPath;
    editorLoadedText = String(file.content || '');
    editorUndoStack = [];
    editorRedoStack = [];
    editorUndoSnapshot = editorLoadedText;
    editorCollapsedMode = false;
    editorCollapsedSelection = null;
    editorCollapsedAnchor = null;
    editorExpandedCollapsedBlock = null;

    const textarea = byId('editor-textarea');
    if (textarea) textarea.value = editorLoadedText;
    updateCollapsedBlocks();
    setCollapsedMode(false);
    renderEditorTree();
    byId('panel-editor')?.classList.remove('tree-open', 'tree-peek');

    refreshEditorHeader();
    syncEditorHighlight();
  }

  async function saveEditorFile() {
    if (!editorOpenFile) {
      editorToast('No file selected');
      return false;
    }

    setEditorStatus('Saving…');
    const content = currentEditorText();
    const data = await api('/projects/document/save', 'POST', { filePath: editorOpenFile, content });
    if (!data?.ok) {
      setEditorStatus('Save failed');
      editorToast(data?.message || data?.error || 'Save failed');
      return false;
    }

    editorLoadedText = content;
    editorUndoSnapshot = content;
    refreshEditorHeader();
    editorToast('File saved');
    return true;
  }
  function refreshSectionMenu() {
    const select = byId('editor-section-select');
    if (!select) return;
    const current = select.value;
    select.innerHTML = '<option value="">Sections</option>';
    editorCollapsedBlocks.forEach((block, index) => {
      const option = document.createElement('option');
      option.value = String(index);
      option.textContent = `${block.startLine} · ${block.signature}`;
      select.appendChild(option);
    });
    if (current && Number(current) < editorCollapsedBlocks.length) select.value = current;
  }

  function jumpToEditorBlock(index) {
    const block = editorCollapsedBlocks[Math.max(0, Math.min(editorCollapsedBlocks.length - 1, Number(index) || 0))];
    const textarea = byId('editor-textarea');
    if (!block || !textarea) return;
    textarea.focus();
    textarea.setSelectionRange(block.startIndex, block.startIndex);
    const lineHeight = Math.max(14, Number.parseFloat(getComputedStyle(textarea).lineHeight) || editorFontSize * 1.35);
    textarea.scrollTop = Math.max(0, (block.startLine - 2) * lineHeight);
    syncEditorHighlight();
  }

  function editorFolderPath(filePath) {
    const clean = normalizeEditorPath(filePath || '').replace(/\/$/, '');
    const slash = clean.lastIndexOf('/');
    return slash >= 0 ? clean.slice(0, slash) : '';
  }

  function editorPathInFolder(filePath, folderPath) {
    const clean = normalizeEditorPath(filePath || '');
    const root = normalizeEditorPath(folderPath || '').replace(/\/$/, '');
    return !!root && (clean === root || clean.startsWith(`${root}/`));
  }

  function refreshEditorFindOptions() {
    const file = byId('editor-find-file');
    const peers = byId('editor-find-peers');
    const subfolders = byId('editor-find-subfolders');
    const matchCase = byId('editor-find-matchcase');
    const wholeWord = byId('editor-find-wholeword');
    const fileMode = byId('editor-find-file-filter-mode');
    const folderMode = byId('editor-find-folder-filter-mode');
    const typeMode = byId('editor-find-filetype-filter-mode');

    if (file) editorFindOptions.file = file.checked;
    if (peers) editorFindOptions.peers = peers.checked;
    if (subfolders) editorFindOptions.subfolders = subfolders.checked;
    if (matchCase) editorFindOptions.matchCase = matchCase.checked;
    if (wholeWord) editorFindOptions.wholeWord = wholeWord.checked;
    if (fileMode) editorFindOptions.fileFilterMode = fileMode.value === 'exclude' ? 'exclude' : 'include';
    if (folderMode) editorFindOptions.folderFilterMode = folderMode.value === 'include' ? 'include' : 'exclude';
    if (typeMode) editorFindOptions.filetypeFilterMode = typeMode.value === 'exclude' ? 'exclude' : 'include';
  }

  function editorFindFileList(node = editorTree, out = []) {
    if (!node) return out;
    if (node.type === 'file' && isEditorEditableFile(node.path)) out.push(normalizeEditorPath(node.path));
    for (const child of node.children || []) editorFindFileList(child, out);
    return out;
  }

  function editorFindFolderList(node = editorTree, out = new Map()) {
    if (!node) return out;
    if (node.type === 'dir' && node.path && node.path !== '__editor_projects__') {
      const pathValue = normalizeEditorPath(node.path);
      const name = String(pathValue).replace(/\/$/, '').split('/').pop() || node.name || pathValue;
      out.set(pathValue, name);
    }
    for (const child of node.children || []) editorFindFolderList(child, out);
    return out;
  }

  function currentEditorFindBaseFiles() {
    const current = normalizeEditorPath(editorOpenFile);
    if (!current) return [];
    const folder = editorFolderPath(current);
    const all = editorFindFileList();
    const picked = new Set();

    if (editorFindOptions.file) picked.add(current);
    if (editorFindOptions.peers) {
      all.forEach(filePath => {
        if (editorFolderPath(filePath) === folder) picked.add(filePath);
      });
    }
    if (editorFindOptions.subfolders) {
      all.forEach(filePath => {
        if (filePath.startsWith(`${folder}/`)) picked.add(filePath);
      });
    }

    return [...picked].filter(filePath => isEditorEditableFile(filePath));
  }

  function selectableEditorFindFolders() {
    const current = normalizeEditorPath(editorOpenFile);
    const folder = editorFolderPath(current);
    if (!folder) return [];
    return [...editorFindFolderList().entries()]
      .filter(([pathValue]) => pathValue === folder || (editorFindOptions.subfolders && pathValue.startsWith(`${folder}/`)))
      .sort((a, b) => a[0].localeCompare(b[0]));
  }

  function renderEditorFindCheckList(host, entries, selectedSet, formatLabel) {
    if (!host) return;
    host.innerHTML = '';
    if (!entries.length) {
      const empty = document.createElement('div');
      empty.className = 'editor-find-filter-empty';
      empty.textContent = 'No choices in active scope';
      host.appendChild(empty);
      return;
    }

    entries.forEach(entry => {
      const value = Array.isArray(entry) ? entry[0] : entry;
      const meta = Array.isArray(entry) ? entry[1] : null;
      const row = document.createElement('label');
      row.className = 'editor-find-check-row';
      const input = document.createElement('input');
      input.type = 'checkbox';
      input.checked = selectedSet.has(value);
      input.addEventListener('change', () => {
        if (input.checked) selectedSet.add(value);
        else selectedSet.delete(value);
      });
      row.appendChild(input);
      row.appendChild(document.createTextNode(` ${formatLabel(value, meta)}`));
      host.appendChild(row);
    });
  }

  function renderEditorFindOptionLists() {
    refreshEditorFindOptions();

    const fileHost = byId('editor-find-file-list');
    const folderHost = byId('editor-find-folder-list');
    const typeHost = byId('editor-find-filetype-list');

    const files = currentEditorFindBaseFiles().sort((a, b) => a.localeCompare(b));
    const fileSet = new Set(files);
    for (const selected of [...editorFindOptions.files]) if (!fileSet.has(selected)) editorFindOptions.files.delete(selected);

    renderEditorFindCheckList(fileHost, files, editorFindOptions.files, filePath => filePath);

    const folders = selectableEditorFindFolders();
    const folderSet = new Set(folders.map(([pathValue]) => pathValue));
    for (const selected of [...editorFindOptions.folders]) if (!folderSet.has(selected)) editorFindOptions.folders.delete(selected);

    renderEditorFindCheckList(folderHost, folders, editorFindOptions.folders, (pathValue, label) => `${label} · ${pathValue}`);

    const counts = new Map();
    files.forEach(filePath => {
      const ext = editorFileExtension(filePath) || '(none)';
      counts.set(ext, (counts.get(ext) || 0) + 1);
    });

    const types = [...counts.entries()].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]));
    const typeSet = new Set(types.map(([ext]) => ext));
    for (const selected of [...editorFindOptions.extensions]) if (!typeSet.has(selected)) editorFindOptions.extensions.delete(selected);

    renderEditorFindCheckList(typeHost, types, editorFindOptions.extensions, (ext, count) => `${ext} · ${count}`);
  }

  function editorFindFileAllowed(filePath) {
    const clean = normalizeEditorPath(filePath);
    if (!isEditorEditableFile(clean)) return false;

    if (editorFindOptions.files.size) {
      const selected = editorFindOptions.files.has(clean);
      if (editorFindOptions.fileFilterMode === 'include' && !selected) return false;
      if (editorFindOptions.fileFilterMode === 'exclude' && selected) return false;
    }

    if (editorFindOptions.folders.size) {
      const selected = [...editorFindOptions.folders].some(folderPath => editorPathInFolder(clean, folderPath));
      if (editorFindOptions.folderFilterMode === 'include' && !selected) return false;
      if (editorFindOptions.folderFilterMode === 'exclude' && selected) return false;
    }

    if (editorFindOptions.extensions.size) {
      const ext = editorFileExtension(clean) || '(none)';
      const selected = editorFindOptions.extensions.has(ext);
      if (editorFindOptions.filetypeFilterMode === 'include' && !selected) return false;
      if (editorFindOptions.filetypeFilterMode === 'exclude' && selected) return false;
    }

    return true;
  }

  function currentEditorFindScopeFiles() {
    refreshEditorFindOptions();
    return currentEditorFindBaseFiles().filter(filePath => editorFindFileAllowed(filePath));
  }

  function findEditorMatchesInText(text, needle, filePath = editorOpenFile) {
    const source = String(text || '');
    const query = String(needle || '');
    if (!query) return [];

    const lines = source.split('\n');
    const offsets = getLineOffsets(source);

    return findEditorMatchIndexes(source, query).map(index => {
      const lineNumber = indexToLineNumber(offsets, index);
      const lineText = String(lines[lineNumber - 1] || '').trimEnd();
      return { filePath, index, lineNumber, lineText };
    });
  }
  async function gotoEditorFindResult(result) {
    if (!result) return;
    const targetPath = normalizeEditorPath(result.filePath || editorOpenFile);
    if (targetPath && targetPath !== editorOpenFile) await openEditorFile(targetPath);

    const textarea = byId('editor-textarea');
    const input = byId('editor-find-input');
    if (!textarea || !input) return;

    const needle = result.project ? (byId('editor-project-find-input')?.value || '') : (input.value || '');
    setCollapsedMode(false);
    textarea.focus();
    textarea.setSelectionRange(result.index, result.index + needle.length);
    const lineHeight = Math.max(14, Number.parseFloat(getComputedStyle(textarea).lineHeight) || editorFontSize * 1.35);
    textarea.scrollTop = Math.max(0, (result.lineNumber - 3) * lineHeight);
    syncEditorHighlight();
  }
  function selectedEditorTextForFind() {
    const textarea = byId('editor-textarea');
    if (!textarea || editorCollapsedMode) return '';
    return textarea.value.slice(textarea.selectionStart, textarea.selectionEnd);
  }

  function closeEditorFindbar() {
    const bar = byId('editor-findbar');
    const results = byId('editor-find-results');
    if (bar) bar.hidden = true;
    if (results) results.hidden = true;
    syncEditorHighlight();
  }

  function openEditorFindbar() {
    const bar = byId('editor-findbar');
    const input = byId('editor-find-input');
    if (!bar || !input) return;

    const opening = bar.hidden;
    if (!opening) {
      closeEditorFindbar();
      return;
    }

    bar.hidden = false;

    const selected = selectedEditorTextForFind();
    if (selected && !selected.includes('\n')) input.value = selected;
    syncEditorHighlight();
    setTimeout(() => {
      input.focus();
      input.select();
    }, 30);
  }
  function findEditorText(nextOnly = true) {
    const textarea = byId('editor-textarea');
    const input = byId('editor-find-input');
    if (!textarea || !input) return false;
    const needle = input.value;
    if (!needle) { toast('Enter find text'); return false; }
    refreshEditorFindOptions();

    const indexes = findEditorMatchIndexes(textarea.value, needle);
    if (!indexes.length) { toast('No match'); return false; }
    const from = nextOnly ? textarea.selectionEnd : 0;
    let index = indexes.find(value => value >= from);
    if (index == null) index = indexes[0];

    setCollapsedMode(false);
    textarea.focus();
    textarea.setSelectionRange(index, index + needle.length);
    syncEditorHighlight();
    return true;
  }
  async function findAllEditorText() {
    const textarea = byId('editor-textarea');
    const input = byId('editor-find-input');
    const count = byId('editor-local-find-count');
    if (!textarea || !input) return;

    refreshEditorFindOptions();
    const needle = input.value;
    if (!needle) { toast('Enter find text'); return; }

    editorFindResults = findEditorMatchesInText(textarea.value, needle, editorOpenFile);
    if (count) count.textContent = `${editorFindResults.length} local match${editorFindResults.length === 1 ? '' : 'es'}`;
    syncEditorHighlight();
    if (editorFindResults[0]) gotoEditorFindResult(editorFindResults[0]);
  }
  function replaceEditorNext() {
    const textarea = byId('editor-textarea');
    const input = byId('editor-find-input');
    const replacement = byId('editor-replace-input')?.value || '';
    if (!textarea || !input?.value) return;
    refreshEditorFindOptions();
    const selected = textarea.value.slice(textarea.selectionStart, textarea.selectionEnd);
    const selectedMatch = editorFindOptions.matchCase
      ? selected === input.value
      : selected.toLowerCase() === input.value.toLowerCase();
    if (!selectedMatch && !findEditorText(true)) return;
    pushEditorUndoSnapshot();
    textarea.setRangeText(replacement, textarea.selectionStart, textarea.selectionEnd, 'end');
    editorUndoSnapshot = textarea.value;
    refreshEditorHeader();
    updateCollapsedBlocks();
    findEditorText(true);
  }

  function replaceEditorAll() {
    const textarea = byId('editor-textarea');
    const input = byId('editor-find-input');
    const replacement = byId('editor-replace-input')?.value || '';
    if (!textarea || !input?.value) return;

    refreshEditorFindOptions();
    const needle = input.value;
    const matches = findEditorMatchIndexes(textarea.value, needle);
    const count = matches.length;
    if (!count) { toast('No matches'); return; }

    pushEditorUndoSnapshot();
    let next = textarea.value;
    [...matches].reverse().forEach(index => {
      next = next.slice(0, index) + replacement + next.slice(index + needle.length);
    });
    textarea.value = next;
    editorUndoSnapshot = textarea.value;
    refreshEditorHeader();
    updateCollapsedBlocks();
    const localCount = byId('editor-local-find-count');
    if (localCount) localCount.textContent = `Replaced ${count} in current file`;
    toast(`Replaced ${count}`);
  }
  function editorProjectResultKey(result) {
    return `${result.filePath || result.FilePath}:${result.lineNumber || result.line}:${result.index ?? ''}:${result.lineText || result.quote || ''}`;
  }

  function addEditorProjectScopeRow() {
    const host = byId('editor-project-scope-rows');
    if (!host) return;
    const row = document.createElement('div');
    row.className = 'EditorProjectScopeRow-Dx';
    row.innerHTML = '<select class="field-input editor-project-scope-mode"><option value="include">include</option><option value="exclude">exclude</option></select><select class="field-input editor-project-scope-kind"><option value="files">files</option><option value="folders">folders</option><option value="filetypes">filetypes</option></select><input class="field-input editor-project-scope-value" list="editor-project-scope-choice-list" value="ALL" placeholder="ALL, current, or type" />';
    host.appendChild(row);
  }

  function collectEditorProjectScopeRules() {
    return [...document.querySelectorAll('#editor-project-scope-rows .EditorProjectScopeRow-Dx')].map(row => ({
      mode: row.querySelector('.editor-project-scope-mode')?.value === 'exclude' ? 'exclude' : 'include',
      kind: row.querySelector('.editor-project-scope-kind')?.value || 'files',
      value: String(row.querySelector('.editor-project-scope-value')?.value || 'ALL').trim()
    })).filter(rule => rule.value);
  }

  function editorProjectRuleMatches(filePath, rule) {
    const clean = normalizeEditorPath(filePath);
    const value = String(rule.value || '').trim();
    const upper = value.toUpperCase();
    if (!value || upper === 'ALL') return true;
    if (upper === 'CURRENT FILE') return clean === normalizeEditorPath(editorOpenFile);
    if (upper === 'CURRENT FOLDER') return editorPathInFolder(clean, editorFolderPath(editorOpenFile));
    if (rule.kind === 'filetypes') {
      const wanted = value.startsWith('.') ? value.toLowerCase() : `.${value.toLowerCase()}`;
      return editorFileExtension(clean) === wanted;
    }
    if (rule.kind === 'folders') return editorPathInFolder(clean, normalizeEditorPath(value)) || clean.includes(value.replace(/\\/g, '/'));
    return clean === normalizeEditorPath(value) || clean.endsWith(`/${value}`) || clean.includes(value.replace(/\\/g, '/'));
  }

  function editorProjectFileAllowed(filePath) {
    const rules = collectEditorProjectScopeRules();
    const includeRules = rules.filter(rule => rule.mode === 'include' && String(rule.value || '').trim().toUpperCase() !== 'ALL');
    const excludeRules = rules.filter(rule => rule.mode === 'exclude');
    if (includeRules.length && !includeRules.some(rule => editorProjectRuleMatches(filePath, rule))) return false;
    if (excludeRules.some(rule => editorProjectRuleMatches(filePath, rule))) return false;
    return true;
  }

  function sortEditorProjectResults(results) {
    const current = normalizeEditorPath(editorOpenFile);
    return [...results].sort((a, b) => {
      const af = normalizeEditorPath(a.filePath || a.FilePath);
      const bf = normalizeEditorPath(b.filePath || b.FilePath);
      if (af === current && bf !== current) return -1;
      if (bf === current && af !== current) return 1;
      return af.localeCompare(bf) || Number(a.lineNumber || a.line || 0) - Number(b.lineNumber || b.line || 0) || Number(a.index || 0) - Number(b.index || 0);
    });
  }

  function renderEditorProjectResults() {
    const host = byId('editor-find-results');
    const count = byId('editor-project-find-count');
    if (!host) return;
    host.innerHTML = '';
    host.hidden = false;

    const selected = editorProjectFindResults.filter(result => !editorProjectExcludedResults.has(editorProjectResultKey(result)));
    if (count) count.textContent = `${selected.length} selected of ${editorProjectFindResults.length} project match${editorProjectFindResults.length === 1 ? '' : 'es'}`;

    const list = document.createElement('div');
    list.className = 'editor-find-results-list';
    if (!editorProjectFindResults.length) {
      const empty = document.createElement('div');
      empty.className = 'editor-find-result-empty';
      empty.textContent = 'No project matches';
      list.appendChild(empty);
      host.appendChild(list);
      return;
    }

    let lastFile = '';
    editorProjectFindResults.forEach(result => {
      const filePath = normalizeEditorPath(result.filePath || result.FilePath);
      const key = editorProjectResultKey(result);
      if (filePath !== lastFile) {
        const fileBreak = document.createElement('div');
        fileBreak.className = 'editor-find-file-break';
        fileBreak.textContent = filePath;
        list.appendChild(fileBreak);
        lastFile = filePath;
      }

      const row = document.createElement('button');
      row.type = 'button';
      row.className = 'editor-find-result-row';
      if (editorProjectExcludedResults.has(key)) row.classList.add('excluded');

      const x = document.createElement('span');
      x.className = 'editor-find-result-exclude';
      x.textContent = '×';
      x.addEventListener('click', event => {
        event.preventDefault();
        event.stopPropagation();
        if (editorProjectExcludedResults.has(key)) editorProjectExcludedResults.delete(key);
        else editorProjectExcludedResults.add(key);
        renderEditorProjectResults();
      });

      const line = document.createElement('span');
      line.className = 'editor-find-result-line';
      line.textContent = String(result.lineNumber || result.line || '');

      const quote = document.createElement('span');
      quote.className = 'editor-find-result-quote';
      quote.textContent = String(result.lineText || result.quote || '').slice(0, 180);

      row.appendChild(x);
      row.appendChild(line);
      row.appendChild(quote);
      row.addEventListener('click', () => gotoEditorFindResult(result));
      list.appendChild(row);
    });

    host.appendChild(list);
  }

  async function runEditorProjectSearch() {
    const input = byId('editor-project-find-input');
    if (!input?.value) { toast('Enter project search text'); return; }
    refreshEditorFindOptions();
    const term = input.value;
    const result = await api('/verbatim/output', 'POST', { request: 'trace.term', term, limit: 200, matchCase: editorFindOptions.matchCase, wholeWord: editorFindOptions.wholeWord });
    if (!result?.ok) { toast(result?.message || result?.error || 'Project search failed'); return; }

    editorProjectExcludedResults.clear();
    editorProjectFindResults = sortEditorProjectResults((Array.isArray(result.hits) ? result.hits : [])
      .map(hit => ({ project: true, filePath: normalizeEditorPath(hit.FilePath || hit.filePath), lineNumber: Number(hit.line || hit.lineNumber || 0), index: Number(hit.index || 0), lineText: String(hit.quote || hit.lineText || '') }))
      .filter(hit => hit.filePath && editorProjectFileAllowed(hit.filePath)));
    renderEditorProjectResults();
  }

  async function replaceEditorProjectSelected() {
    const needle = String(byId('editor-project-find-input')?.value || '');
    const replacement = String(byId('editor-project-replace-input')?.value || '');
    if (!needle) { toast('Enter project search text'); return; }

    const selected = editorProjectFindResults.filter(result => !editorProjectExcludedResults.has(editorProjectResultKey(result)));
    if (!selected.length) { toast('No selected project matches'); return; }

    const filePaths = [...new Set(selected.map(result => normalizeEditorPath(result.filePath)).filter(Boolean))];
    if (!filePaths.length) { toast('No selected project files'); return; }

    if (!confirm(`Replace all occurrences in ${filePaths.length} selected file${filePaths.length === 1 ? '' : 's'}?`)) return;

    const manifests = filePaths.map(filePath => ({
      Task: `Editor Replace All in ${filePath}`,
      Action: 'replaceall',
      FilePath: filePath,
      Payload: {
        target: needle,
        input: replacement
      },
      Comment: 'Queued by Editor Find/Replace.'
    }));

    const sent = await api('/record', 'POST', manifests.length === 1 ? manifests[0] : manifests);
    if (!sent?.ok) {
      toast(sent?.message || sent?.error || 'Replace All failed');
      return;
    }

    toast(`Queued ${manifests.length} recorder manifest${manifests.length === 1 ? '' : 's'}`);
  }
  function cycleEditorCollapseMode() {
    const currentIndex = EDITOR_OUTLINE_MODES.indexOf(editorOutlineMode);
    editorOutlineMode = EDITOR_OUTLINE_MODES[(currentIndex + 1) % EDITOR_OUTLINE_MODES.length];
    editorExpandedCollapsedBlock = null;
    updateCollapsedBlocks();
    setCollapsedMode(true);
    renderCollapsedView();
    toast(`Collapse: ${EDITOR_OUTLINE_LABELS[editorOutlineMode] || editorOutlineMode}`);
  }

  function bootEditor() {
    if (editorBooted) return;
    editorBooted = true;

    restoreEditorFontSize();
    renderEditorTree();
    refreshEditorHeader();

    byId('editor-refresh-btn')?.addEventListener('click', () => loadEditorTree());
    byId('editor-tree-toggle-btn')?.addEventListener('click', () => byId('panel-editor')?.classList.toggle('tree-open'));
    const treeToggle = byId('editor-tree-toggle-btn');
    const panelEditor = byId('panel-editor');
    byId('editor-save-btn')?.addEventListener('click', () => saveEditorFile());
    byId('editor-copy-btn')?.addEventListener('click', () => copyEditorSelection(false));
    byId('editor-font-dec-btn')?.addEventListener('click', () => adjustEditorFontSize(-1));
    byId('editor-font-inc-btn')?.addEventListener('click', () => adjustEditorFontSize(1));
    byId('editor-find-toggle-btn')?.addEventListener('click', () => openEditorFindbar());
    byId('editor-find-next-btn')?.addEventListener('click', () => findEditorText(true));
    byId('editor-replace-next-btn')?.addEventListener('click', () => replaceEditorNext());
    byId('editor-find-all-btn')?.addEventListener('click', () => findAllEditorText());
    byId('editor-replace-all-btn')?.addEventListener('click', () => replaceEditorAll());
    byId('editor-find-close-btn')?.addEventListener('click', () => closeEditorFindbar());
    byId('editor-find-add-filter-btn')?.addEventListener('click', () => addEditorProjectScopeRow());
    byId('editor-project-find-all-btn')?.addEventListener('click', () => runEditorProjectSearch());
    byId('editor-project-replace-selected-btn')?.addEventListener('click', () => replaceEditorProjectSelected());
    ['editor-find-matchcase', 'editor-find-wholeword'].forEach(id => {
      byId(id)?.addEventListener('change', () => {
        refreshEditorFindOptions();
        syncEditorHighlight();
      });
    });
    byId('editor-find-input')?.addEventListener('input', () => syncEditorHighlight());
    byId('editor-collapse-btn')?.addEventListener('click', () => cycleEditorCollapseMode());
    byId('editor-expand-btn')?.addEventListener('click', () => setCollapsedMode(false));
    byId('editor-tree-expand-btn')?.addEventListener('click', () => { editorCollapsedTree.clear(); renderEditorTree(); });
    byId('editor-tree-collapse-btn')?.addEventListener('click', () => {
      const visit = node => { if (node?.type === 'dir') editorCollapsedTree.add(node.path); (node?.children || []).forEach(visit); };
      visit(editorTree);
      renderEditorTree();
    });

    byId('editor-section-select')?.addEventListener('change', event => {
      const value = Number(event.target.value);
      if (Number.isFinite(value)) setEditorBlockSelection(value, { jump: true });
    });

    byId('editor-prev-section')?.addEventListener('click', () => {
      if (!editorCollapsedBlocks.length) updateCollapsedBlocks();
      const current = editorCollapsedSelection?.start ?? 0;
      setEditorBlockSelection(Math.max(0, current - 1), { jump: true });
    });

    byId('editor-next-section')?.addEventListener('click', () => {
      if (!editorCollapsedBlocks.length) updateCollapsedBlocks();
      const current = editorCollapsedSelection?.start ?? -1;
      setEditorBlockSelection(Math.min(editorCollapsedBlocks.length - 1, current + 1), { jump: true });
    });

    const textarea = byId('editor-textarea');
    if (textarea) {
      textarea.addEventListener('input', () => {
        refreshEditorHeader();
        updateCollapsedBlocks();
      });
      textarea.addEventListener('scroll', syncEditorHighlight);
      textarea.addEventListener('keydown', event => {
        if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === 's') {
          event.preventDefault();
          saveEditorFile();
        }
      });
    }

    loadEditorTree().catch(err => {
      console.warn('[Editor] boot tree load failed', err);
      setEditorStatus('File tree failed to load');
    });
  }

  function discardEditorChanges() {
    if (!editorOpenFile) return;
    const textarea = byId('editor-textarea');
    if (textarea) textarea.value = editorLoadedText;
    editorUndoStack = [];
    editorRedoStack = [];
    editorUndoSnapshot = editorLoadedText;
    updateCollapsedBlocks();
    if (editorCollapsedMode) renderCollapsedView();
    refreshEditorHeader();
  }

  function ensureEditorExitPrompt() {
    let modal = byId('editor-exit-prompt');
    if (modal) return modal;

    modal = document.createElement('div');
    modal.id = 'editor-exit-prompt';
    modal.hidden = true;
    modal.innerHTML = [
      '<div class="editor-exit-card">',
      '<div class="editor-exit-title">SAVE CHANGES?</div>',
      '<div class="editor-exit-message" id="editor-exit-message"></div>',
      '<div class="editor-exit-actions">',
      '<button type="button" class="inp-btn" id="editor-exit-cancel-btn">CANCEL</button>',
      '<button type="button" class="inp-btn" id="editor-exit-discard-btn">NO</button>',
      '<button type="button" class="inp-btn primary" id="editor-exit-save-btn">YES</button>',
      '</div>',
      '</div>'
    ].join('');
    document.body.appendChild(modal);
    return modal;
  }

  async function confirmEditorExit() {
    if (!editorIsDirty()) return true;

    const modal = ensureEditorExitPrompt();
    const message = byId('editor-exit-message');
    const saveBtn = byId('editor-exit-save-btn');
    const discardBtn = byId('editor-exit-discard-btn');
    const cancelBtn = byId('editor-exit-cancel-btn');
    if (message) message.textContent = `Unsaved changes to ${editorOpenFile}. Save changes before continuing?`;
    modal.hidden = false;

    return await new Promise(resolve => {
      const done = value => {
        modal.hidden = true;
        if (saveBtn) saveBtn.onclick = null;
        if (discardBtn) discardBtn.onclick = null;
        if (cancelBtn) cancelBtn.onclick = null;
        resolve(value);
      };
      if (saveBtn) saveBtn.onclick = async () => done(await saveEditorFile());
      if (discardBtn) discardBtn.onclick = () => { discardEditorChanges(); done(true); };
      if (cancelBtn) cancelBtn.onclick = () => done(false);
    });
  }

  async function reloadEditorProjectScope() {
    if (editorIsDirty()) {
      const allowed = await confirmEditorExit();
      if (!allowed) return false;
    }

    editorTree = null;
    editorOpenFile = '';
    editorLoadedText = '';
    editorCollapsedTree.clear();
    editorCollapsedMode = false;
    editorCollapsedBlocks = [];
    editorCollapsedSelection = null;
    editorCollapsedAnchor = null;
    editorExpandedCollapsedBlock = null;
    editorFindResults = [];
    editorFindOptions.files.clear();
    editorFindOptions.folders.clear();
    editorFindOptions.extensions.clear();

    const textarea = byId('editor-textarea');
    if (textarea) textarea.value = '';
    const code = byId('editor-highlight-code');
    if (code) code.textContent = '';
    renderEditorTree();
    refreshEditorHeader();
    if (editorBooted) await loadEditorTree();
    return true;
  }

  window.__reloadEditorProjectScope = reloadEditorProjectScope;
  window.__editorBeforeExit = confirmEditorExit;
  window.addEventListener('beforeunload', event => {
    if (!editorIsDirty()) return;
    event.preventDefault();
    event.returnValue = '';
  });

  window.__bootEditor = function () {
    bootEditor();
  };

  async function chooseEditorDestination(action, sourcePath) {
    const modal = ensureEditorDestinationPicker();
    const title = byId('editor-destination-title');
    const source = byId('editor-destination-source');
    const nameInput = byId('editor-destination-name');
    const folders = byId('editor-destination-folders');
    const cancel = byId('editor-destination-cancel');
    const apply = byId('editor-destination-apply');
    let selectedFolder = '';

    if (title) title.textContent = action === 'copy' ? 'Copy file to folder' : 'Move file to folder';
    if (source) source.textContent = sourcePath;
    if (nameInput) nameInput.value = getEditorFileName(sourcePath);
    if (folders) folders.innerHTML = '';

    if (!editorTree) await loadEditorTree();
    const options = collectEditorFolderOptions(editorTree);
    options.forEach(option => {
      const row = document.createElement('button');
      row.type = 'button';
      row.textContent = option.path;
      row.style.display = 'block';
      row.style.width = '100%';
      row.style.textAlign = 'left';
      row.style.margin = '2px 0';
      row.dataset.path = option.path;
      row.addEventListener('click', () => {
        selectedFolder = option.path;
        folders.querySelectorAll('button').forEach(btn => btn.classList.remove('active'));
        row.classList.add('active');
      });
      folders.appendChild(row);
    });

    modal.hidden = false;

    return await new Promise(resolve => {
      const done = value => {
        modal.hidden = true;
        cancel.onclick = null;
        apply.onclick = null;
        resolve(value);
      };
      cancel.onclick = () => done(null);
      apply.onclick = () => {
        const fileName = String(nameInput?.value || '').trim();
        if (!selectedFolder) { toast('Choose a destination folder'); return; }
        if (!fileName || fileName.includes('/') || fileName.includes('\\')) { toast('Enter only a file name'); return; }
        done(joinEditorPathParts(selectedFolder, fileName));
      };
    });
  }

})();