UNPKG

@phren/cli

Version:

Knowledge layer for AI agents - CLI, MCP server, and data layer

3,220 lines • 170 kB
/**
 * Returns a <script> block with shared browser helpers used across all UI IIFEs:
 *   window._phrenEsc(s)          — HTML-escape a value
 *   window._phrenAuthToken       — the current auth token
 *   window._phrenAuthUrl(base)   — append _auth param to a URL
 *   window._phrenAuthBody(body)  — append _auth param to a form body
 *   window._phrenFetchCsrfToken(cb) — fetch the CSRF token and call cb(token)
 */
export function renderSharedWebUiHelpers(authToken) {
    const safeToken = JSON.stringify(authToken).slice(1, -1); // escape for JS string literal
    return `(function() {
  window._phrenAuthToken = '${safeToken}';
  window._phrenEsc = function(s) {
    return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
  };
  window._phrenAuthUrl = function(base) {
    var tok = window._phrenAuthToken;
    return base + (base.indexOf('?') === -1 ? '?' : '&') + '_auth=' + encodeURIComponent(tok);
  };
  window._phrenAuthBody = function(body) {
    var tok = window._phrenAuthToken;
    return body + (tok ? '&_auth=' + encodeURIComponent(tok) : '');
  };
  window._phrenFetchCsrfToken = function(cb) {
    var tok = window._phrenAuthToken;
    var url = '/api/csrf-token' + (tok ? '?_auth=' + encodeURIComponent(tok) : '');
    fetch(url).then(function(r) { return r.json(); }).then(function(d) { cb(d.token || null); }).catch(function() { cb(null); });
  };
})();`;
}
export function renderProfileSwitcherScript(_authToken) {
    return `(function() {
  var esc = window._phrenEsc;
  var authUrl = window._phrenAuthUrl;

  function loadProfiles() {
    fetch(authUrl('/api/profiles')).then(function(r) { return r.json(); }).then(function(data) {
      var select = document.getElementById('profile-select');
      if (!select) return;
      if (!data.ok || !data.profiles) {
        select.innerHTML = '<option>Error loading profiles</option>';
        return;
      }
      var html = '';
      data.profiles.forEach(function(p) {
        var selected = p.name === data.activeProfile ? ' selected' : '';
        html += '<option value="' + esc(p.name) + '"' + selected + '>' + esc(p.name) + '</option>';
      });
      select.innerHTML = html;
      select.onchange = function() { switchProfile(this.value); };
    }).catch(function(err) {
      var select = document.getElementById('profile-select');
      if (select) select.innerHTML = '<option>Error loading</option>';
    });
  }

  function switchProfile(profileName) {
    if (!profileName) return;
    var status = document.getElementById('profile-status');
    if (status) status.textContent = 'Switching...';
    fetch(authUrl('/api/profile'), {
      method: 'POST',
      headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
      body: window._phrenAuthBody('profile=' + encodeURIComponent(profileName))
    }).then(function(r) { return r.json(); }).then(function(data) {
      if (data.ok) {
        if (status) status.textContent = 'Reloading...';
        setTimeout(function() { location.reload(); }, 500);
      } else {
        if (status) status.textContent = 'Error: ' + (data.error || 'Unknown');
      }
    }).catch(function(err) {
      if (status) status.textContent = 'Error loading';
    });
  }

  window.phrenLoadProfiles = loadProfiles;
  window.phrenSwitchProfile = switchProfile;

  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', loadProfiles);
  } else {
    loadProfiles();
  }
})();`;
}
export function renderFindingCaptureEnhancementScript() {
    return `(function() {
  function selectedProject() {
    var card = document.querySelector('.project-card.selected');
    return card ? (card.getAttribute('data-project') || '') : '';
  }

  function findingsTab() {
    return document.querySelector('.project-detail-tab[data-file="FINDINGS.md"]');
  }

  function wireCaptureBar() {
    var container = document.getElementById('project-content');
    var tab = findingsTab();
    if (!container || !tab || !tab.classList.contains('active')) return;

    container.querySelectorAll('.finding-detail-card summary').forEach(function(summary) {
      summary.textContent = (summary.textContent || '').replace(/\\s*<!--.*?-->\\s*/g, ' ').trim();
    });

    var input = document.getElementById('finding-add-input');
    if (!input) {
      var project = selectedProject();
      if (!project || container.querySelector('.project-detail-empty')) return;
      var bar = document.createElement('div');
      bar.className = 'finding-capture-bar';
      bar.innerHTML = '<input id="finding-add-input" type="text" placeholder="Record durable knowledge…" aria-label="Finding text">' +
        '<button type="button" class="btn btn-sm btn-primary" data-finding-capture-submit>Add finding</button>';
      container.insertBefore(bar, container.firstChild);
      input = bar.querySelector('#finding-add-input');
    } else {
      var existingBar = input.parentElement;
      if (existingBar) existingBar.classList.add('finding-capture-bar');
    }

    if (!input || input.getAttribute('data-capture-wired') === 'true') return;
    input.setAttribute('data-capture-wired', 'true');
    input.setAttribute('placeholder', 'Record durable knowledge…');
    input.addEventListener('keydown', function(event) {
      if (event.key !== 'Enter') return;
      event.preventDefault();
      window.phrenAddFinding(selectedProject());
    });
    var button = input.parentElement && input.parentElement.querySelector('button');
    if (button) {
      button.textContent = 'Add finding';
      button.addEventListener('click', function(event) {
        event.preventDefault();
        window.phrenAddFinding(selectedProject());
      });
    }
  }

  window.selectProjectFile = function(file) {
    var tab = document.querySelector('.project-detail-tab[data-file="' + file + '"]');
    if (tab && typeof window.loadProjectFile === 'function') window.loadProjectFile(file, tab);
  };

  var baseSelectProject = window.selectProject;
  if (typeof baseSelectProject === 'function') {
    window.selectProject = function(name, el) {
      baseSelectProject(name, el);
      var header = document.querySelector('.project-detail-header');
      if (header && !header.querySelector('.capture-finding-btn')) {
        var button = document.createElement('button');
        button.type = 'button';
        button.className = 'btn btn-sm btn-primary capture-finding-btn';
        button.textContent = '+ Add finding';
        button.addEventListener('click', function() {
          window.selectProjectFile('FINDINGS.md');
          window.setTimeout(function() {
            wireCaptureBar();
            var input = document.getElementById('finding-add-input');
            if (input) input.focus();
          }, 0);
        });
        header.appendChild(button);
      }
    };
  }

  var detailArea = document.getElementById('project-detail-area');
  if (detailArea) {
    new MutationObserver(function() { window.setTimeout(wireCaptureBar, 0); })
      .observe(detailArea, { childList: true, subtree: true });
  }
})();`;
}
export function renderNotesEnhancementScript() {
    return `(function() {
  var esc = window._phrenEsc;
  var authUrl = window._phrenAuthUrl;

  function selectedProject() {
    var card = document.querySelector('.project-card.selected');
    return card ? (card.getAttribute('data-project') || '') : '';
  }

  function request(project, method, values, done) {
    window._phrenFetchCsrfToken(function(csrfToken) {
      var body = new URLSearchParams(values || {});
      if (csrfToken) body.set('_csrf', csrfToken);
      fetch(authUrl('/api/notes/' + encodeURIComponent(project)), {
        method: method,
        headers: { 'content-type': 'application/x-www-form-urlencoded' },
        body: window._phrenAuthBody(body.toString())
      }).then(function(r) { return r.json(); }).then(function(data) {
        if (!data.ok) throw new Error(data.error || 'Note operation failed');
        done(data);
      }).catch(function(err) { alert(String(err.message || err)); });
    });
  }

  function renderNotes(project, notes) {
    var container = document.getElementById('project-content');
    if (!container) return;
    var today = new Date().toISOString().slice(0, 10);
    var html = '<div class="notes-panel">' +
      '<div class="note-capture">' +
        '<textarea id="note-add-text" rows="3" placeholder="What happened today? Markdown is welcome." aria-label="Note text"></textarea>' +
        '<div class="note-capture-actions"><input id="note-add-date" type="date" value="' + today + '" aria-label="Note date">' +
        '<button type="button" class="btn btn-primary" data-note-action="add">Add note</button></div>' +
      '</div>';
    if (!notes.length) {
      html += '<div class="project-detail-empty">No notes yet. Add a quick daily note above.</div>';
    } else {
      var lastDate = '';
      notes.forEach(function(note) {
        if (note.date !== lastDate) {
          if (lastDate) html += '</div>';
          lastDate = note.date;
          html += '<div class="note-day"><div class="note-day-heading">' + esc(note.date) + '</div>';
        }
        html += '<article class="note-card" data-note-id="' + esc(note.id) + '">' +
          '<div class="note-card-meta"><span>' + esc(String(note.time || '').slice(0, 5)) + '</span>' +
          '<code>' + esc(note.id) + '</code>' +
          (note.promoted ? '<span class="note-promoted">promoted</span>' : '') + '</div>' +
          '<div class="note-card-text">' + esc(note.text).replace(/\\n/g, '<br>') + '</div>' +
          '<div class="note-card-actions">' +
            '<button type="button" class="btn btn-sm" data-note-action="edit">Edit</button>' +
            (!note.promoted ? '<button type="button" class="btn btn-sm" data-note-action="promote">Promote to finding</button>' : '') +
            '<button type="button" class="btn btn-sm note-remove" data-note-action="remove">Remove</button>' +
          '</div>' +
          '<textarea class="note-raw-text" hidden>' + esc(note.text) + '</textarea>' +
        '</article>';
      });
      if (lastDate) html += '</div>';
    }
    container.innerHTML = html + '</div>';
  }

  function loadNotes(focusCapture) {
    var project = selectedProject();
    var container = document.getElementById('project-content');
    if (!project || !container) return;
    container.innerHTML = '<div class="project-detail-empty">Loading notes...</div>';
    fetch(authUrl('/api/notes/' + encodeURIComponent(project)))
      .then(function(r) { return r.json(); })
      .then(function(data) {
        if (!data.ok) throw new Error(data.error || 'Failed to load notes');
        renderNotes(project, (data.data && data.data.notes) || []);
        if (focusCapture) {
          var input = document.getElementById('note-add-text');
          if (input) input.focus();
        }
      }).catch(function(err) {
        container.innerHTML = '<div class="project-detail-empty">' + esc(err.message || err) + '</div>';
      });
  }

  var baseSelectProject = window.selectProject;
  if (typeof baseSelectProject === 'function') {
    window.selectProject = function(name, el) {
      baseSelectProject(name, el);
      var tabs = document.querySelector('.project-detail-tabs');
      if (tabs && !tabs.querySelector('[data-file="notes:daily"]')) {
        var tab = document.createElement('button');
        tab.type = 'button';
        tab.className = 'project-detail-tab';
        tab.setAttribute('data-ui-action', 'loadProjectFile');
        tab.setAttribute('data-file', 'notes:daily');
        tab.textContent = 'Notes';
        var reference = tabs.querySelector('[data-file="reference:browser"]');
        tabs.insertBefore(tab, reference);
      }
      var header = document.querySelector('.project-detail-header');
      if (header && !header.querySelector('.capture-note-btn')) {
        var button = document.createElement('button');
        button.type = 'button';
        button.className = 'btn btn-sm capture-note-btn';
        button.textContent = '+ Add note';
        button.addEventListener('click', function() {
          window.selectProjectFile('notes:daily');
          window.setTimeout(function() { loadNotes(true); }, 0);
        });
        header.appendChild(button);
      }
    };
  }

  var baseLoadProjectFile = window.loadProjectFile;
  window.loadProjectFile = function(file, btn) {
    if (file !== 'notes:daily') return baseLoadProjectFile(file, btn);
    document.querySelectorAll('.project-detail-tab').forEach(function(tab) { tab.classList.remove('active'); });
    var notesTab = btn || document.querySelector('.project-detail-tab[data-file="notes:daily"]');
    if (notesTab) notesTab.classList.add('active');
    loadNotes(false);
  };

  document.addEventListener('click', function(event) {
    var button = event.target && event.target.closest ? event.target.closest('[data-note-action]') : null;
    if (!button) return;
    var project = selectedProject();
    if (!project) return;
    var action = button.getAttribute('data-note-action');
    if (action === 'add') {
      var text = document.getElementById('note-add-text');
      var date = document.getElementById('note-add-date');
      if (!text || !text.value.trim()) return;
      request(project, 'POST', { text: text.value.trim(), date: date ? date.value : '' }, function() { loadNotes(true); });
      return;
    }
    var card = button.closest('.note-card');
    if (!card) return;
    var note = card.getAttribute('data-note-id') || '';
    var raw = card.querySelector('.note-raw-text');
    var current = raw ? raw.value : '';
    if (action === 'edit') {
      var next = prompt('Edit note', current);
      if (next === null || !next.trim() || next === current) return;
      request(project, 'PUT', { note: note, text: next.trim() }, function() { loadNotes(false); });
    } else if (action === 'remove') {
      if (!confirm('Remove this note?')) return;
      request(project, 'DELETE', { note: note }, function() { loadNotes(false); });
    } else if (action === 'promote') {
      var findingType = prompt('Finding type (optional): decision, pitfall, pattern, bug', '');
      if (findingType === null) return;
      findingType = findingType.trim().toLowerCase();
      var valid = ['', 'decision', 'pitfall', 'pattern', 'bug'];
      if (valid.indexOf(findingType) === -1) { alert('Unknown finding type.'); return; }
      request(project, 'POST', { action: 'promote', note: note, finding_type: findingType }, function() { loadNotes(false); });
    }
  });
})();`;
}
export function renderSkillUiEnhancementScript(_authToken) {
    return `(function() {
    var _skillCurrent = null;
    var _skillEditing = false;
    var esc = window._phrenEsc;
    var authUrl = window._phrenAuthUrl;
    var authBody = window._phrenAuthBody;
    var fetchCsrfToken = window._phrenFetchCsrfToken;

    function renderSkillReader(content) {
      var reader = document.getElementById('skills-reader');
      if (!_skillCurrent || !reader) return;
      var statusBadge = '<span class="badge ' + (_skillCurrent.enabled ? 'badge-on' : 'badge-off') + '" id="skill-enabled-badge">' + (_skillCurrent.enabled ? 'enabled' : 'disabled') + '</span>';
      var toggleLabel = _skillCurrent.enabled ? 'Disable' : 'Enable';
      reader.innerHTML =
        '<div class="reader-toolbar">' +
          '<span class="reader-title">' + esc(_skillCurrent.name) + '</span>' +
          '<span class="reader-path">' + esc(_skillCurrent.path) + '</span>' +
          statusBadge +
          '<span id="skill-status"></span>' +
          '<button class="btn btn-sm" data-action="phrenToggleSkill">' + toggleLabel + '</button>' +
          '<button class="btn btn-sm" data-action="phrenEditSkill">Edit</button>' +
        '</div>' +
        '<div class="reader-content"><pre id="skill-pre">' + esc(content) + '</pre></div>';
    }
    function loadSkills(selectPath) {
      fetch(authUrl('/api/skills')).then(function(r) { return r.json(); }).then(function(data) {
        var list = document.getElementById('skills-list');
        if (!list) return;
        if (!data.length) {
          list.innerHTML = '<div style="padding:40px 20px;color:var(--muted);text-align:center">No skills installed</div>';
          return;
        }
        var bySource = {};
        data.forEach(function(s) { (bySource[s.source] = bySource[s.source] || []).push(s); });
        var html = '';
        Object.keys(bySource).sort().forEach(function(src) {
          html += '<div class="split-group-label">' + esc(src) + '</div>';
          bySource[src].forEach(function(s) {
            html += '<div class="split-item" data-path="' + esc(s.path) + '" data-name="' + esc(s.name) + '" data-source="' + esc(s.source) + '" data-enabled="' + (s.enabled ? 'true' : 'false') + '" data-action="phrenSelectSkillFromEl">' +
              '<span>' + esc(s.name) + '</span>' +
              '<span class="badge ' + (s.enabled ? 'badge-on' : 'badge-off') + '">' + (s.enabled ? 'enabled' : 'disabled') + '</span>' +
            '</div>';
          });
        });
        list.innerHTML = html;
        if (selectPath) {
          var current = list.querySelector('.split-item[data-path="' + CSS.escape(selectPath) + '"]');
          if (current) current.click();
        }
      });
    }
    window.phrenSelectSkillFromEl = function(el) {
      if (!el) return;
      window.phrenSelectSkill(
        el.getAttribute('data-path') || '',
        el.getAttribute('data-name') || '',
        el.getAttribute('data-source') || '',
        el.getAttribute('data-enabled') === 'true',
        el
      );
    };
    window.phrenSelectSkill = function(filePath, name, source, enabled, el) {
      if (_skillEditing && !confirm('Discard unsaved changes?')) return;
      _skillEditing = false;
      _skillCurrent = { path: filePath, name: name, source: source, enabled: enabled };
      document.querySelectorAll('#skills-list .split-item').forEach(function(i) { i.classList.remove('selected'); });
      if (el) el.classList.add('selected');
      var reader = document.getElementById('skills-reader');
      if (reader) reader.innerHTML = '<div class="reader-empty">Loading...</div>';
      fetch(authUrl('/api/skill-content?path=' + encodeURIComponent(filePath))).then(function(r) { return r.json(); }).then(function(data) {
        if (!data.ok) {
          if (reader) reader.innerHTML = '<div class="reader-empty">' + esc(data.error || 'Error loading file') + '</div>';
          return;
        }
        renderSkillReader(data.content);
      });
    };
    window.phrenEditSkill = function() {
      var pre = document.getElementById('skill-pre');
      if (!pre || !_skillCurrent) return;
      _skillEditing = true;
      var content = pre.textContent || '';
      var toolbar = document.querySelector('#skills-reader .reader-toolbar');
      if (!toolbar) return;
      Array.from(toolbar.querySelectorAll('.btn')).forEach(function(btn) { btn.remove(); });
      toolbar.insertAdjacentHTML('beforeend', '<button class="btn btn-sm btn-primary" data-action="phrenSaveSkill">Save</button><button class="btn btn-sm" data-action="phrenCancelSkillEdit">Cancel</button>');
      var ta = document.createElement('textarea');
      ta.id = 'skill-textarea';
      ta.value = content;
      pre.replaceWith(ta);
      ta.focus();
    };
    window.phrenCancelSkillEdit = function() {
      _skillEditing = false;
      if (_skillCurrent) window.phrenSelectSkill(_skillCurrent.path, _skillCurrent.name, _skillCurrent.source, _skillCurrent.enabled);
    };
    window.phrenSaveSkill = function() {
      var ta = document.getElementById('skill-textarea');
      if (!ta || !_skillCurrent) return;
      fetchCsrfToken(function(csrfToken) {
        var csrfPart = csrfToken ? '&_csrf=' + encodeURIComponent(csrfToken) : '';
        fetch('/api/skill-save', {
          method: 'POST',
          headers: { 'content-type': 'application/x-www-form-urlencoded' },
          body: authBody('path=' + encodeURIComponent(_skillCurrent.path) + '&content=' + encodeURIComponent(ta.value)) + csrfPart,
        }).then(function(r) { return r.json(); }).then(function(data) {
          var status = document.getElementById('skill-status');
          if (status) {
            status.textContent = data.ok ? 'Saved' : (data.error || 'Save failed');
            status.className = data.ok ? 'text-success' : 'text-danger';
          }
          if (data.ok) {
            _skillEditing = false;
            renderSkillReader(ta.value);
          }
        });
      });
    };
    window.phrenToggleSkill = function() {
      if (!_skillCurrent) return;
      fetchCsrfToken(function(csrfToken) {
        var csrfPart = csrfToken ? '&_csrf=' + encodeURIComponent(csrfToken) : '';
        var nextEnabled = !_skillCurrent.enabled;
        fetch('/api/skill-toggle', {
          method: 'POST',
          headers: { 'content-type': 'application/x-www-form-urlencoded' },
          body: authBody('project=' + encodeURIComponent(_skillCurrent.source) + '&name=' + encodeURIComponent(_skillCurrent.name) + '&enabled=' + encodeURIComponent(String(nextEnabled))) + csrfPart,
        }).then(function(r) { return r.json(); }).then(function(data) {
          if (!data.ok) return;
          _skillCurrent.enabled = nextEnabled;
          loadSkills(_skillCurrent.path);
          window.phrenSelectSkill(_skillCurrent.path, _skillCurrent.name, _skillCurrent.source, _skillCurrent.enabled);
        });
      });
    };
    var baseSwitchTab = window.switchTab;
    if (typeof baseSwitchTab === 'function') {
      window.switchTab = function(tab) {
        baseSwitchTab(tab);
        if (tab === 'skills') setTimeout(function() { loadSkills(_skillCurrent && _skillCurrent.path); }, 0);
      };
    }
    // Event delegation for dynamically generated skill UI buttons
    document.addEventListener('click', function(e) {
      var target = e.target;
      if (!target || typeof target.closest !== 'function') return;
      var actionEl = target.closest('[data-action]');
      if (!actionEl) return;
      var action = actionEl.getAttribute('data-action');
      if (action === 'phrenToggleSkill') { phrenToggleSkill(); }
      else if (action === 'phrenEditSkill') { phrenEditSkill(); }
      else if (action === 'phrenSaveSkill') { phrenSaveSkill(); }
      else if (action === 'phrenCancelSkillEdit') { phrenCancelSkillEdit(); }
      else if (action === 'phrenSelectSkillFromEl') { phrenSelectSkillFromEl(actionEl); }
    });
  })();`;
}
export function renderProjectReferenceEnhancementScript(_authToken) {
    return `(function() {
    var _referenceState = {
      project: '',
      topicsData: null,
      referenceData: null,
      selectedType: '',
      selectedKey: '',
      editor: null
    };
    var esc = window._phrenEsc;
    var authUrl = window._phrenAuthUrl;
    var authBody = window._phrenAuthBody;
    var fetchCsrfToken = window._phrenFetchCsrfToken;

    function currentProject() {
      var selected = document.querySelector('.project-card.selected');
      return selected ? (selected.getAttribute('data-project') || '') : '';
    }
    function findTopic(slug) {
      var topics = (_referenceState.topicsData && _referenceState.topicsData.topics) || [];
      return topics.find(function(topic) { return topic.slug === slug; }) || null;
    }
    function findTopicDoc(slug) {
      var docs = (_referenceState.referenceData && _referenceState.referenceData.topicDocs) || [];
      return docs.find(function(doc) { return doc.slug === slug; }) || null;
    }
    function setStatus(message, type) {
      var el = document.getElementById('reference-status');
      if (!el) return;
      el.textContent = message || '';
      el.className = 'reference-status' + (type ? ' ' + type : '');
      if (message) setTimeout(function() {
        var live = document.getElementById('reference-status');
        if (live) { live.textContent = ''; live.className = 'reference-status'; }
      }, 3000);
    }
    function loadJson(url) {
      return fetch(authUrl(url)).then(function(r) { return r.json(); });
    }
    function readerToolbar(title, pathLabel, actionsHtml) {
      return '<div class="reader-toolbar">' +
        '<span class="reader-title">' + esc(title) + '</span>' +
        '<span class="reader-path">' + esc(pathLabel || '') + '</span>' +
        '<span id="reference-status" class="reference-status"></span>' +
        (actionsHtml || '') +
      '</div>';
    }
    function renderReferenceHome() {
      var reader = document.getElementById('reference-reader');
      if (!reader) return;
      var topicsData = _referenceState.topicsData || { source: 'default', topics: [], suggestions: [] };
      var banner = topicsData.source === 'default'
        ? '<div class="reference-banner">This project is using starter topics. Customize them so archived findings match the project domain instead of generic web-dev buckets.</div>'
        : '';
      reader.innerHTML = readerToolbar('Reference Topics', _referenceState.project, '<button class="btn btn-sm" data-ref-action="addTopic">Add topic</button><button class="btn btn-sm" data-ref-action="reclassify">Reclassify archived findings</button>') +
        '<div class="topic-empty">' +
          banner +
          '<p>Select a topic doc or a reference file from the sidebar. Topic definitions live in <code>topic-config.json</code> and archive docs live under <code>reference/topics/</code>.</p>' +
        '</div>';
    }
    function renderTopicEditor(mode, topic, suggestion) {
      var reader = document.getElementById('reference-reader');
      if (!reader) return;
      var source = topic || suggestion || { slug: '', label: '', description: '', keywords: [] };
      var title = mode === 'edit' ? 'Edit topic' : 'Add topic';
      reader.innerHTML = readerToolbar(title, _referenceState.project, '<button class="btn btn-sm" data-ref-action="cancelEditor">Cancel</button>') +
        '<div class="reader-content">' +
          '<form class="topic-editor" id="topic-editor-form">' +
            '<label>Label<input id="topic-label-input" value="' + esc(source.label || '') + '" placeholder="Rendering" /></label>' +
            '<label>Slug<input id="topic-slug-input" value="' + esc(source.slug || '') + '" placeholder="rendering" /></label>' +
            '<label>Description<textarea id="topic-description-input" placeholder="What belongs in this topic?">' + esc(source.description || '') + '</textarea></label>' +
            '<label>Keywords<input id="topic-keywords-input" value="' + esc((source.keywords || []).join(', ')) + '" placeholder="shader, frame, gpu, lighting" /></label>' +
            '<div class="topic-editor-actions">' +
              '<button class="btn btn-primary" type="submit">Save</button>' +
              '<button class="btn btn-sm" type="button" data-ref-action="cancelEditor">Cancel</button>' +
            '</div>' +
          '</form>' +
        '</div>';
    }
    function renderTopicSummary(slug) {
      var topic = findTopic(slug);
      var doc = findTopicDoc(slug);
      var reader = document.getElementById('reference-reader');
      if (!topic || !reader) return;
      var actions = '<button class="btn btn-sm" data-ref-action="addTopic">Add topic</button>' +
        '<button class="btn btn-sm" data-ref-action="reclassify">Reclassify archived findings</button>' +
        '<button class="btn btn-sm" data-ref-action="editTopic" data-slug="' + esc(topic.slug) + '">Edit</button>';
      if (topic.slug !== 'general') actions += '<button class="btn btn-sm" data-ref-action="deleteTopic" data-slug="' + esc(topic.slug) + '">Delete</button>';
      if (!doc || !doc.exists) {
        reader.innerHTML = readerToolbar(topic.label, 'reference/topics/' + topic.slug + '.md', actions) +
          '<div class="topic-empty">' +
            '<p>No archived entries have landed here yet. Saving the topic created the bucket, but it stays empty until findings are archived or legacy topic docs are reclassified.</p>' +
            (topic.description ? '<p>' + esc(topic.description) + '</p>' : '') +
            ((topic.keywords || []).length ? '<div class="topic-keywords">' + topic.keywords.map(function(keyword) { return '<span class="topic-keyword">' + esc(keyword) + '</span>'; }).join('') + '</div>' : '') +
          '</div>';
        return;
      }
      reader.innerHTML = readerToolbar(topic.label, doc.file, actions) + '<div class="reader-content"><div class="reader-empty">Loading...</div></div>';
      loadJson('/api/project-reference-content?project=' + encodeURIComponent(_referenceState.project) + '&file=' + encodeURIComponent(doc.file)).then(function(data) {
        var liveReader = document.getElementById('reference-reader');
        if (!liveReader) return;
        if (!data.ok) {
          liveReader.innerHTML = readerToolbar(topic.label, doc.file, actions) + '<div class="reader-empty">' + esc(data.error || 'File not found') + '</div>';
          return;
        }
        liveReader.innerHTML = readerToolbar(topic.label, doc.file, actions) + '<div class="reader-content"><pre>' + esc(data.content) + '</pre></div>';
      });
    }
    function renderReferenceFile(file) {
      var reader = document.getElementById('reference-reader');
      if (!reader) return;
      var actions = '<button class="btn btn-sm" data-ref-action="addTopic">Add topic</button><button class="btn btn-sm" data-ref-action="reclassify">Reclassify archived findings</button>';
      reader.innerHTML = readerToolbar(file.title || file.file, file.file, actions) + '<div class="reader-content"><div class="reader-empty">Loading...</div></div>';
      loadJson('/api/project-reference-content?project=' + encodeURIComponent(_referenceState.project) + '&file=' + encodeURIComponent(file.file)).then(function(data) {
        var liveReader = document.getElementById('reference-reader');
        if (!liveReader) return;
        if (!data.ok) {
          liveReader.innerHTML = readerToolbar(file.title || file.file, file.file, actions) + '<div class="reader-empty">' + esc(data.error || 'File not found') + '</div>';
          return;
        }
        liveReader.innerHTML = readerToolbar(file.title || file.file, file.file, actions) + '<div class="reader-content"><pre>' + esc(data.content) + '</pre></div>';
      });
    }
    function renderReferenceSidebar() {
      var container = document.getElementById('project-content');
      if (!container || !_referenceState.topicsData || !_referenceState.referenceData) return;
      var topicsData = _referenceState.topicsData;
      var referenceData = _referenceState.referenceData;
      var topicRows = (topicsData.topics || []).map(function(topic) {
        var doc = findTopicDoc(topic.slug);
        var selected = _referenceState.selectedType === 'topic' && _referenceState.selectedKey === topic.slug ? ' selected' : '';
        var meta = (doc && doc.exists ? doc.entryCount + ' entries' : 'empty bucket') + (topicsData.source === 'default' ? ' · starter' : '');
        return '<div class="split-item' + selected + '" data-ref-action="selectTopic" data-slug="' + esc(topic.slug) + '">' +
          '<div class="reference-item-main"><span class="reference-item-title">' + esc(topic.label) + '</span><span class="reference-item-meta">' + esc(meta) + '</span></div>' +
        '</div>';
      }).join('');
      var suggestionRows = (topicsData.suggestions || []).length
        ? topicsData.suggestions.map(function(suggestion) {
            var confidencePct = Math.round((Number(suggestion.confidence || 0)) * 100);
            var meta = suggestion.reason + (confidencePct > 0 ? ' · confidence ' + confidencePct + '%' : '');
            var isPinned = suggestion.source === 'pinned';
            return '<div class="split-item">' +
              '<div class="reference-item-main"><span class="reference-item-title">' + esc(suggestion.label) + '</span><span class="reference-item-meta">' + esc(meta) + '</span></div>' +
              (isPinned
                ? '<button class="btn btn-sm reference-item-action" data-ref-action="unpinSuggestion" data-slug="' + esc(suggestion.slug) + '">Unpin</button>'
                : '<button class="btn btn-sm reference-item-action" data-ref-action="pinSuggestion" data-slug="' + esc(suggestion.slug) + '">Pin</button><button class="btn btn-sm reference-item-action" data-ref-action="useSuggestion" data-slug="' + esc(suggestion.slug) + '">Use</button>') +
            '</div>';
          }).join('')
        : '<div class="reference-sidebar-note">No topic suggestions right now.</div>';
      var legacyByFile = {};
      (topicsData.legacyDocs || []).forEach(function(doc) { legacyByFile[doc.file] = doc; });
      var otherRows = (referenceData.otherDocs || []).length
        ? referenceData.otherDocs.map(function(file) {
            var selected = _referenceState.selectedType === 'file' && _referenceState.selectedKey === file.file ? ' selected' : '';
            var legacy = legacyByFile[file.file];
            var suffix = legacy ? (legacy.eligible ? ' · legacy topic doc' : ' · legacy skip: ' + legacy.reason) : '';
            return '<div class="split-item' + selected + '" data-ref-action="selectFile" data-file="' + esc(file.file) + '">' +
              '<div class="reference-item-main"><span class="reference-item-title">' + esc(file.title || file.file) + '</span><span class="reference-item-meta">' + esc(file.file + suffix) + '</span></div>' +
            '</div>';
          }).join('')
        : '<div class="reference-sidebar-note">No other reference docs.</div>';
      var banner = topicsData.source === 'default'
        ? '<div class="reference-banner">Starter topics are active for this project. Add project-owned topics to make archives match the actual domain.</div>'
        : '';
      container.innerHTML = banner +
        '<div class="split-view project-reference-shell">' +
          '<div class="split-sidebar">' +
            '<div class="reference-sidebar-toolbar"><button class="btn btn-sm" data-ref-action="addTopic">Add topic</button><button class="btn btn-sm" data-ref-action="reclassify">Reclassify</button></div>' +
            '<div class="split-group-label">Topics</div>' +
            topicRows +
            '<div class="split-group-label">Suggested Topics</div>' +
            suggestionRows +
            '<div class="split-group-label">Other Reference Docs</div>' +
            otherRows +
          '</div>' +
          '<div class="split-reader" id="reference-reader"></div>' +
        '</div>';
      if (_referenceState.editor) {
        renderTopicEditor(_referenceState.editor.mode, _referenceState.editor.topic, _referenceState.editor.suggestion);
        return;
      }
      if (_referenceState.selectedType === 'file') {
        var selectedFile = (referenceData.otherDocs || []).find(function(file) { return file.file === _referenceState.selectedKey; });
        if (selectedFile) {
          renderReferenceFile(selectedFile);
          return;
        }
      }
      if (_referenceState.selectedType === 'topic' && findTopic(_referenceState.selectedKey)) {
        renderTopicSummary(_referenceState.selectedKey);
        return;
      }
      renderReferenceHome();
    }
    function loadReferenceState(nextType, nextKey) {
      var project = currentProject();
      if (!project) return;
      _referenceState.project = project;
      loadJson('/api/project-topics?project=' + encodeURIComponent(project)).then(function(topicsData) {
        if (!topicsData.ok) throw new Error(topicsData.error || 'Failed to load topics');
        return Promise.all([topicsData, loadJson('/api/project-reference-list?project=' + encodeURIComponent(project))]);
      }).then(function(results) {
        var topicsData = results[0];
        var referenceData = results[1];
        if (!referenceData.ok) throw new Error(referenceData.error || 'Failed to load reference docs');
        _referenceState.topicsData = topicsData;
        _referenceState.referenceData = referenceData;
        if (nextType) _referenceState.selectedType = nextType;
        if (nextKey !== undefined) _referenceState.selectedKey = nextKey;
        renderReferenceSidebar();
      }).catch(function(err) {
        var container = document.getElementById('project-content');
        if (container) container.innerHTML = '<div class="project-detail-empty">' + esc(err && err.message ? err.message : 'Failed to load reference data') + '</div>';
      });
    }
    function saveTopics(nextTopics, onDone) {
      fetchCsrfToken(function(csrfToken) {
        var body = 'project=' + encodeURIComponent(_referenceState.project) + '&topics=' + encodeURIComponent(JSON.stringify(nextTopics));
        if (csrfToken) body += '&_csrf=' + encodeURIComponent(csrfToken);
        fetch('/api/project-topics/save', {
          method: 'POST',
          headers: { 'content-type': 'application/x-www-form-urlencoded' },
          body: authBody(body)
        }).then(function(r) { return r.json(); }).then(function(data) {
          if (!data.ok) {
            setStatus(data.error || 'Save failed', 'err');
            return;
          }
          _referenceState.topicsData = data;
          _referenceState.editor = null;
          loadReferenceState('topic', onDone || _referenceState.selectedKey || 'general');
          setStatus('Saved', 'ok');
        }).catch(function() { setStatus('Save failed', 'err'); });
      });
    }
    function collectEditorTopic() {
      var label = document.getElementById('topic-label-input');
      var slug = document.getElementById('topic-slug-input');
      var description = document.getElementById('topic-description-input');
      var keywords = document.getElementById('topic-keywords-input');
      return {
        slug: slug ? slug.value : '',
        label: label ? label.value : '',
        description: description ? description.value : '',
        keywords: keywords ? keywords.value.split(',').map(function(item) { return item.trim(); }).filter(Boolean) : []
      };
    }
    window.phrenLoadProjectReference = function() {
      _referenceState.editor = null;
      loadReferenceState(_referenceState.selectedType || 'topic', _referenceState.selectedKey || 'general');
    };
    window.phrenReferenceSelectTopic = function(slug) {
      _referenceState.editor = null;
      _referenceState.selectedType = 'topic';
      _referenceState.selectedKey = slug;
      renderReferenceSidebar();
    };
    window.phrenReferenceSelectFile = function(file) {
      _referenceState.editor = null;
      _referenceState.selectedType = 'file';
      _referenceState.selectedKey = file;
      renderReferenceSidebar();
    };
    window.phrenReferenceAddTopic = function() {
      _referenceState.editor = { mode: 'add', topic: null, suggestion: null };
      _referenceState.selectedType = '';
      _referenceState.selectedKey = '';
      renderReferenceSidebar();
    };
    window.phrenReferenceEditTopic = function(slug) {
      var topic = findTopic(slug);
      if (!topic) return;
      _referenceState.editor = { mode: 'edit', topic: topic, suggestion: null };
      renderReferenceSidebar();
    };
    window.phrenReferenceCancelEditor = function() {
      _referenceState.editor = null;
      renderReferenceSidebar();
    };
    window.phrenReferenceSaveTopic = function(e) {
      e.preventDefault();
      if (!_referenceState.topicsData) return;
      var topic = collectEditorTopic();
      var topics = (_referenceState.topicsData.topics || []).slice();
      if (_referenceState.editor && _referenceState.editor.mode === 'edit' && _referenceState.editor.topic) {
        topics = topics.map(function(item) { return item.slug === _referenceState.editor.topic.slug ? topic : item; });
      } else {
        topics.push(topic);
      }
      saveTopics(topics, topic.slug || 'general');
    };
    window.phrenReferenceDeleteTopic = function(slug) {
      if (slug === 'general' || !_referenceState.topicsData) return;
      var topics = (_referenceState.topicsData.topics || []).filter(function(topic) { return topic.slug !== slug; });
      saveTopics(topics, 'general');
    };
    window.phrenReferenceUseSuggestion = function(slug) {
      if (!_referenceState.topicsData) return;
      var suggestion = (_referenceState.topicsData.suggestions || []).find(function(item) { return item.slug === slug; });
      if (!suggestion) return;
      var topics = (_referenceState.topicsData.topics || []).slice();
      topics.push({
        slug: suggestion.slug,
        label: suggestion.label,
        description: suggestion.description,
        keywords: suggestion.keywords || []
      });
      saveTopics(topics, suggestion.slug);
    };
    window.phrenReferencePinSuggestion = function(slug) {
      if (!_referenceState.topicsData) return;
      var suggestion = (_referenceState.topicsData.suggestions || []).find(function(item) { return item.slug === slug; });
      if (!suggestion) return;
      fetchCsrfToken(function(csrfToken) {
        var topicPayload = JSON.stringify({
          slug: suggestion.slug,
          label: suggestion.label,
          description: suggestion.description,
          keywords: suggestion.keywords || []
        });
        var body = 'project=' + encodeURIComponent(_referenceState.project) + '&topic=' + encodeURIComponent(topicPayload);
        if (csrfToken) body += '&_csrf=' + encodeURIComponent(csrfToken);
        fetch('/api/project-topics/pin', {
          method: 'POST',
          headers: { 'content-type': 'application/x-www-form-urlencoded' },
          body: authBody(body)
        }).then(function(r) { return r.json(); }).then(function(data) {
          if (!data.ok) {
            setStatus(data.error || 'Pin failed', 'err');
            return;
          }
          _referenceState.topicsData = data;
          loadReferenceState(_referenceState.selectedType || 'topic', _referenceState.selectedKey || 'general');
          setStatus('Pinned', 'ok');
        }).catch(function() { setStatus('Pin failed', 'err'); });
      });
    };
    window.phrenReferenceUnpinSuggestion = function(slug) {
      fetchCsrfToken(function(csrfToken) {
        var body = 'project=' + encodeURIComponent(_referenceState.project) + '&slug=' + encodeURIComponent(slug || '');
        if (csrfToken) body += '&_csrf=' + encodeURIComponent(csrfToken);
        fetch('/api/project-topics/unpin', {
          method: 'POST',
          headers: { 'content-type': 'application/x-www-form-urlencoded' },
          body: authBody(body)
        }).then(function(r) { return r.json(); }).then(function(data) {
          if (!data.ok) {
            setStatus(data.error || 'Unpin failed', 'err');
            return;
          }
          _referenceState.topicsData = data;
          loadReferenceState(_referenceState.selectedType || 'topic', _referenceState.selectedKey || 'general');
          setStatus('Unpinned', 'ok');
        }).catch(function() { setStatus('Unpin failed', 'err'); });
      });
    };
    window.phrenReferenceReclassify = function() {
      fetchCsrfToken(function(csrfToken) {
        var body = 'project=' + encodeURIComponent(_referenceState.project);
        if (csrfToken) body += '&_csrf=' + encodeURIComponent(csrfToken);
        fetch('/api/project-topics/reclassify', {
          method: 'POST',
          headers: { 'content-type': 'application/x-www-form-urlencoded' },
          body: authBody(body)
        }).then(function(r) { return r.json(); }).then(function(data) {
          if (!data.ok) {
            setStatus(data.error || 'Reclassify failed', 'err');
            return;
          }
          loadReferenceState(_referenceState.selectedType || 'topic', _referenceState.selectedKey || 'general');
          setStatus('Moved ' + (data.movedEntries || 0) + ' entries; skipped ' + ((data.skipped || []).length) + ' docs', 'ok');
        }).catch(function() { setStatus('Reclassify failed', 'err'); });
      });
    };
    // Event delegation for dynamically generated reference UI
    document.addEventListener('click', function(e) {
      var target = e.target;
      if (!target || typeof target.closest !== 'function') return;
      var actionEl = target.closest('[data-ref-action]');
      if (!actionEl) return;
      var action = actionEl.getAttribute('data-ref-action');
      if (action === 'addTopic') { phrenReferenceAddTopic(); }
      else if (action === 'reclassify') { phrenReferenceReclassify(); }
      else if (action === 'cancelEditor') { phrenReferenceCancelEditor(); }
      else if (action === 'editTopic') { phrenReferenceEditTopic(actionEl.getAttribute('data-slug')); }
      else if (action === 'deleteTopic') { phrenReferenceDeleteTopic(actionEl.getAttribute('data-slug')); }
      else if (action === 'selectTopic') { phrenReferenceSelectTopic(actionEl.getAttribute('data-slug')); }
      else if (action === 'selectFile') { phrenReferenceSelectFile(actionEl.getAttribute('data-file')); }
      else if (action === 'useSuggestion') { e.stopPropagation(); phrenReferenceUseSuggestion(actionEl.getAttribute('data-slug')); }
      else if (action === 'pinSuggestion') { e.stopPropagation(); phrenReferencePinSuggestion(actionEl.getAttribute('data-slug')); }
      else if (action === 'unpinSuggestion') { e.stopPropagation(); phrenReferenceUnpinSuggestion(actionEl.getAttribute('data-slug')); }
    });
    document.addEventListener('submit', function(e) {
      var form = e.target;
      if (form && form.id === 'topic-editor-form') {
        e.preventDefault();
        phrenReferenceSaveTopic(e);
      }
    });
  })();`;
}
// renderReviewQueueEditSyncScript removed — was dead code returning ""
export function renderTasksAndSettingsScript(authToken) {
    const safeToken = JSON.stringify(authToken).slice(1, -1);
    return `(function() {
    var _tsAuthToken = '${safeToken}';
    var _allTasks = [];
    var esc = window._phrenEsc;

    function tsAuthUrl(base) {
      return base + (base.indexOf('?') === -1 ? '?' : '&') + '_auth=' + encodeURIComponent(_tsAuthToken);
    }

    function loadJson(url) {
      return fetch(url).then(function(r) { return r.json(); });
    }

    function priorityBadge(p) {
      if (!p) return '';
      return '<span class="task-priority-badge task-priority-' + esc(p) + '">' + esc(p) + '</span>';
    }

    function projectBadge(proj) {
      return '<span class="task-project-badge">' + esc(proj) + '</span>';
    }

    function pinIndicator(pinned) {
      if (!pinned) return '';
      return '<span class="task-pin-indicator" title="Pinned"><svg width="12" height="12" viewBox="0 0 16 16" fill="currentColor"><path d="M9.828 2.172a2 2 0 0 1 2.828 0l1.172 1.172a2 2 0 0 1 0 2.828L12 8l-1.5 1.5.5 3.5L8 10l-3.5.5.5-3.5L3.5 5.5 2 4l2.172-1.828a2 2 0 0 1 2.828 0L8 3l1.828-.828z"/></svg></span>';
    }

    function githubBadge(issue, url) {
      if (!issue || !url) return '';
      return '<a href="' + esc(url) + '" target="_blank" rel="noopener noreferrer" class="task-github-badge" title="GitHub Issue #' + esc(String(issue)) + '"><svg width="12" height="12" viewBox="0 0 16 16" fill="currentColor"><path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"/></svg>#' + esc(String(issue)) + '</a>';
    }

    function loadTasks() {
      var url = _tsAuthToken ? tsAuthUrl('/api/tasks') : '/api/tasks';
      fetch(url).then(function(r) { return r.json(); }).then(function(data) {
        _allTasks = data.tasks || [];
        populateTaskProjectFilter();
        filterTasks();
      }).catch(function(err) {
        document.getElementById('tasks-list').innerHTML = '<div style="padding:40px;color:var(--muted);text-align:center">Failed to load tasks: ' + esc(String(err)) + '</div>';
      });
    }

    window.editTaskContract = function(index, field, value) {
      var task = _allTasks[index];
      if (!task) return;
      var updates = { project: task.project, item: task.stableId ? 'bid:' + task.stableId : task.id };
      if (field === 'dependencies') {
        var choices = _allTasks.filter(function(t) { return t.identity && t !== task; });
        var selected = task.dependencies || [];
        var retained = selected.filter(function(d) { return !choices.some(function(t) { return t.identity.storeId === d.storeId && t.identity.project === d.project && t.identity.stableId === d.stableId; }); });
        var panel = document.getElementById('task-prerequisites-' + index);
        if (!panel) return;
        if (value !== 'save') {
          panel.hidden = !panel.hidden;
          var unavailableNotice = retained.length ? '<p role="note">Some prerequisites are not available in this picker and will be kept. This picker cannot remove them. Use the CLI or API to remove these links or clear the dependency list. If a retained target is unavailable to the server, saving other prerequisite changes will fail until it is restored or removed.</p>' + retained.map(function(d) {
            var current = (task.prerequisites || []).find(function(p) { return p.storeId === d.storeId && p.project === d.project && p.stableId === d.stableId; });
            var responsibility = !current || current.missing ? 'Unknown responsibility' : current.responsibility === 'human' ? 'Human' : 'Agent';
            return '<div>' + esc(responsibility + ' · ' + (current ? current.title : 'Unavailable prerequisite')) + ' <code style="overflow-wrap:anywhere">' + esc(d.storeId + '/' + d.project + '/' + d.stableId) + '</code></div>';
          }).join('') : '';
          panel.innerHTML = unavailableNotice + choices.map(function(t, choice) {
            var checked = selected.some(function(d) { return d.storeId === t.identity.storeId && d.project === t.identity.project && d.stableId === t.identity.stableId; });
            var responsibility = t.responsibility === 'human' ? 'Human' : 'Agent';
            return '<label><input type="checkbox" data-task-choice="' + choice + '"' + (checked ? ' checked' : '') + '> ' + esc(responsibility + ' · ' + t.line) + ' <code style="overflow-wrap:anywhere">' + esc(t.identity.storeId + '/' + t.identity.project + '/' + t.identity.stableId) + '</code></label><br>';
          }).join('') + '<button data-ts-action="savePrerequisites" data-index="' + index + '">Save prerequisites</button>';
          panel._choices = choices;
          return;
        }
        // Retain unavailable prerequisites unless explicitly removed through CLI/API.
        var dependencies = retained;
        panel.querySelectorAll('input:checked').forEach(function(input) { dependencies.push(panel._choices[Number(input.getAttribute('data-task-choice'))].identity); });
        updates.dependencies = JSON.stringify(dependencies);
      } else updates[field] = value;
      var csrfUrl = _tsAuthToken ? tsAuthUrl('/api/csrf-token') : '/api/csrf-token';
      fetch(csrfUrl).then(function(r) { return r.json(); }).then(function(csrf) {
        var body = new URLSearchParams(updates);
        if (csrf.token) body.set('_csrf', csrf.token);
        return fetch(_tsAuthToken ? tsAuthUrl('/api/tasks/update') : '/api/tasks/update', { method: 'POST', body: body });
      }).then(function(r) { return r.json(); }).then(function(data) { if (data.ok) loadTasks(); else alert(data.error || 'Task update failed. Your selections have been kept.'); })
        .catch(function(err) { alert('Task update could not be confirmed. Your selections have been kept; check the task before retrying. ' + String(err)); });
    };

    window.completeTaskFromUi = function(project, item) {
      var csrfUrl = _tsAuthToken ? tsAuthUrl('/api/csrf-token') : '/api/csrf-token';
      fetch(csrfUrl).then(function(r) { return r.json(); }).then(function(csrfData) {
        var body = new URLSearchParams({ project: project, item: item });
        if (csrfData.token) body.set('_csrf', csrfData.token);
        var url = _tsAuthToken ? tsAuthUrl('/api/tasks/complete') : '/api/tasks/complete';
        return fetch(url, { method: 'POST', body: body, headers: { 'content-type': 'application/x-www-form-urlencoded' } });
      }).then(function(r) { return r.json(); }).then(function(data) {
        if (data.ok) { loadTasks(); } else { alert(data.error || 'Failed to complete task'); }
      }).catch(function(err) { alert('Error: ' + String(err)); });
    };

    window.removeTaskFromUi = function(project, item) {
      if (!confirm('Delete this task?')) return;
      var csrfUrl = _tsAuthToken ? tsAuthUrl('/api/csrf-token') : '/api/csrf-token';
      fetch(csrfUrl).then(function(r) { return r.json(); }).then(function(csrfData) {
        var body = new URLSearchParams({ project: project, item: item });
        if (csrfData.token) body.set('_csrf', csrfData.token);
        var url = _tsAuthToken ? tsAuthUrl('/api/tasks/remove') : '/api/tasks/remove';
        return fetch(url, { method: 'POST', body: body, headers: { 'content-type': 'application/x-www-form-urlencoded' } });
      }).then(function(r) { return r.json(); }).then(function(data) {
        if (data.ok) { loadTasks(); } else { alert(data.error || 'Failed to remove task'); }
      }).catch(function(err) { alert('Error: ' + String(err)); });
    };

    window.addTaskFromUi = function(project) {
      var input = document.getElementById('task-add-input-' + project);
      if (!input || !input.value.trim()) return;
      var item = input.value.trim();
      var csrfUrl = _tsAuthToken ? tsAuthUrl('/api/csrf-token') : '/api/csrf-token';
      fetch(csrfUrl).then(function(r) { return r.json(); }).then(function(csrfData) {
        var body = new URLSearchParams({ project: project, item: item });
        if (csrfData.token) body.set('_csrf', csrfData.token);
        var url = _tsAuthToken ? tsAuthUrl('/api/tasks/add') : '/api/tasks/add';
        return fetch(url, { method: 'POST', body: body, headers: { 'content-type': 'application/x-www-form-urlencoded' } });
      }).then(function(r) { return r.json(); }).then(function(data) {
        if (data.ok) { input.value = ''; loadTasks(); } else { alert(data.error || 'Failed to add task'); }
      }).catch(function(err) { alert('Error: ' + String(err)); });
    };

    window.toggleDoneSection = function(btn) {
      if (!btn) return;
      var list = btn.nextElementSibling;
      var arrow = btn.querySelector('.task-toggle-arrow');
      if (!list) return;
      var isOpen = list.style.display !== 'none';
      list.style.display = isOpen ? 'none' : 'block';
      if (arrow) arrow.textContent = isOpen ? '\u25B6' : '\u25BC';
    };

    function populateTaskProjectFilter() {
      var projects = Array.from(new Set(_allTasks.map(function(t) { return t.project; }))).sort();
      var sel = document.getElementById('tasks-filter-project');
      if (!sel) return;
      var html = '<option value="">All projects</option>';
      projects.forEach(function(p) { html += '<option value="' + esc(p) + '">' + esc(p) + '</option>'; });
      sel.innerHTML = html;
    }

    window.filterTasks = function() {
      var projectFilter = (document.getElementById('tasks-filter-project') || {}).value || '';
      var sectionFilter = (document.getElementById('tasks-filter-section') || {}).value || '';
      var responsibilityFilter = (document.getElementById('tasks-filter-responsibility') || {}).value || '';
      var showDone = sectionFilter === 'Done';
      var tasks = _allTasks.filter(function(t) {
        if (projectFilter && t.project !== projectFilter) return false;
        if (responsibilityFilter && (t.responsibility || 'agent') !== responsibilityFilter) return false;
        if (sectionFilter && t.section !== sectionFilter) return false;
        if (!sectionFilter && t.section === 'Done') return false;
        return true;
      });
      var doneTasks = showDone ? [] : _allTasks.filter(function(t) {
        if (projectFilter && t.project !== projectFilter) return false;
        if (responsibilityFilter && (t.responsibility || 'agent') !== responsibilityFilter) return false;
        return t.section === 'Done' || t.checked;
      });

      var activeCount = tasks.filter(function(t) { return t.section !== 'Done' && !t.checked; }).length;
      var countEl = document.getElementById('tasks-count');
      if (countEl) countEl.textContent = activeCount + ' active' + (doneTasks.length ? ', ' + doneTasks.length + ' done' : '');

      var container = document.getElementById('tasks-list');
      if (!container) return;
      if (!tasks.length && !doneTasks.length) {
        container.innerHTML = '<div class="task-empty-state"><svg viewBox="0 0 48 48" width="48" height="48" style="display:block;margin:0 auto 12px"><ellipse cx="24" cy="24" rx="16" ry="15" fill="#7B68AE" opacity="0.25"/><ellipse cx="24" cy="24" rx="12" ry="11.5" fill="#7B68AE" opacity="0.4"/><circle cx="19" cy="22" r="1.5" fill="#2D2255"/><circle cx="29" cy="22" r="1.5" fill="#2D2255"/><path d="M21 28c1 1.2 2.5 1.5 3.5 1.3 1-.2 2-1 2.5-1.3" stroke="#2D2255" stroke-width="1" fill="none" stroke-linecap="round"/></svg><div style="font-size:var(--text-md);font-weight:600;color:var(--ink);margin-bottom:4px">Nothing to do!</div><div style="color:var(--muted);font-size:var(--text-sm)">Add a task to get started.</div></div>';
        return;
      }

      var priorityOrder = { high: 0, medium: 1, low: 2 };
      function sortByPriority(a, b) {
        if (a.pinned && !b.pinned) return -1;
        if (!a.pinned && b.pinned) return 1;
        var pa = priorityOrder[a.priority] !== undefined ? priorityOrder[a.priority] : 1;
        var pb = priorityOrder[b.priority] !== undefined ? priorityOrder[b.priority] : 1;
        return pa - pb;
      }

      function isNotDone(t) { return t.section !== 'Done' && !t.checked; }

      function renderTaskRow(t) {
        var index = _allTasks.indexOf(t);
        var taskMatch = t.stableId ? 'bid:' + t.stableId : t.id;
        var isDone = t.section === 'Done' || t.checked;
        var priClass = t.priority ? 'task-row-priority-' + esc(t.priority) : 'task-row-priority-none';
        var html = '<div class="task-row' + (isDone ? ' task-row-done' : '') + '">';
        html += '<div class="task-row-priority ' + priClass + '"></div>';
        html += '<div class="task-row-content">';
        html += '<span class="task-row-text">' + esc(t.line) + '</span>';
        html += '<div>' + esc((t.responsibility || 'agent') === 'human' ? 'Human' : 'Agent') + ' · ' + esc(t.readiness || 'ready') + '</div>';
        (t.prerequisites || []).forEach(function(d) {
          var responsibility = d.missing ? 'Unknown responsibility' : d.responsibility === 'human' ? 'Human' : 'Agent';
          html += '<div>' + (d.missing ? 'Unavailable' : d.completed ? '✓' : 'Waiting on') + ' · ' + esc(responsibility + ' · ' + d.title) + '<br><code style="overflow-wrap:anywhere">' + esc(d.storeId + '/' + d.project + '/' + d.stableId) + '</code></div>';
        });
        html += '<div id="task-prerequisites-' + index + '" hidden></div>';
        html += '</div>';
        html += '<div class="task-row-meta">';
        html += pinIndicator(t.pinned);
        html += githubBadge(t.githubIssue, t.githubUrl);
        html += priorityBadge(t.priority);
        html += projectBadge(t.project);
        html += '</div>';
        html += '<div class="task-row-actions">';
        html += '<select aria-label="Task responsibility" data-task-field="responsibility" data-index="' + index + '"><option value="agent"' + ((t.responsibility || 'agent') === 'agent' ? ' selected' : '') + '>Agent</option><option value="human"' + (t.responsibility === 'human' ? ' selected' : '') + '>Human</option></select>';
        html += '<select aria-label="Task section" data-task-field="section" data-index="' + index + '">' + ['Queue', 'Active', 'Done'].map(function(section) { return '<option' + (t.section === section ? ' selected' : '') + '>' + section + '</option>'; }).join('') + '</select>';
        html += '<button data-ts-action="editPrerequisites" data-index="' + index + '">Prerequisites</button>';
        if (!isDone) {
          html += '<button class="task-action-btn task-action-complete" data-ts-action="completeTask" data-project="' + esc(t.project) + '" data-item="' + esc(taskMatch) + '" title="Mark done"><svg width="14" height="14" viewBox="0 0 16 16" fill="none"><path d="M3 8.5l3.5 3.5 6.5-7" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg></button>';
        }
        html += '<button class="task-action-btn task-action-delete" data-ts-action="removeTask" data-project="' + esc(t.project) + '" data-item="' + esc(taskMatch) + '" title="Delete task"><svg width="12" height="12" viewBox="0 0 16 16" fill="none"><path d="M4 4l8 8M12 4l-8 8" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg></button>';
        html += '</div>';
        html += '</div>';
        return html;
      }

      var html = '';

      // Summary bar
      var allActive = _allTasks.filter(function(t) { return t.section !== 'Done' && !t.checked; });
      var highCount = allActive.filter(function(t) { return t.priority === 'high'; }).length;
      var medCount = allActive.filter(function(t) { return t.priority === 'medium'; }).length;
      var lowCount = allActive.filter(function(t) { return t.priority === 'low'; }).length;
      var projectCounts = {};
      allActive.forEach(function(t) { projectCounts[t.project] = (projectCounts[t.project] || 0) + 1; });
      var topProjects = Object.keys(projectCounts).sort(function(a, b) { return projectCounts[b] - projectCounts[a]; }).slice(0, 3);
      html += '<div class="task-summary-bar">';
      html += '<span class="task-summary-total">' + allActive.length + ' active</span>';
      if (highCount) html += '<span class="task-summary-pill task-summary-high">' + highCount + ' high</span>';
      if (medCount) html += '<span class="task-summary-pill task-summary-medium">' + medCount + ' medium</span>';
      if (lowCount) html += '<span class="task-summary-pill task-summary-low">' + lowCount + ' low</span>';
      if (topProjects.length) {
        html += '<span class="task-summary-projects">';
        topProjects.forEach(function(p) { html += '<span class="task-summary-project">' + esc(p) + ' (' + projectCounts[p] + ')</span>'; });
        html += '</span>';
      }
      html += '</div>';

      // Add task input at top (only when a specific project is selected)
      if (projectFilter) {
        html += '<div class="task-add-bar">';
        html += '<input id="task-add-input-' + esc(projectFilter) + '" type="text" class="task-add-input" placeholder="Add an Agent task to ' + esc(projectFilter) + '\u2026" data-ts-action="addTaskKeydown" data-project="' + esc(projectFilter) + '">';
        html += '<button class="task-add-btn" data-ts-action="addTask" data-project="' + esc(projectFilter) + '"><svg width="14" height="14" viewBox="0 0 16 16" fill="none"><path d="M8 3v10M3 8h10" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg> Add Agent task</button>';
        html += '</div>';
        html += '<p role="note">New tasks are created as Agent work. Creating a Human task in one step is not supported here.</p>';
      }

      // Group by section: Active first, then Queue
      var activeTasks = tasks.filter(function(t) { return t.section === 'Active' && isNotDone(t); }).sort(sortByPriority);
      var queueTasks = tasks.filter(function(t) { return t.section !== 'Active' && isNotDone(t); }).sort(sortByPriority);

      function renderSection(title, items) {
        if (!items.length) return '';
        var shtml = '<div class="task-section-group">';
        shtml += '<div class="task-section-header">' + title + ' <span class="task-section-count">' + items.length + '</span></div>';
        shtml += '<div class="task-list">';
        items.forEach(function(t) { shtml += renderTaskRow(t); });
        shtml += '</div></div>';
        return shtml;
      }

      html += renderSection('Active', activeTasks);
      html += renderSection('Queue', queueTasks);

      // Done section (collapsible)
      var allDone = showDone ? tasks.filter(function(t) { return t.section === 'Done' || t.checked; }) : doneTasks;
      if (allDone.length) {
        html += '<div class="task-done-section">';
        html += '<button class="task-done-toggle" data-ts-action="toggleDoneSection">';
        html += '<span class="task-toggle-arrow">\u25B6</span> Completed <span class="task-section-count">' + allDone.length + '</span></button>';
        html += '<div class="task-done-list" style="display:none">';
        html += '<div class="task-list">';
        allDone.forEach(function(t) { html += renderTaskRow(t); });
        html += '</div></div></div>';
      }

      container.innerHTML = html;
    };

    function setSettingsStatus(message, type) {
      var el = document.getElementById('settings-status-inline');
      if (!el) return;
      el.textContent = message || '';
      el.className = 'settings-status-inline' + (type ? ' ' + type : '');
    }

    function findingUiToStorage(level) {
      var map = { high: 'aggressive', medium: 'balanced', low: 'conservative', minimal: 'minimal' };
      return map[level] || 'balanced';
    }

    function findingStorageToUi(level) {
      var map = { aggressive: 'high', balanced: 'medium', conservative: 'low', minimal: 'minimal' };
      return map[level] || 'medium';
    }

    function postSettings(endpoint, payload, okMessage) {
      var csrfUrl = _tsAuthToken ? tsAuthUrl('/api/csrf-token') : '/api/csrf-token';
      fetch(csrfUrl).then(function(r) { return r.json(); }).then(function(csrfData) {
        var body = new URLSearchParams(payload || {});
        if (csrfData.token) body.set('_csrf', csrfData.token);
        var url = _tsAuthToken ? tsAuthUrl(endpoint) : endpoint;
        return fetch(url, { method: 'POST', body: body, headers: { 'content-type': 'application/x-www-form-urlencoded' } });
      }).then(function(r) { return r.json(); }).then(function(data) {
        if (!data.ok) {
          setSettingsStatus(data.error || 'Settings update failed', 'err');
          return;
        }
        _settingsLoaded = false;
        loadSettings();
        setSettingsStatus(okMessage || 'Settings updated', 'ok');
      }).catch(function(err) {
        setSettingsStatus('Settings update failed: ' + String(err), 'err');
      });
    }

    function getSettingsProject() {
      var sel = document.getElementById('settings-project-select');
      return sel ? sel.value : '';
    }

    function postProjectOverride(project, field, value, clearField) {
      var csrfUrl = _tsAuthToken ? tsAuthUrl('/api/csrf-token') : '/api/csrf-token';
      fetch(csrfUrl).then(function(r) { return r.json(); }).then(function(csrfData) {
        var payload = { project: project, field: field, value: value || '', clear: clearField ? 'true' : 'false' };
        var body = new URLSearchParams(payload);
        if (csrfData.token) body.set('_csrf', csrfData.token);
        var url = _tsAuthToken ? tsAuthUrl('/api/settings/project-overrides') : '/api/settings/project-overrides';
        return fetch(url, { method: 'POST', body: body, headers: { 'content-type': 'application/x-www-form-urlencoded' } });
      }).then(function(r) { return r.json(); }).then(function(data) {
        if (!data.ok) {
          setSettingsStatus(data.error || 'Failed to update project override', 'err');
          return;
        }
        _settingsLoaded = false;
        loadSettings();
        setSettingsStatus('Project override updated', 'ok');
      }).catch(function(err) {
        setSettingsStatus('Failed: ' + String(err), 'err');
      });
    }

    function postGlobalRetention(field, value, clearField) {
      var csrfUrl = _tsAuthToken ? tsAuthUrl('/api/csrf-token') : '/api/csrf-token';
      fetch(csrfUrl).then(function(r) { return r.json(); }).then(function(csrfData) {
        var payload = { field: field, value: value || '', clear: clearField ? 'true' : 'false', globalUpdate: 'true' };
        var body = new URLSearchParams(payload);
        if (csrfData.token) body.set('_csrf', csrfData.token);
        var url = _tsAuthToken ? tsAuthUrl('/api/settings/project-overrides') : '/api/settings/project-overrides';
        return fetch(url, { method: 'POST', body: body, headers: { 'content-type': 'application/x-www-form-urlencoded' } });
      }).then(function(r) { return r.json(); }).then(function(data) {
        if (!data.ok) {
          setSettingsStatus(data.error || 'Failed to update retention policy', 'err');
          return;
        }
        _settingsLoaded = false;
        loadSettings();
        setSettingsStatus('Retention policy updated', 'ok');
      }).catch(function(err) {
        setSettingsStatus('Failed: ' + String(err), 'err');
      });
    }

    function postAccess(role, value) {
      var proj = getSettingsProject();
      var csrfUrl = _tsAuthToken ? tsAuthUrl('/api/csrf-token') : '/api/csrf-token';
      fetch(csrfUrl).then(function(r) { return r.json(); }).then(function(csrfData) {
        var payload = {};
        payload[role] = value || '';
        if (proj) payload.project = proj;
        var body = new URLSearchParams(payload);
        if (csrfData.token) body.set('_csrf', csrfData.token);
        var url = _tsAuthToken ? tsAuthUrl('/api/settings/access') : '/api/settings/access';
        return fetch(url, { method: 'POST', body: body, headers: { 'content-type': 'application/x-www-form-urlencoded' } });
      }).then(function(r) { return r.json(); }).then(function(data) {
        if (!data.ok) {
          setSettingsStatus(data.error || 'Failed to update access roles', 'err');
          return;
        }
        _settingsLoaded = false;
        loadSettings();
        setSettingsStatus('Access roles updated', 'ok');
      }).catch(function(err) {
        setSettingsStatus('Failed: ' + String(err), 'err');
      });
    }

    function postGlobalWorkflow(field, value, clearField) {
      var csrfUrl = _tsAuthToken ? tsAuthUrl('/api/csrf-token') : '/api/csrf-token';
      fetch(csrfUrl).then(function(r) { return r.json(); }).then(function(csrfData) {
        var payload = { field: field, value: value || '', clear: clearField ? 'true' : 'false', globalUpdate: 'true' };
        var body = new URLSearchParams(payload);
        if (csrfData.token) body.set('_csrf', csrfData.token);
        var url = _tsAuthToken ? tsAuthUrl('/api/settings/project-overrides') : '/api/settings/project-overrides';
        return fetch(url, { method: 'POST', body: body, headers: { 'content-type': 'application/x-www-form-urlencoded' } });
      }).then(function(r) { return r.json(); }).then(function(data) {
        if (!data.ok) {
          setSettingsStatus(data.error || 'Failed to update workflow policy', 'err');
          return;
        }
        _settingsLoaded = false;
        loadSettings();
        setSettingsStatus('Workflow policy updated', 'ok');
      }).catch(function(err) {
        setSettingsStatus('Failed: ' + String(err), 'err');
      });
    }

    function loadSettings() {
      var selectedProject = getSettingsProject();
      var baseUrl = '/api/settings';
      if (selectedProject) baseUrl += '?project=' + encodeURIComponent(selectedProject);
      var url = _tsAuthToken ? tsAuthUrl(baseUrl) : baseUrl;

      // Populate project selector on first load
      var sel = document.getElementById('settings-project-select');
      if (sel && sel.querySelectorAll('option[data-proj]').length === 0) {
        var configUrl = _tsAuthToken ? tsAuthUrl('/api/config') : '/api/config';
        fetch(configUrl).then(function(r) { return r.json(); }).then(function(d) {
          if (d.ok && d.projects && d.projects.length && sel) {
            d.projects.forEach(function(p) {
              var opt = document.createElement('option');
              opt.value = p; opt.textContent = p;
              opt.setAttribute('data-proj', '1');
              sel.appendChild(opt);
            });
            if (selectedProject) sel.value = selectedProject;
          }
        }).catch(function() {});
      }

      var scopeNote = document.getElementById('settings-scope-note');
      if (scopeNote) {
        scopeNote.textContent = selectedProject
          ? 'Showing effective config for "' + selectedProject + '". Overrides are saved to that project\\\'s phren.project.yaml.'
          : 'Showing global settings. Select a project to view and edit per-project overrides.';
      }

      // Wire onChange once
      if (sel && !sel.getAttribute('data-onchange-wired')) {
        sel.setAttribute('data-onchange-wired', '1');
        sel.addEventListener('change', function() {
          _settingsLoaded = false;
          loadSettings();
        });
      }

      fetch(url).then(function(r) { return r.json(); }).then(function(data) {
        if (!data.ok) {
          setSettingsStatus(data.error || 'Failed to load settings', 'err');
          return;
        }

        // Use merged config when a project is selected, else global
        var effective = (selectedProject && data.merged) ? data.merged : null;
        var rawOverrides = (selectedProject && data.overrides) ? data.overrides : null;
        var effectiveSensitivity = effective ? effective.findingSensitivity : (data.findingSensitivity || 'balanced');
        var effectiveTaskMode = effective ? effective.taskMode : (data.taskMode || 'auto');
        var effectiveProactivity = data.proactivity || 'high';
        var effectiveRetention = (effective && effective.retentionPolicy) ? effective.retentionPolicy : (data.retentionPolicy || {});
        var effectiveWorkflow = (effective && effective.workflowPolicy) ? effective.workflowPolicy : (data.workflowPolicy || {});

        var isProject = Boolean(selectedProject);

        // Render project info section
        var infoSection = document.getElementById('settings-project-info-section');
        var infoEl = document.getElementById('settings-project-info');
        if (infoSection && infoEl) {
          if (isProject && data.projectInfo) {
            var pi = data.projectInfo;
            infoSection.style.display = '';
            var infoHtml = '<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:12px;font-size:var(--text-sm)">';
            infoHtml += '<div><strong style="color:var(--ink)">Disk path</strong><div class="text-muted" style="font-family:var(--mono);font-size:var(--text-xs);word-break:break-all">' + esc(pi.diskPath) + '</div></div>';
            infoHtml += '<div><strong style="color:var(--ink)">Ownership</strong><div class="text-muted">' + esc(pi.ownership) + '</div></div>';
            infoHtml += '<div><strong style="color:var(--ink)">Config file</strong><div class="text-muted" style="font-family:var(--mono);font-size:var(--text-xs);word-break:break-all">' + esc(pi.configFile) + '</div>' + (pi.configExists ? '<span style="color:var(--green);font-size:var(--text-xs)">exists</span>' : '<span style="color:var(--muted);font-size:var(--text-xs)">not created</span>') + '</div>';
            infoHtml += '<div><strong style="color:var(--ink)">Findings</strong><div class="text-muted">' + pi.findingCount + ' entries</div></div>';
            infoHtml += '<div><strong style="color:var(--ink)">Tasks</strong><div class="text-muted">' + pi.taskCount + ' in queue</div></div>';
            infoHtml += '</div>';
            var files = [];
            if (pi.hasFindings) files.push('FINDINGS.md');
            if (pi.hasTasks) files.push('tasks.md');
            if (pi.hasSummary) files.push('summary.md');
            if (pi.hasClaudeMd) files.push('AGENTS.md');
            if (files.length) {
              infoHtml += '<div style="margin-top:10px;font-size:var(--text-xs);color:var(--muted)">Files: ' + files.map(function(f) { return '<span class="badge" style="margin-right:4px">' + esc(f) + '</span>'; }).join('') + '</div>';
            }
            infoEl.innerHTML = infoHtml;
          } else {
            infoSection.style.display = 'none';
            infoEl.innerHTML = '';
          }
        }

        function sourceBadge(isOverride) {
          if (!isProject) return '';
          return isOverride
            ? '<span style="font-size:10px;font-weight:600;color:var(--warning);margin-left:6px;padding:1px 6px;border:1px solid color-mix(in srgb,var(--warning) 40%,transparent);border-radius:var(--radius-sm)">project override</span>'
            : '<span style="font-size:10px;color:var(--text-muted);margin-left:6px;padding:1px 6px;border:1px solid var(--border);border-radius:var(--radius-sm)">global default</span>';
        }

        var findingDescriptions = {
          high: 'Capture findings proactively, including minor observations.',
          medium: 'Capture findings that are likely useful.',
          low: 'Capture findings only when clearly significant.',
          minimal: 'Only capture explicitly flagged findings.'
        };

        var findingsEl = document.getElementById('settings-findings');
        if (findingsEl) {
          var fsUi = findingStorageToUi(effectiveSensitivity);
          var findingsHtml = '';
          var fsSensOverride = rawOverrides && rawOverrides.findingSensitivity != null;
          findingsHtml += '<div class="settings-control">';
          findingsHtml += '<div class="settings-control-header"><span class="settings-control-label">Finding sensitivity</span>' + sourceBadge(fsSensOverride);
          if (isProject && fsSensOverride) {
            findingsHtml += '<button data-ts-action="clearProjectOverride" data-field="findingSensitivity" class="settings-chip" style="font-size:11px;margin-left:auto">Clear override</button>';
          }
          findingsHtml += '</div>';
          findingsHtml += '<div class="settings-chip-row">';
          ['high', 'medium', 'low', 'minimal'].forEach(function(level) {
            var active = level === fsUi ? ' active' : '';
            var action = isProject ? 'setProjectFindingSensitivity' : 'setFindingSensitivity';
            findingsHtml += '<button data-ts-action="' + action + '" data-level="' + esc(level) + '" class="settings-chip' + active + '">' + esc(level) + '</button>';
          });
          findingsHtml += '</div>';
          findingsHtml += '<div class="settings-control-note" id="settings-fs-desc">' + esc(findingDescriptions[fsUi] || '') + '</div>';
          findingsHtml += '</div>';
          if (!isProject) {
            findingsHtml += '<div class="settings-control">';
            findingsHtml += '<div class="settings-control-header"><span class="settings-control-label">Auto-capture</span>';
            findingsHtml += '<button data-ts-action="toggleAutoCapture" data-enabled="' + (data.autoCaptureEnabled ? 'true' : 'false') + '" class="settings-chip' + (data.autoCaptureEnabled ? ' active' : '') + '">' + (data.autoCaptureEnabled ? 'On' : 'Off') + '</button></div>';
            findingsHtml += '<div class="settings-control-note">Turn automatic finding capture on or off.</div>';
            findingsHtml += '</div>';
            findingsHtml += '<div class="settings-control">';
            findingsHtml += '<div class="settings-control-header"><span class="settings-control-label">Consolidation threshold</span><span class="badge">' + esc(String(data.consolidationEntryThreshold || 25)) + ' entries</span></div>';
            findingsHtml += '<div class="settings-control-note">Consolidation is also recommended after 60+ days with at least 10 new entries.</div>';
            findingsHtml += '</div>';
          }
          findingsEl.innerHTML = findingsHtml;
        }

        var behaviorEl = document.getElementById('settings-behavior');
        if (behaviorEl) {
          var taskMode = effectiveTaskMode || 'auto';
          var proactivity = effectiveProactivity;
          var behaviorHtml = '';
          var taskModeOverride = rawOverrides && rawOverrides.taskMode != null;
          behaviorHtml += '<div class="settings-control">';
          behaviorHtml += '<div class="settings-control-header"><span class="settings-control-label">Task mode</span>' + sourceBadge(taskModeOverride);
          if (isProject && taskModeOverride) {
            behaviorHtml += '<button data-ts-action="clearProjectOverride" data-field="taskMode" class="settings-chip" style="font-size:11px;margin-left:auto">Clear override</button>';
          }
          behaviorHtml += '</div>';
          behaviorHtml += '<div class="settings-chip-row">';
          ['auto', 'suggest', 'manual', 'off'].forEach(function(mode) {
            var active = mode === taskMode ? ' active' : '';
            var action = isProject ? 'setProjectTaskMode' : 'setTaskMode';
            behaviorHtml += '<button data-ts-action="' + action + '" data-mode="' + esc(mode) + '" class="settings-chip' + active + '">' + esc(mode) + '</button>';
          });
          behaviorHtml += '</div></div>';
          if (!isProject) {
            behaviorHtml += '<div class="settings-control">';
            behaviorHtml += '<div class="settings-control-header"><span class="settings-control-label">Proactivity level</span></div>';
            behaviorHtml += '<div class="settings-chip-row">';
            ['high', 'medium', 'low'].forEach(function(level) {
              var active = level === proactivity ? ' active' : '';
              behaviorHtml += '<button data-ts-action="setProactivity" data-level="' + esc(level) + '" class="settings-chip' + active + '">' + esc(level) + '</button>';
            });
            behaviorHtml += '</div></div>';
          }
          behaviorEl.innerHTML = behaviorHtml;
        }

        var retentionEl = document.getElementById('settings-retention');
        if (retentionEl) {
          var ret = effectiveRetention;
          var retHtml = '';
          function retRow(label, field, value, note) {
            var isOverride = isProject && rawOverrides && rawOverrides.retentionPolicy && rawOverrides.retentionPolicy[field] !== undefined;
            retHtml += '<div class="settings-control">';
            retHtml += '<div class="settings-control-header"><span class="settings-control-label">' + esc(label) + '</span>' + sourceBadge(isOverride);
            retHtml += '<span class="settings-control-value" style="margin-left:auto">' + esc(String(value != null ? value : '—')) + '</span>';
            if (isProject && isOverride) {
              retHtml += '<button data-ts-action="clearProjectOverride" data-field="' + esc(field) + '" class="settings-chip" style="font-size:11px">Clear</button>';
            }
            retHtml += '</div>';
            if (note) retHtml += '<div class="settings-control-note">' + esc(note) + '</div>';
            // Show editable inputs for both global and per-project
            retHtml += '<div style="display:flex;gap:8px;align-items:center;margin-top:8px">' +
              '<input type="number" id="ret-input-' + esc(field) + '" value="' + esc(String(value != null ? value : '')) + '" style="width:100px;border:1px solid var(--border);border-radius:var(--radius-sm);padding:4px 8px;font-size:var(--text-sm);background:var(--surface);color:var(--ink)">' +
              '<button data-ts-action="' + (isProject ? 'setProjectRetention' : 'setGlobalRetention') + '" data-field="' + esc(field) + '" class="settings-chip active" style="font-size:11px">Set</button>' +
              '</div>';
            retHtml += '</div>';
          }
          retRow('TTL days', 'ttlDays', ret.ttlDays, 'Memories older than this are eligible for pruning.');
          retRow('Retention days', 'retentionDays', ret.retentionDays, 'Hard cutoff — memories past this age are removed.');
          retRow('Auto-accept threshold', 'autoAcceptThreshold', ret.autoAcceptThreshold, 'Confidence score (0–1) above which memories are auto-accepted.');
          retRow('Min inject confidence', 'minInjectConfidence', ret.minInjectConfidence, 'Minimum confidence score to inject a memory into context.');
          var decay = ret.decay || {};
          retRow('Decay @30d', 'decay.d30', decay.d30, 'Confidence multiplier (0–1) applied to memories ~30 days old.');
          retRow('Decay @60d', 'decay.d60', decay.d60, 'Confidence multiplier (0–1) applied to memories ~60 days old.');
          retRow('Decay @90d', 'decay.d90', decay.d90, 'Confidence multiplier (0–1) applied to memories ~90 days old.');
          retRow('Decay @120d', 'decay.d120', decay.d120, 'Confidence multiplier (0–1) applied to memories ~120 days old.');
          retentionEl.innerHTML = retHtml;
        }

        var workflowEl = document.getElementById('settings-workflow');
        if (workflowEl) {
          var wf = effectiveWorkflow;
          var wfHtml = '';
          var lctOverride = isProject && rawOverrides && rawOverrides.workflowPolicy && rawOverrides.workflowPolicy.lowConfidenceThreshold !== undefined;
          var riskySectionsOverride = isProject && rawOverrides && rawOverrides.workflowPolicy && Array.isArray(rawOverrides.workflowPolicy.riskySections) && rawOverrides.workflowPolicy.riskySections.length > 0;
          wfHtml += '<div class="settings-control">';
          wfHtml += '<div class="settings-control-header"><span class="settings-control-label">Low confidence threshold</span>' + sourceBadge(lctOverride);
          wfHtml += '<span class="settings-control-value" style="margin-left:auto">' + esc(String(wf.lowConfidenceThreshold != null ? wf.lowConfidenceThreshold : '—')) + '</span>';
          if (isProject && lctOverride) {
            wfHtml += '<button data-ts-action="clearProjectOverride" data-field="lowConfidenceThreshold" class="settings-chip" style="font-size:11px">Clear</button>';
          }
          // Show editable inputs for both global and per-project
          wfHtml += '</div><div style="display:flex;gap:8px;align-items:center;margin-top:8px">' +
            '<input type="number" id="wf-input-lowConfidenceThreshold" min="0" max="1" step="0.05" value="' + esc(String(wf.lowConfidenceThreshold != null ? wf.lowConfidenceThreshold : '')) + '" style="width:100px;border:1px solid var(--border);border-radius:var(--radius-sm);padding:4px 8px;font-size:var(--text-sm);background:var(--surface);color:var(--ink)">' +
            '<button data-ts-action="' + (isProject ? 'setProjectWorkflow' : 'setGlobalWorkflow') + '" data-field="lowConfidenceThreshold" class="settings-chip active" style="font-size:11px">Set</button>' +
            '</div>';
          wfHtml += '<div class="settings-control-note">Memories below this confidence score are flagged for review.</div></div>';
          wfHtml += '<div class="settings-control">';
          wfHtml += '<div class="settings-control-header"><span class="settings-control-label">Risky sections</span>' + sourceBadge(riskySectionsOverride);
          if (isProject && riskySectionsOverride) {
            wfHtml += '<button data-ts-action="clearProjectOverride" data-field="riskySections" class="settings-chip" style="font-size:11px;margin-left:auto">Clear</button>';
          }
          wfHtml += '</div><div style="display:flex;gap:8px;align-items:center;margin-top:8px">' +
            '<input type="text" id="wf-input-riskySections" value="' + esc(Array.isArray(wf.riskySections) ? wf.riskySections.join(', ') : '') + '" placeholder="Review, Stale, Conflicts" style="flex:1;min-width:0;border:1px solid var(--border);border-radius:var(--radius-sm);padding:4px 8px;font-size:var(--text-sm);background:var(--surface);color:var(--ink)">' +
            '<button data-ts-action="' + (isProject ? 'setProjectWorkflow' : 'setGlobalWorkflow') + '" data-field="riskySections" class="settings-chip active" style="font-size:11px">Set</button>' +
            '</div>';
          wfHtml += '<div class="settings-control-note">Comma-separated sections that trigger approval gates when memories are written (Review, Stale, Conflicts).</div></div>';
          workflowEl.innerHTML = wfHtml;
        }

        var indexEl = document.getElementById('settings-index');
        if (indexEl && !isProject) {
          var idx = data.indexPolicy || {};
          var idxHtml = '';
          function idxGlobRow(label, field, note) {
            var val = Array.isArray(idx[field]) ? idx[field].join(', ') : '';
            idxHtml += '<div class="settings-control">';
            idxHtml += '<div class="settings-control-header"><span class="settings-control-label">' + esc(label) + '</span></div>';
            idxHtml += '<div class="settings-control-note">' + esc(note) + '</div>';
            idxHtml += '<div style="display:flex;gap:8px;align-items:center;margin-top:8px">' +
              '<input type="text" id="idx-input-' + esc(field) + '" value="' + esc(val) + '" style="flex:1;min-width:0;border:1px solid var(--border);border-radius:var(--radius-sm);padding:4px 8px;font-size:var(--text-sm);background:var(--surface);color:var(--ink)">' +
              '<button data-ts-action="setGlobalIndex" data-field="' + esc(field) + '" class="settings-chip active" style="font-size:11px">Set</button>' +
              '</div></div>';
          }
          idxGlobRow('Include globs', 'includeGlobs', 'Comma-separated glob patterns the indexer reads.');
          idxGlobRow('Exclude globs', 'excludeGlobs', 'Comma-separated glob patterns the indexer skips. Excludes win over includes.');
          idxHtml += '<div class="settings-control"><div class="settings-control-header"><span class="settings-control-label">Index hidden files</span>';
          idxHtml += '<button data-ts-action="toggleIndexHidden" data-enabled="' + (idx.includeHidden ? 'true' : 'false') + '" class="settings-chip' + (idx.includeHidden ? ' active' : '') + '" style="margin-left:auto">' + (idx.includeHidden ? 'On' : 'Off') + '</button></div>';
          idxHtml += '<div class="settings-control-note">When on, dotfiles and dot-directories are eligible for indexing.</div></div>';
          indexEl.innerHTML = idxHtml;
        } else if (indexEl && isProject) {
          indexEl.innerHTML = '<div style="color:var(--muted);font-size:var(--text-sm)">Index policy is global — switch to Global scope to edit it.</div>';
        }

        var accessEl = document.getElementById('settings-access');
        if (accessEl) {
          var accFields = (data.view && data.view.fields) ? data.view.fields : {};
          function accVal(role) {
            if (isProject) {
              return (rawOverrides && rawOverrides.access && Array.isArray(rawOverrides.access[role])) ? rawOverrides.access[role].join(', ') : '';
            }
            var f = accFields['access.' + role];
            return (f && Array.isArray(f.value)) ? f.value.join(', ') : '';
          }
          var accHtml = '';
          function accRow(label, role, note) {
            accHtml += '<div class="settings-control">';
            accHtml += '<div class="settings-control-header"><span class="settings-control-label">' + esc(label) + '</span></div>';
            accHtml += '<div class="settings-control-note">' + esc(note) + '</div>';
            accHtml += '<div style="display:flex;gap:8px;align-items:center;margin-top:8px">' +
              '<input type="text" id="access-input-' + esc(role) + '" value="' + esc(accVal(role)) + '" placeholder="comma,separated actors" style="flex:1;min-width:0;border:1px solid var(--border);border-radius:var(--radius-sm);padding:4px 8px;font-size:var(--text-sm);background:var(--surface);color:var(--ink)">' +
              '<button data-ts-action="setAccessRole" data-role="' + esc(role) + '" class="settings-chip active" style="font-size:11px">Set</button>' +
              '</div></div>';
          }
          accRow('Admins', 'admins', 'Full access — all actions, including config changes.');
          accRow('Contributors', 'contributors', 'Can add, edit, and remove findings and tasks.');
          accRow('Readers', 'readers', 'Read-only access (search and get).');
          accHtml += '<div class="settings-control-note">' + (isProject
            ? 'Editing the per-project access list for this project. Effective roles are the union of global and project lists.'
            : 'Editing the global access list. Effective roles are the union of global and per-project lists.') + ' All lists empty everywhere = open mode (everyone allowed).</div>';
          accessEl.innerHTML = accHtml;
        }

        var integrationsEl = document.getElementById('settings-integrations');
        if (integrationsEl && !isProject) {
          var tools = Array.isArray(data.hookTools) ? data.hookTools : [];
          var html = '';
          html += '<div class="settings-control-header" style="margin-bottom:10px"><span class="settings-control-label">Global MCP</span>';
          html += '<button data-ts-action="toggleMcpEnabled" data-enabled="' + (data.mcpEnabled ? 'true' : 'false') + '" class="settings-chip' + (data.mcpEnabled ? ' active' : '') + '">' + (data.mcpEnabled ? 'On' : 'Off') + '</button></div>';
          html += '<table class="settings-integrations-table"><thead><tr><th>Tool</th><th>Hooks</th><th>MCP</th><th>Control</th></tr></thead><tbody>';
          tools.forEach(function(tool) {
            var hooksOn = !!tool.enabled;
            var mcpOn = !!data.mcpEnabled;
            html += '<tr>';
            html += '<td><span class="settings-tool">' + esc(tool.tool) + '</span></td>';
            html += '<td><span class="settings-indicator ' + (hooksOn ? 'on' : 'off') + '"></span>' + (hooksOn ? 'enabled' : 'disabled') + '</td>';
            html += '<td><span class="settings-indicator ' + (mcpOn ? 'on' : 'off') + '"></span>' + (mcpOn ? 'enabled' : 'disabled') + '</td>';
            html += '<td><button data-ts-action="toggleIntegrationTool" data-tool="' + esc(tool.tool) + '" class="settings-chip' + (hooksOn ? ' active' : '') + '">' + (hooksOn ? 'Disable' : 'Enable') + '</button></td>';
            html += '</tr>';
          });
          html += '</tbody></table>';
          integrationsEl.innerHTML = html;
        } else if (integrationsEl && isProject) {
          integrationsEl.innerHTML = '<div style="color:var(--muted);font-size:var(--text-sm)">Integration settings are global — switch to Global scope to edit them.</div>';
        }

        // Load stores if on global scope
        if (!isProject) {
          loadStores();
        }
      }).catch(function(err) {
        setSettingsStatus('Failed to load settings: ' + String(err), 'err');
      });
    }

    function loadStores() {
      var storesEl = document.getElementById('settings-stores');
      if (!storesEl) return;
      var url = _tsAuthToken ? tsAuthUrl('/api/stores') : '/api/stores';
      fetch(url).then(function(r) { return r.json(); }).then(function(data) {
        if (!data.ok || !Array.isArray(data.stores)) {
          storesEl.innerHTML = '<div style="color:var(--muted);font-size:var(--text-sm)">No team stores configured.</div>';
          return;
        }
        if (data.stores.length === 0) {
          storesEl.innerHTML = '<div style="color:var(--muted);font-size:var(--text-sm)">No team stores configured.</div>';
          return;
        }
        var html = '';
        data.stores.forEach(function(store) {
          html += '<div class="settings-control" style="border:1px solid var(--border);border-radius:var(--radius-sm);padding:12px;margin-bottom:12px">';
          html += '<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px">';
          html += '<div>';
          html += '<div style="font-weight:600;color:var(--ink)">' + esc(store.name) + '</div>';
          html += '<div style="font-size:var(--text-sm);color:var(--muted)">Role: ' + esc(store.role) + '</div>';
          html += '</div>';
          html += '</div>';
          html += '<div style="font-size:var(--text-xs);color:var(--muted);margin-bottom:10px;font-family:var(--mono);word-break:break-all">' + esc(store.path) + '</div>';
          html += '<div style="font-size:var(--text-sm);color:var(--ink);margin-bottom:6px;font-weight:500">Projects</div>';
          if (Array.isArray(store.availableProjects) && store.availableProjects.length > 0) {
            var subscribed = Array.isArray(store.subscribedProjects) ? store.subscribedProjects : [];
            html += '<div style="display:flex;flex-direction:column;gap:6px">';
            store.availableProjects.forEach(function(proj) {
              var isSubscribed = subscribed.indexOf(proj) !== -1;
              html += '<label style="display:flex;align-items:center;gap:8px;cursor:pointer;font-size:var(--text-sm)">';
              html += '<input type="checkbox" class="store-project-checkbox" data-store="' + esc(store.name) + '" data-project="' + esc(proj) + '" ' + (isSubscribed ? 'checked' : '') + ' style="cursor:pointer;width:16px;height:16px">';
              html += '<span>' + esc(proj) + '</span>';
              html += '</label>';
            });
            html += '</div>';
          } else {
            html += '<div style="color:var(--muted);font-size:var(--text-sm)">No projects available</div>';
          }
          html += '</div>';
        });
        storesEl.innerHTML = html;
      }).catch(function(err) {
        storesEl.innerHTML = '<div style="color:var(--error);font-size:var(--text-sm)">Failed to load stores: ' + esc(String(err)) + '</div>';
      });
    }

    // Hook into switchTab to lazy-load
    var _origSwitchTab = window.switchTab;
    var _tasksLoaded = false;
    var _settingsLoaded = false;
    window.switchTab = function(tab) {
      if (typeof _origSwitchTab === 'function') _origSwitchTab(tab);
      if (tab === 'tasks' && !_tasksLoaded) { _tasksLoaded = true; loadTasks(); }
      if (tab === 'settings' && !_settingsLoaded) { _settingsLoaded = true; loadSettings(); }
    };

    // Handle store project checkbox changes
    document.addEventListener('change', function(e) {
      var target = e.target;
      if (target && target.classList && target.classList.contains('store-project-checkbox')) {
        var storeName = target.getAttribute('data-store');
        var projectName = target.getAttribute('data-project');
        var isChecked = target.checked;
        if (storeName && projectName) {
          handleStoreProjectToggle(storeName, projectName, isChecked);
        }
      }
    });

    function handleStoreProjectToggle(storeName, projectName, isSubscribing) {
      var endpoint = isSubscribing ? '/api/stores/subscribe' : '/api/stores/unsubscribe';
      var csrfUrl = _tsAuthToken ? tsAuthUrl('/api/csrf-token') : '/api/csrf-token';
      fetch(csrfUrl).then(function(r) { return r.json(); }).then(function(csrfData) {
        var payload = { store: storeName, projects: [projectName] };
        var body = new URLSearchParams();
        body.set('store', storeName);
        body.set('projects', JSON.stringify([projectName]));
        if (csrfData.token) body.set('_csrf', csrfData.token);
        var url = _tsAuthToken ? tsAuthUrl(endpoint) : endpoint;
        return fetch(url, { method: 'POST', body: body, headers: { 'content-type': 'application/x-www-form-urlencoded' } });
      }).then(function(r) { return r.json(); }).then(function(data) {
        if (!data.ok) {
          setSettingsStatus((isSubscribing ? 'Subscribe' : 'Unsubscribe') + ' failed: ' + (data.error || ''), 'err');
          loadStores();
          return;
        }
        setSettingsStatus((isSubscribing ? 'Subscribed to' : 'Unsubscribed from') + ' ' + projectName, 'ok');
        loadStores();
      }).catch(function(err) {
        setSettingsStatus('Request failed: ' + String(err), 'err');
      });
    }

    // Event delegation for dynamically generated tasks/settings UI
    document.addEventListener('click', function(e) {
      var target = e.target;
      if (!target || typeof target.closest !== 'function') return;
      var actionEl = target.closest('[data-ts-action]');
      if (!actionEl) return;
      var action = actionEl.getAttribute('data-ts-action');
      if (action === 'editPrerequisites') { editTaskContract(Number(actionEl.getAttribute('data-index')), 'dependencies', 'edit'); }
      else if (action === 'savePrerequisites') { editTaskContract(Number(actionEl.getAttribute('data-index')), 'dependencies', 'save'); }
      else if (action === 'toggleDoneSection') { toggleDoneSection(actionEl); }
      else if (action === 'completeTask') { completeTaskFromUi(actionEl.getAttribute('data-project'), actionEl.getAttribute('data-item')); }
      else if (action === 'removeTask') { removeTaskFromUi(actionEl.getAttribute('data-project'), actionEl.getAttribute('data-item')); }
      else if (action === 'addTask') { addTaskFromUi(actionEl.getAttribute('data-project')); }
      else if (action === 'setFindingSensitivity') { setFindingSensitivity(actionEl.getAttribute('data-level')); }
      else if (action === 'toggleAutoCapture') { setAutoCapture(actionEl.getAttribute('data-enabled') !== 'true'); }
      else if (action === 'setTaskMode') { setTaskMode(actionEl.getAttribute('data-mode')); }
      else if (action === 'setProactivity') { setProactivity(actionEl.getAttribute('data-level')); }
      else if (action === 'toggleMcpEnabled') { setMcpEnabled(actionEl.getAttribute('data-enabled') !== 'true'); }
      else if (action === 'toggleIntegrationTool') { toggleIntegrationTool(actionEl.getAttribute('data-tool')); }
      else if (action === 'setProjectFindingSensitivity') {
        var proj = getSettingsProject();
        var level = actionEl.getAttribute('data-level');
        postProjectOverride(proj, 'findingSensitivity', findingUiToStorage(level || 'medium'), false);
      }
      else if (action === 'setProjectTaskMode') {
        var proj = getSettingsProject();
        postProjectOverride(proj, 'taskMode', actionEl.getAttribute('data-mode') || 'auto', false);
      }
      else if (action === 'clearProjectOverride') {
        var proj = getSettingsProject();
        postProjectOverride(proj, actionEl.getAttribute('data-field') || '', '', true);
      }
      else if (action === 'setProjectRetention') {
        var proj = getSettingsProject();
        var field = actionEl.getAttribute('data-field') || '';
        var inputEl = document.getElementById('ret-input-' + field);
        var val = inputEl ? inputEl.value : '';
        postProjectOverride(proj, field, val, false);
      }
      else if (action === 'setGlobalRetention') {
        var field = actionEl.getAttribute('data-field') || '';
        var inputEl = document.getElementById('ret-input-' + field);
        var val = inputEl ? inputEl.value : '';
        postGlobalRetention(field, val);
      }
      else if (action === 'setProjectRetention') {
        var proj = getSettingsProject();
        var field = actionEl.getAttribute('data-field') || '';
        var inputEl = document.getElementById('ret-input-' + field);
        var val = inputEl ? inputEl.value : '';
        postProjectOverride(proj, field, val, false);
      }
      else if (action === 'setGlobalWorkflow') {
        var field = actionEl.getAttribute('data-field') || '';
        var inputEl = document.getElementById('wf-input-' + field);
        var val = inputEl ? inputEl.value : '';
        postGlobalWorkflow(field, val);
      }
      else if (action === 'setProjectWorkflow') {
        var proj = getSettingsProject();
        var field = actionEl.getAttribute('data-field') || '';
        var inputEl = document.getElementById('wf-input-' + field);
        var val = inputEl ? inputEl.value : '';
        postProjectOverride(proj, field, val, false);
      }
      else if (action === 'setGlobalIndex') {
        var field = actionEl.getAttribute('data-field') || '';
        var inputEl = document.getElementById('idx-input-' + field);
        var payload = {};
        payload[field] = inputEl ? inputEl.value : '';
        postSettings('/api/settings/index-policy', payload, 'Index policy updated.');
      }
      else if (action === 'toggleIndexHidden') {
        var nextHidden = actionEl.getAttribute('data-enabled') !== 'true';
        postSettings('/api/settings/index-policy', { includeHidden: String(nextHidden) }, 'Index policy updated.');
      }
      else if (action === 'setAccessRole') {
        var role = actionEl.getAttribute('data-role') || '';
        var inputEl = document.getElementById('access-input-' + role);
        postAccess(role, inputEl ? inputEl.value : '');
      }
    });

    // Keydown delegation for add-task inputs (Enter key)
    document.addEventListener('keydown', function(e) {
      var target = e.target;
      if (!target || !target.getAttribute) return;
      if (target.getAttribute('data-ts-action') === 'addTaskKeydown' && e.key === 'Enter') {
        addTaskFromUi(target.getAttribute('data-project'));
      }
    });

    window.setFindingSensitivity = function(level) {
      var descriptions = {
        high: 'Capture findings proactively, including minor observations.',
        medium: 'Capture findings that are likely useful.',
        low: 'Capture findings only when clearly significant.',
        minimal: 'Only capture explicitly flagged findings.'
      };
      postSettings('/api/settings/finding-sensitivity', { value: findingUiToStorage(level || 'medium') }, 'Finding sensitivity updated.');
      var desc = document.getElementById('settings-fs-desc');
      if (desc) desc.textContent = descriptions[level] || level;
    };

    window.setAutoCapture = function(enabled) {
      postSettings('/api/settings/auto-capture', { enabled: String(!!enabled) }, 'Auto-capture updated.');
    };

    window.setTaskMode = function(mode) {
      postSettings('/api/settings/task-mode', { value: String(mode || 'auto') }, 'Task mode updated.');
    };

    window.setProactivity = function(level) {
      postSettings('/api/settings/proactivity', { value: String(level || 'high') }, 'Proactivity updated.');
    };

    window.setMcpEnabled = function(enabled) {
      postSettings('/api/settings/mcp-enabled', { enabled: String(!!enabled) }, 'MCP setting updated.');
    };

    window.toggleIntegrationTool = function(tool) {
      postSettings('/api/hook-toggle', { tool: String(tool || '') }, 'Integration updated.');
    };
  })();`;
}
export function renderSearchScript(authToken) {
    return `(function() {
  var _searchAuthToken = ${JSON.stringify(authToken)};
  var _searchProjectsLoaded = false;

  function searchAuthUrl(path) {
    return window._phrenAuthUrl ? window._phrenAuthUrl(path) : (_searchAuthToken ? path + (path.includes('?') ? '&' : '?') + '_auth=' + encodeURIComponent(_searchAuthToken) : path);
  }

  var esc = window._phrenEsc;

  function relativeDate(iso) {
    if (!iso) return '';
    var d = new Date(iso);
    var now = new Date();
    var diff = now.getTime() - d.getTime();
    var days = Math.floor(diff / 86400000);
    if (days < 1) return 'today';
    if (days === 1) return '1d ago';
    if (days < 7) return days + 'd ago';
    if (days < 30) return Math.floor(days / 7) + 'w ago';
    var months = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
    return months[d.getMonth()] + ' ' + d.getDate();
  }

  // Multi-select project filter
  var _selectedProjects = [];
  function getSelectedProjects() { return _selectedProjects.slice(); }
  function toggleProjectFilter(name) {
    var idx = _selectedProjects.indexOf(name);
    if (idx === -1) _selectedProjects.push(name);
    else _selectedProjects.splice(idx, 1);
    renderProjectFilterLabel();
    renderProjectFilterChecks();
  }
  function renderProjectFilterLabel() {
    var btn = document.getElementById('search-project-btn');
    if (!btn) return;
    if (!_selectedProjects.length) btn.textContent = 'All projects';
    else if (_selectedProjects.length === 1) btn.textContent = _selectedProjects[0];
    else btn.textContent = _selectedProjects.length + ' projects';
  }
  function renderProjectFilterChecks() {
    var items = document.querySelectorAll('#search-project-dropdown input[type=checkbox]');
    for (var i = 0; i < items.length; i++) {
      items[i].checked = _selectedProjects.indexOf(items[i].value) !== -1;
    }
  }
  window._phrenToggleProjectDropdown = function() {
    var dd = document.getElementById('search-project-dropdown');
    if (dd) dd.style.display = dd.style.display === 'none' ? 'block' : 'none';
  };
  document.addEventListener('click', function(e) {
    var wrap = document.getElementById('search-project-wrap');
    var dd = document.getElementById('search-project-dropdown');
    if (wrap && dd && !wrap.contains(e.target)) dd.style.display = 'none';
  });

  function parseResults(lines) {
    var cards = [];
    var current = null;
    for (var i = 0; i < lines.length; i++) {
      var line = lines[i];
      if (!line.trim()) continue;
      if (line.startsWith('[') && line.indexOf(']') > 0) {
        if (current) cards.push(current);
        var bracket = line.indexOf(']');
        var source = line.slice(1, bracket);
        var meta = line.slice(bracket + 1).trim();
        current = { source: source, meta: meta, snippets: [] };
      } else if (line === '(keyword fallback)') {
        // skip
      } else if (current) {
        current.snippets.push(line);
      } else {
        cards.push({ source: '', meta: '', snippets: [line] });
      }
    }
    if (current) cards.push(current);
    return cards;
  }
  function renderCards(cards) {
    var html = '';
    for (var c = 0; c < cards.length; c++) {
      var card = cards[c];
      html += '<div class="card" style="margin-bottom:8px">';
      html += '<div class="card-header" style="padding:10px 14px;display:flex;align-items:center">';
      if (card.source) {
        html += '<span style="font-weight:500;font-size:var(--text-sm)">' + esc(card.source) + '</span>';
      }
      if (card.meta) {
        html += '<span class="text-muted" style="font-size:var(--text-xs);margin-left:8px">' + esc(card.meta) + '</span>';
      }
      html += '</div>';
      if (card.snippets.length) {
        html += '<div class="card-body" style="padding:10px 14px;font-size:var(--text-sm);white-space:pre-wrap;color:var(--ink-secondary)">';
        html += esc(card.snippets.join('\\n'));
        html += '</div>';
      }
      html += '</div>';
    }
    return html;
  }

  function doSearch() {
    var q = document.getElementById('search-query').value.trim();
    if (!q) return;
    var projects = getSelectedProjects();
    var type = document.getElementById('search-type-filter').value;
    var statusEl = document.getElementById('search-status');
    var resultsEl = document.getElementById('search-results');
    statusEl.textContent = 'Searching...';
    resultsEl.innerHTML = '';

    var fetches = [];
    if (projects.length <= 1) {
      var url = '/api/search?q=' + encodeURIComponent(q) + '&limit=20';
      if (projects.length === 1) url += '&project=' + encodeURIComponent(projects[0]);
      if (type) url += '&type=' + encodeURIComponent(type);
      fetches.push(fetch(searchAuthUrl(url)).then(function(r) { return r.json(); }));
    } else {
      for (var pi = 0; pi < projects.length; pi++) {
        (function(proj) {
          var purl = '/api/search?q=' + encodeURIComponent(q) + '&limit=20&project=' + encodeURIComponent(proj);
          if (type) purl += '&type=' + encodeURIComponent(type);
          fetches.push(fetch(searchAuthUrl(purl)).then(function(r) { return r.json(); }));
        })(projects[pi]);
      }
    }

    Promise.all(fetches).then(function(results) {
      var allCards = [];
      var hasError = false;
      for (var ri = 0; ri < results.length; ri++) {
        if (!results[ri].ok) { hasError = true; continue; }
        var parsed = parseResults(results[ri].results || []);
        allCards = allCards.concat(parsed);
      }
      if (!allCards.length) {
        statusEl.textContent = hasError ? 'Search error.' : 'No results.';
        resultsEl.innerHTML = '<div style="padding:40px;color:var(--muted);text-align:center">' + (hasError ? 'Search failed' : 'No results for \\u201c' + esc(q) + '\\u201d') + '</div>';
        return;
      }
      statusEl.textContent = allCards.length + ' result(s)';
      resultsEl.innerHTML = renderCards(allCards);
    }).catch(function(err) {
      statusEl.textContent = '';
      resultsEl.innerHTML = '<div style="padding:24px;color:var(--muted);text-align:center">Search error: ' + esc(String(err)) + '</div>';
    });
  }

  // Populate project filter dropdown
  function loadSearchProjects() {
    if (_searchProjectsLoaded) return;
    _searchProjectsLoaded = true;
    fetch(searchAuthUrl('/api/projects')).then(function(r) { return r.json(); }).then(function(data) {
      if (!data.ok) return;
      var dd = document.getElementById('search-project-dropdown');
      if (!dd) return;
      var html = '';
      (data.projects || []).forEach(function(p) {
        html += '<label class="search-ms-item" style="display:flex;align-items:center;gap:8px;padding:6px 12px;cursor:pointer;font-size:var(--text-sm);white-space:nowrap"><input type="checkbox" value="' + esc(p.name) + '" style="accent-color:var(--accent);cursor:pointer" /><span>' + esc(p.name) + '</span></label>';
      });
      dd.innerHTML = html;
      dd.querySelectorAll('input[type=checkbox]').forEach(function(cb) {
        cb.addEventListener('change', function() { toggleProjectFilter(cb.value); });
      });
    }).catch(function() {});
  }

  // Wire up events
  var searchBtn = document.getElementById('search-btn');
  var searchInput = document.getElementById('search-query');
  if (searchBtn) searchBtn.addEventListener('click', doSearch);
  if (searchInput) searchInput.addEventListener('keydown', function(e) {
    if (e.key === 'Enter') doSearch();
  });

  // Hook into switchTab to lazy-load project filter
  var _searchOrigSwitchTab = window.switchTab;
  window.switchTab = function(tab) {
    if (typeof _searchOrigSwitchTab === 'function') _searchOrigSwitchTab(tab);
    if (tab === 'search') {
      loadSearchProjects();
      setTimeout(function() { var el = document.getElementById('search-query'); if (el) el.focus(); }, 50);
    }
  };
})();`;
}
export function renderEventWiringScript() {
    return `(function() {
  // --- Navigation tabs ---
  document.querySelectorAll('.nav-item[data-tab]').forEach(function(btn) {
    btn.addEventListener('click', function() { switchTab(btn.getAttribute('data-tab')); });
  });

  // --- Header buttons ---
  var themeBtn = document.getElementById('theme-toggle');
  if (themeBtn) themeBtn.addEventListener('click', function() { toggleTheme(); });

  var cmdpalOpenBtn = document.getElementById('cmdpal-open-btn');
  if (cmdpalOpenBtn) {
    cmdpalOpenBtn.addEventListener('click', function() { openCmdPal(); });
    cmdpalOpenBtn.addEventListener('mouseover', function() { this.style.color='var(--ink)'; this.style.borderColor='var(--muted)'; });
    cmdpalOpenBtn.addEventListener('mouseout', function() { this.style.color='var(--muted)'; this.style.borderColor='var(--border)'; });
  }

  // --- Projects search ---
  var projectsSearch = document.getElementById('projects-search');
  if (projectsSearch) projectsSearch.addEventListener('input', function() { filterProjects(this.value); });

  // --- Review filters ---
  var reviewFilterProject = document.getElementById('review-filter-project');
  if (reviewFilterProject) reviewFilterProject.addEventListener('change', function() { filterReviewCards(); });

  var highlightBtn = document.getElementById('highlight-only-btn');
  if (highlightBtn) highlightBtn.addEventListener('change', function() { filterReviewCards(); });

  var selectAllCb = document.getElementById('review-select-all-cb');
  if (selectAllCb) selectAllCb.addEventListener('change', function() { toggleSelectAll(this.checked); });

  // Batch bar buttons
  document.addEventListener('click', function(e) {
    var target = e.target;
    if (!target || typeof target.closest !== 'function') return;
    var actionEl = target.closest('[data-batch-action]');
    if (!actionEl) return;
    var action = actionEl.getAttribute('data-batch-action');
    if (action === 'approve') { batchAction('approve'); }
    else if (action === 'reject') { batchAction('reject'); }
    else if (action === 'clear') { clearBatchSelection(); }
  });

  // The legacy hook rows use .hook-item rather than .split-item. Their original
  // delegated handler therefore passes null to selectHookFromEl; wire the actual
  // row here so lifecycle hook details remain selectable.
  document.addEventListener('click', function(e) {
    var target = e.target;
    if (!target || typeof target.closest !== 'function') return;
    var hook = target.closest('.hook-item[data-ui-action="selectHookFromEl"]');
    if (hook && typeof selectHookFromEl === 'function') selectHookFromEl(hook);
  });

  // Keep review-card action metadata aligned with the queue line after an edit.
  // The base submit handler sends the old line first; this listener then updates
  // the DOM identity used by subsequent approve/reject actions.
  document.addEventListener('submit', function(e) {
    var form = e.target;
    if (!form || form.getAttribute('data-ui-action') !== 'reviewEditSubmit') return;
    var card = form.closest('.review-card');
    var textarea = form.querySelector('textarea[name="new_text"]');
    if (!card || !textarea) return;
    var oldLine = form.getAttribute('data-line') || '';
    var text = String(textarea.value || '').replace(/[\\r\\n]+/g, ' ').trim();
    if (!text) return;
    var dateMatch = oldLine.match(/^- \\[(\\d{4}-\\d{2}-\\d{2})\\]\\s*/);
    var updatedLine = dateMatch ? '- [' + dateMatch[1] + '] ' + text : '- ' + text;
    var project = card.getAttribute('data-project') || form.getAttribute('data-project') || '';
    card.setAttribute('data-key', project + '\\\\x00' + updatedLine);
    form.setAttribute('data-line', updatedLine);
    card.querySelectorAll('[data-line]').forEach(function(el) { el.setAttribute('data-line', updatedLine); });
  });

  // --- Graph controls ---
  var graphZoomIn = document.getElementById('graph-zoom-in');
  if (graphZoomIn) graphZoomIn.addEventListener('click', function() { graphZoom(1.2); });
  var graphZoomOut = document.getElementById('graph-zoom-out');
  if (graphZoomOut) graphZoomOut.addEventListener('click', function() { graphZoom(0.8); });
  var graphResetBtn = document.getElementById('graph-reset');
  if (graphResetBtn) graphResetBtn.addEventListener('click', function() { graphReset(); });
  var graphResetLayoutBtn = document.getElementById('graph-reset-layout');
  if (graphResetLayoutBtn) graphResetLayoutBtn.addEventListener('click', function() { if (typeof graphResetLayout === 'function') graphResetLayout(); });

  // --- Tasks filters ---
  var tasksFilterProject = document.getElementById('tasks-filter-project');
  if (tasksFilterProject) tasksFilterProject.addEventListener('change', function() { filterTasks(); });
  document.addEventListener('change', function(e) { var el = e.target; if (el && el.hasAttribute('data-task-field')) editTaskContract(Number(el.getAttribute('data-index')), el.getAttribute('data-task-field'), el.value); });
  var taskResponsibilityFilter = document.getElementById('tasks-filter-responsibility');
  if (taskResponsibilityFilter) taskResponsibilityFilter.addEventListener('change', function() { filterTasks(); });
  var tasksFilterSection = document.getElementById('tasks-filter-section');
  if (tasksFilterSection) tasksFilterSection.addEventListener('change', function() { filterTasks(); });

  // --- Mascot click animation ---
  var mascotSvg = document.querySelector('.header-brand svg');
  if (mascotSvg) {
    mascotSvg.addEventListener('click', function() {
      mascotSvg.classList.remove('popped');
      void mascotSvg.offsetWidth;
      mascotSvg.classList.add('popped');
      mascotSvg.addEventListener('animationend', function handler() {
        mascotSvg.classList.remove('popped');
        mascotSvg.removeEventListener('animationend', handler);
      });
    });
  }

  // --- Command palette ---
  var cmdpal = document.getElementById('cmdpal');
  if (cmdpal) cmdpal.addEventListener('click', function(e) { closeCmdPal(e); });
  var cmdpalBox = document.getElementById('cmdpal-box');
  if (cmdpalBox) cmdpalBox.addEventListener('click', function(e) { e.stopPropagation(); });
  var cmdpalInput = document.getElementById('cmdpal-input');
  if (cmdpalInput) {
    cmdpalInput.addEventListener('input', function() { cmdpalSearch(this.value); });
    cmdpalInput.addEventListener('keydown', function(e) { cmdpalKey(e); });
  }
})();`;
}
export function renderGraphHostScript() {
    return `(function() {
  var currentNode = null;
  var editMode = null;

  function graphApi() {
    return window.phrenGraph || null;
  }

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

  function graphData() {
    var api = graphApi();
    return api && api.getData ? api.getData() : { nodes: [], links: [], topics: [], total: 0 };
  }

  function authToken() {
    try {
      return new URL(window.location.href).searchParams.get('_auth') || '';
    } catch {
      return '';
    }
  }

  function authUrl(path) {
    var token = authToken();
    if (!token) return path;
    return path + (path.indexOf('?') === -1 ? '?' : '&') + '_auth=' + encodeURIComponent(token);
  }

  function graphToast(message, type) {
    var container = document.getElementById('toast-container');
    if (!container) return;
    var toast = document.createElement('div');
    toast.className = 'toast' + (type ? ' ' + type : '');
    toast.textContent = message;
    container.appendChild(toast);
    setTimeout(function() {
      if (toast.parentNode) toast.parentNode.removeChild(toast);
    }, 2600);
  }

  function graphUndoToast(message, onUndo) {
    var container = document.getElementById('toast-container');
    if (!container) return;
    var toast = document.createElement('div');
    toast.className = 'toast';
    toast.style.display = 'flex';
    toast.style.alignItems = 'center';
    toast.style.gap = '10px';
    var msg = document.createElement('span');
    msg.textContent = message;
    var btn = document.createElement('button');
    btn.type = 'button';
    btn.textContent = 'Undo';
    btn.style.cssText = 'cursor:pointer;border:1px solid var(--accent,#67e8f9);background:transparent;color:var(--accent,#67e8f9);border-radius:6px;padding:2px 10px;font:inherit;font-size:12px';
    var remove = function() { if (toast.parentNode) toast.parentNode.removeChild(toast); };
    btn.addEventListener('click', function() { remove(); onUndo(); });
    toast.appendChild(msg);
    toast.appendChild(btn);
    container.appendChild(toast);
    setTimeout(remove, 8000);
  }

  function fetchCsrfToken() {
    return fetch(authUrl('/api/csrf-token')).then(function(r) { return r.json(); }).then(function(data) {
      return data && data.ok ? (data.token || null) : null;
    }).catch(function() { return null; });
  }

  function formBody(fields, csrfToken) {
    var parts = [];
    Object.keys(fields).forEach(function(key) {
      var value = fields[key];
      if (value === undefined || value === null) return;
      parts.push(encodeURIComponent(key) + '=' + encodeURIComponent(String(value)));
    });
    if (csrfToken) parts.push('_csrf=' + encodeURIComponent(csrfToken));
    return parts.join('&');
  }

  function graphRequest(path, method, fields) {
    return fetchCsrfToken().then(function(csrfToken) {
      return fetch(authUrl(path), {
        method: method,
        headers: { 'content-type': 'application/x-www-form-urlencoded' },
        body: formBody(fields || {}, csrfToken)
      }).then(function(r) { return r.json(); });
    });
  }

  function hidePopover() {
    currentNode = null;
    editMode = null;
    var popover = document.getElementById('graph-node-popover');
    if (popover) popover.style.display = 'none';
    if (typeof window.graphClearSelection === 'function') window.graphClearSelection();
  }

  // The dossier docks to the right edge of the graph viewport — a stable
  // reading pane instead of a cursor-chasing popover. Signature kept for
  // callers; the x/y hint is no longer needed.
  function ensureDossierResize(popover) {
    if (!popover || popover.querySelector('.phren-dossier-resize')) return;
    var handle = document.createElement('div');
    handle.className = 'phren-dossier-resize';
    handle.setAttribute('aria-hidden', 'true');
    handle.addEventListener('pointerdown', function(ev) {
      ev.preventDefault();
      ev.stopPropagation();
      handle.classList.add('dragging');
      try { handle.setPointerCapture(ev.pointerId); } catch (_) { /* ignore */ }
      function move(e) {
        var rect = popover.getBoundingClientRect();
        var w = Math.max(280, Math.min(680, e.clientX - rect.left));
        popover.style.width = w + 'px';
        popover.style.maxWidth = 'none';
      }
      function up() {
        handle.classList.remove('dragging');
        window.removeEventListener('pointermove', move);
        window.removeEventListener('pointerup', up);
      }
      window.addEventListener('pointermove', move);
      window.addEventListener('pointerup', up);
    });
    popover.appendChild(handle);
  }

  function positionPopover(x, y) {
    var popover = document.getElementById('graph-node-popover');
    if (!popover) return;
    popover.classList.add('phren-docked');
    popover.style.left = '';
    popover.style.top = '';
    popover.style.visibility = 'visible';
    popover.style.display = 'block';
    ensureDossierResize(popover);
  }

  function currentPopoverPoint() {
    var popover = document.getElementById('graph-node-popover');
    return {
      x: popover ? parseFloat(popover.style.left || '24') : 24,
      y: popover ? parseFloat(popover.style.top || '24') : 24
    };
  }

  function neighborIds(nodeId) {
    var data = graphData();
    var ids = [];
    (data.links || []).forEach(function(link) {
      if (link.source === nodeId) ids.push(link.target);
      else if (link.target === nodeId) ids.push(link.source);
    });
    return ids;
  }

  function nodeMap() {
    var data = graphData();
    var map = {};
    (data.nodes || []).forEach(function(node) { map[node.id] = node; });
    return map;
  }

  function projectCounts(node) {
    var map = nodeMap();
    var counts = { finding: 0, task: 0, entity: 0, reference: 0, other: 0 };
    neighborIds(node.id).forEach(function(id) {
      var neighbor = map[id];
      if (!neighbor) return;
      var kind = neighbor.kind || 'other';
      if (counts[kind] === undefined) counts.other++;
      else counts[kind]++;
    });
    return counts;
  }

  function kindLabel(node) {
    if (!node) return '';
    if (node.kind === 'entity') return node.entityType ? 'Fragment · ' + node.entityType : 'Fragment';
    if (node.kind === 'reference') return 'Reference';
    if (node.kind === 'task') return 'Task';
    if (node.kind === 'project') return 'Project';
    if (node.kind === 'finding') return node.topicLabel ? 'Finding · ' + node.topicLabel : 'Finding';
    return node.kind || 'Node';
  }

  function findingSiblings(node) {
    var data = graphData();
    return (data.nodes || []).filter(function(candidate) {
      return candidate.kind === 'finding' && candidate.projectName === node.projectName;
    }).sort(function(a, b) { return a.id < b.id ? -1 : a.id > b.id ? 1 : 0; });
  }

  function relatedFindings(node) {
    return findingSiblings(node).filter(function(candidate) {
      return candidate.id !== node.id && candidate.topicSlug && candidate.topicSlug === node.topicSlug;
    }).slice(0, 4);
  }

  // Neighbors of the node from the live graph links, scored so the panel can
  // list "strongest relationships" like GraphRAG. Entities/findings first.
  function strongestRelationships(node) {
    var map = nodeMap();
    var ids = neighborIds(node.id);
    var out = [];
    for (var i = 0; i < ids.length; i++) {
      var nb = map[ids[i]];
      if (!nb || nb.kind === 'project') continue;
      var w = 0.6;
      if (nb.kind === 'entity') w = 0.7 + Math.min(0.28, (nb.refCount || 0) * 0.03);
      else if (nb.kind === 'finding') w = typeof nb.qualityScore === 'number' ? 0.55 + nb.qualityScore * 0.4 : 0.6;
      var name = nb.displayLabel || nb.label || nb.id;
      var sub = nb.kind === 'entity' ? ((nb.refCount || 0) + ' references') : (nb.tooltipLabel || nb.fullLabel || '');
      if (sub === name) sub = '';
      out.push({ id: nb.id, name: name, sub: sub, w: w });
    }
    // same-topic siblings that aren't already linked, as softer relationships.
    // Dedupe by display TEXT too — stores can hold near-duplicate findings and
    // two identical rows read as a rendering bug.
    var seen = {}; var seenName = {};
    out.forEach(function(r) { seen[r.id] = true; seenName[r.name.toLowerCase()] = true; });
    relatedFindings(node).forEach(function(rel) {
      if (seen[rel.id]) return;
      var relName = rel.displayLabel || rel.label || rel.id;
      if (seenName[relName.toLowerCase()]) return;
      seenName[relName.toLowerCase()] = true;
      var relSub = 'same topic' + (rel.projectName && rel.projectName !== node.projectName ? ' · ' + rel.projectName : '');
      out.push({ id: rel.id, name: relName, sub: relSub, w: 0.5 });
    });
    out.sort(function(a, b) { return b.w - a.w; });
    return out.slice(0, 6);
  }

  function gotoChip(label, nodeId, accent) {
    var border = accent ? 'var(--accent)' : 'var(--border)';
    var bg = accent ? 'var(--accent-dim)' : 'var(--surface-raised)';
    return '<span data-graph-goto="' + esc(nodeId) + '" role="button" tabindex="0" style="display:inline-flex;align-items:center;gap:6px;padding:4px 9px;border-radius:999px;border:1px solid ' + border + ';background:' + bg + ';font-size:11px;color:var(--ink);cursor:pointer;max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">' + esc(label) + '</span>';
  }

  function chip(text, accent) {
    var border = accent ? 'var(--accent)' : 'var(--border)';
    var bg = accent ? 'var(--accent-dim)' : 'var(--surface-raised)';
    return '<span style="display:inline-flex;align-items:center;gap:6px;padding:4px 9px;border-radius:999px;border:1px solid ' + border + ';background:' + bg + ';font-size:11px;color:var(--ink)">' + esc(text) + '</span>';
  }

  function scoreLine(node) {
    var score = typeof node.qualityScore === 'number' ? Math.round(node.qualityScore * 100) : null;
    return score ? chip('Quality ' + score, false) : '';
  }

  function docChip(doc) {
    var border = 'var(--border)';
    var bg = 'var(--surface-raised)';
    return '<span data-doc-click="' + esc(doc) + '" style="display:inline-flex;align-items:center;gap:6px;padding:4px 9px;border-radius:999px;border:1px solid ' + border + ';background:' + bg + ';font-size:11px;color:var(--accent);cursor:pointer;text-decoration:underline dotted" title="Search for ' + esc(doc) + '">' + esc(doc) + '</span>';
  }
  function docsList(node) {
    var docs = (node.refDocs || []).map(function(ref) { return ref.doc; });
    if (!docs.length) return '';
    return '<div style="display:flex;flex-wrap:wrap;gap:8px">' + docs.slice(0, 12).map(function(doc) { return docChip(doc); }).join('') + '</div>';
  }

  // Icon paths for the dossier header's icon buttons: pencil, trash,
  // previous/next chevrons, close. Stroke follows currentColor.
  var DOSSIER_ICONS = {
    edit: '<path d="M4 16l3.4-.8L16 6.6a1.8 1.8 0 0 0-2.6-2.6l-8.6 8.6L4 16Z"></path><path d="m12.3 5.1 2.6 2.6M4.8 12.6l2.6 2.6"></path>',
    delete: '<path d="M3.5 5.5h13M8 5.5V3.8h4v1.7M5.5 5.5l.7 10.7h7.6l.7-10.7M8 8.5v4.8M12 8.5v4.8"></path>',
    'prev-node': '<path d="m12.5 5-5 5 5 5"></path>',
    'next-node': '<path d="m7.5 5 5 5-5 5"></path>',
    close: '<path d="m5 5 10 10M15 5 5 15"></path>'
  };

  function dossierIconBtn(action, label, opts) {
    opts = opts || {};
    return '<button type="button" class="phren-dossier-iconbtn' + (opts.danger ? ' danger' : '') + '"'
      + (opts.id ? ' id="' + opts.id + '"' : '')
      + ' data-graph-action="' + action + '" aria-label="' + label + '" title="' + label + '"'
      + ' style="width:44px;height:44px">'
      + '<svg viewBox="0 0 20 20" aria-hidden="true" focusable="false">' + DOSSIER_ICONS[action] + '</svg></button>';
  }

  // The header row: kind/title/chips on the left, icon buttons on the right
  // beside Close. Prev/Next walk the ranked list the list mode shows; Edit
  // and Delete replace the old text buttons in the actions row.
  function dossierHeader(node) {
    var title = node.displayLabel || node.label || node.tooltipLabel || node.id;
    var meta = [kindLabel(node)];
    if (node.projectName) meta.push(node.projectName);
    if (node.kind === 'task' && node.section) meta.push(node.section);
    if (node.kind === 'task' && node.priority) meta.push('Priority ' + node.priority);
    if (node.kind === 'finding' && node.topicLabel) meta.push(node.topicLabel);
    if (node.kind === 'finding' && node.date) meta.push(node.date);

    var api = graphApi();
    var pos = api && api.dossierPosition ? api.dossierPosition(node.id) : null;
    var editable = node.kind === 'finding' || node.kind === 'task';
    var tools = [];
    if (pos) {
      tools.push(dossierIconBtn('prev-node', 'Previous node'));
      tools.push(dossierIconBtn('next-node', 'Next node'));
      tools.push('<span class="phren-dossier-count">' + (pos.index + 1) + ' of ' + pos.total + '</span>');
    }
    if (editable) {
      tools.push(dossierIconBtn('edit', 'Edit'));
      tools.push(dossierIconBtn('delete', 'Delete', { danger: true }));
    }
    tools.push(dossierIconBtn('close', 'Close selected node', { id: 'graph-node-close' }));

    return '<div class="phren-dossier-head">'
      + '<div style="display:flex;flex-direction:column;gap:8px;flex:1 1 auto;min-width:0">'
      + '<div class="phren-dossier-kind" style="font-size:11px;letter-spacing:.06em;text-transform:uppercase;color:var(--muted)">' + esc(kindLabel(node)) + '</div>'
      + '<div style="font-size:var(--text-lg);font-weight:600;line-height:1.2">' + esc(title) + '</div>'
      + '<div style="display:flex;flex-wrap:wrap;gap:8px">' + meta.filter(Boolean).map(function(item, index) { return chip(item, index === 0); }).join('') + scoreLine(node) + '</div>'
      + '</div>'
      + '<div class="phren-dossier-tools">' + tools.join('') + '</div>'
      + '</div>';
  }

  function renderView(node) {
    var title = node.displayLabel || node.label || node.tooltipLabel || node.id;
    var header = dossierHeader(node);

    var body = '';
    var actions = [];

    if (node.kind === 'project') {
      var counts = projectCounts(node);
      // Server totals beat visible-adjacency counts (filters can hide nodes)
      var findingTotal = typeof node.findingCount === 'number' && node.findingCount > counts.finding ? node.findingCount : counts.finding;
      var taskTotal = typeof node.taskCount === 'number' && node.taskCount > counts.task ? node.taskCount : counts.task;
      // Compact stat line — the detailed findings/tasks browser lives in the
      // contents pane on the right, so the bulky card grid was redundant.
      var stat = function(n, label) {
        return '<span style="display:inline-flex;align-items:baseline;gap:5px"><b style="font-size:var(--text-md);font-weight:700;color:var(--ink)">' + n + '</b><span style="font-size:11px;color:var(--muted);text-transform:uppercase;letter-spacing:.05em">' + label + '</span></span>';
      };
      body += '<div style="display:flex;flex-wrap:wrap;gap:16px;padding:10px 0">'
        + stat(findingTotal, 'findings') + stat(taskTotal, 'tasks')
        + stat(counts.entity, 'fragments') + stat(counts.reference, 'refs')
        + '</div>';
      body += '<div class="text-muted" style="font-size:var(--text-sm)">Browse and prune this project\\'s findings and tasks in the contents pane →</div>';
    } else if (node.kind === 'finding') {
      // GraphRAG-style stats row: links / project / helpful
      var conn = node.connections || {};
      var helpful = node.score && typeof node.score.helpful === 'number' ? node.score.helpful : 0;
      body += '<div class="phren-dossier-stats">'
        + '<div><div class="k">Links</div><div class="v">' + (conn.total || 0) + '</div></div>'
        + '<div><div class="k">Project</div><div class="v" style="font-size:11px;letter-spacing:.04em;text-transform:none">' + esc(node.projectName || '—') + '</div></div>'
        + '<div><div class="k">Helpful</div><div class="v">' + helpful + '</div></div>'
        + '</div>';
      body += '<div class="phren-dossier-section">◈ Description</div>';
      body += '<div id="graph-node-text" class="phren-dossier-text">' + esc(node.tooltipLabel || node.fullLabel || title) + '</div>';
      // Strongest relationships — related same-topic findings + entity neighbors
      var rels = strongestRelationships(node);
      if (rels.length) {
        body += '<div class="phren-dossier-section">⟿ Strongest relationships · ' + rels.length + '</div>';
        body += '<div style="margin-top:8px">' + rels.map(function(r) {
          return '<div class="phren-rel-row" data-graph-goto="' + esc(r.id) + '"><div class="n">' + esc(r.name) + (r.sub ? '<small>' + esc(r.sub) + '</small>' : '') + '</div><div class="w">' + r.w.toFixed(2) + '</div></div>';
        }).join('') + '</div>';
      }
      if (node.projectName) {
        body += '<div class="phren-dossier-section">Community</div>';
        body += '<div style="display:flex;flex-wrap:wrap;gap:8px;margin-top:6px">' + gotoChip(node.projectName, node.projectName, true) + '</div>';
      }
    } else if (node.kind === 'task') {
      body += '<div id="graph-node-text" style="white-space:pre-wrap;line-height:1.65;font-size:var(--text-base)">' + esc(node.tooltipLabel || node.fullLabel || title) + '</div>';
      body += '<div style="display:flex;flex-wrap:wrap;gap:8px;margin-top:12px">';
      body += chip('Status ' + (node.section || 'Queue'), true);
      if (node.priority) body += chip('Priority ' + node.priority, false);
      body += '</div>';
      if ((node.section || '').toLowerCase() !== 'done') actions.push('<button type="button" class="btn btn-sm" data-graph-action="complete">Done</button>');
      if ((node.section || '').toLowerCase() !== 'active') actions.push('<button type="button" class="btn btn-sm" data-graph-action="move-active">Move to Active</button>');
      if ((node.section || '').toLowerCase() !== 'queue') actions.push('<button type="button" class="btn btn-sm" data-graph-action="move-queue">Move to Queue</button>');
    } else if (node.kind === 'entity') {
      if (node.connectedProjects && node.connectedProjects.length) {
        body += '<div style="display:flex;flex-wrap:wrap;gap:8px;margin-bottom:12px">' + node.connectedProjects.map(function(project) { return chip(project, true); }).join('') + '</div>';
      }
      body += docsList(node) || '<div class="text-muted">No linked references.</div>';
    } else if (node.kind === 'reference') {
      body += '<div style="white-space:pre-wrap;line-height:1.6;font-size:var(--text-base)">' + esc(node.tooltipLabel || title) + '</div>';
      body += docsList(node);
    } else {
      body += '<div style="white-space:pre-wrap;line-height:1.6;font-size:var(--text-base)">' + esc(node.tooltipLabel || title) + '</div>';
    }

    return header
      + '<div style="display:flex;flex-direction:column;gap:14px;margin-top:16px">' + body + '</div>'
      + (actions.length ? '<div style="display:flex;flex-wrap:wrap;gap:8px;margin-top:18px">' + actions.join('') + '</div>' : '');
  }

  function renderEdit(node) {
    var title = node.kind === 'task' ? 'Edit task' : 'Edit finding';
    var text = node.tooltipLabel || node.fullLabel || node.displayLabel || '';
    var section = node.section || 'Queue';
    var priority = node.priority || '';
    var sectionControls = '';
    var priorityControls = '';
    if (node.kind === 'task') {
      sectionControls = '<label style="display:flex;flex-direction:column;gap:6px;font-size:12px;color:var(--muted)">Status<select id="graph-task-section" style="border:1px solid var(--border);border-radius:8px;padding:8px 10px;background:var(--surface);color:var(--ink)"><option value="Queue"' + (section === 'Queue' ? ' selected' : '') + '>Queue</option><option value="Active"' + (section === 'Active' ? ' selected' : '') + '>Active</option><option value="Done"' + (section === 'Done' ? ' selected' : '') + '>Done</option></select></label>';
      priorityControls = '<label style="display:flex;flex-direction:column;gap:6px;font-size:12px;color:var(--muted)">Priority<select id="graph-task-priority" style="border:1px solid var(--border);border-radius:8px;padding:8px 10px;background:var(--surface);color:var(--ink)"><option value=""' + (!priority ? ' selected' : '') + '>None</option><option value="high"' + (priority === 'high' ? ' selected' : '') + '>High</option><option value="medium"' + (priority === 'medium' ? ' selected' : '') + '>Medium</option><option value="low"' + (priority === 'low' ? ' selected' : '') + '>Low</option></select></label>';
    }
    return '<div class="phren-dossier-head">'
      + '<div style="display:flex;flex-direction:column;gap:8px;flex:1 1 auto;min-width:0">'
      + '<div style="font-size:11px;letter-spacing:.06em;text-transform:uppercase;color:var(--muted)">' + esc(title) + '</div>'
      + '<div style="font-size:var(--text-md);font-weight:600">' + esc(node.projectName || kindLabel(node)) + '</div>'
      + '</div>'
      + '<div class="phren-dossier-tools">' + dossierIconBtn('close', 'Close selected node', { id: 'graph-node-close' }) + '</div>'
      + '</div>'
      + '<div style="display:flex;flex-direction:column;gap:12px;margin-top:16px">'
      + '<textarea id="graph-node-editor" style="min-height:180px;width:100%;border:1px solid var(--border);border-radius:12px;padding:12px 14px;background:var(--surface-sunken);color:var(--ink);font:inherit;line-height:1.55;resize:vertical">' + esc(text) + '</textarea>'
      + (node.kind === 'task' ? '<div style="display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px">' + sectionControls + priorityControls + '</div>' : '')
      + '<div style="display:flex;flex-wrap:wrap;gap:8px"><button type="button" class="btn btn-sm" data-graph-action="save-edit">Save</button><button type="button" class="btn btn-sm" data-graph-action="cancel-edit">Cancel</button></div>'
      + '</div>';
  }

  function renderPopover(node, x, y) {
    currentNode = node;
    var content = document.getElementById('graph-node-content');
    if (!content || !node) {
      currentNode = null;
      editMode = null;
      var popover = document.getElementById('graph-node-popover');
      if (popover) popover.style.display = 'none';
      return;
    }
    content.innerHTML = editMode ? renderEdit(node) : renderView(node);
    bindPopoverActions();
    positionPopover(x, y);
  }

  function matchesNode(node, match) {
    if (!node || !match) return false;
    if (match.id && node.id !== match.id) return false;
    if (match.kind && node.kind !== match.kind) return false;
    if (match.projectName && node.projectName !== match.projectName) return false;
    if (match.tooltipLabel && (node.tooltipLabel || node.fullLabel || '') !== match.tooltipLabel) return false;
    if (match.displayLabel && (node.displayLabel || node.label || '') !== match.displayLabel) return false;
    return true;
  }

  function reloadGraph(match) {
    return fetch(authUrl('/api/graph')).then(function(r) {
      if (!r.ok) throw new Error('HTTP ' + r.status);
      return r.json();
    }).then(function(data) {
      var api = graphApi();
      if (!api || !api.mount) return;
      api.mount(data);
      if (!match) {
        hidePopover();
        return;
      }
      var next = null;
      var dataNodes = api.getData ? api.getData().nodes : [];
      for (var index = 0; index < dataNodes.length; index++) {
        if (matchesNode(dataNodes[index], match)) {
          next = dataNodes[index];
          break;
        }
      }
      if (next && api.focusNode) api.focusNode(next.id);
      else hidePopover();
    });
  }

  function saveFindingEdit() {
    var editor = document.getElementById('graph-node-editor');
    if (!editor || !currentNode) return;
    var nextText = editor.value.trim();
    if (!nextText || nextText === (currentNode.tooltipLabel || currentNode.fullLabel || '').trim()) {
      editMode = null;
      var point = currentPopoverPoint();
      renderPopover(currentNode, point.x, point.y);
      return;
    }
    graphRequest('/api/findings/' + encodeURIComponent(currentNode.projectName), 'PUT', {
      old_text: currentNode.tooltipLabel || currentNode.fullLabel || '',
      new_text: nextText,
      score_key: currentNode.scoreKey || ''
    }).then(function(result) {
      if (!result || !result.ok) throw new Error(result && result.error ? result.error : 'Save failed');
      graphToast('Finding updated', 'ok');
      editMode = null;
      return reloadGraph({ kind: 'finding', projectName: currentNode.projectName, tooltipLabel: nextText });
    }).catch(function(err) {
      graphToast('Update failed: ' + err.message, 'err');
    });
  }

  function saveTaskEdit() {
    var editor = document.getElementById('graph-node-editor');
    var sectionEl = document.getElementById('graph-task-section');
    var priorityEl = document.getElementById('graph-task-priority');
    if (!editor || !currentNode) return;
    var nextText = editor.value.trim();
    if (!nextText) {
      graphToast('Task text cannot be empty', 'err');
      return;
    }
    var updates = { text: nextText };
    if (sectionEl && sectionEl.value) updates.section = sectionEl.value;
    if (priorityEl) updates.priority = priorityEl.value;
    graphRequest('/api/tasks/update', 'POST', {
      project: currentNode.projectName,
      item: currentNode.stableId || currentNode.id || currentNode.tooltipLabel || currentNode.fullLabel || currentNode.displayLabel || '',
      text: updates.text,
      section: updates.section || '',
      priority: updates.priority || ''
    }).then(function(result) {
      if (!result || !result.ok) throw new Error(result && result.error ? result.error : 'Save failed');
      graphToast('Task updated', 'ok');
      editMode = null;
      var nextSection = updates.section || currentNode.section || 'Queue';
      if (nextSection === 'Done') {
        hidePopover();
        return reloadGraph(null);
      }
      return reloadGraph({ kind: 'task', projectName: currentNode.projectName, tooltipLabel: nextText });
    }).catch(function(err) {
      graphToast('Update failed: ' + err.message, 'err');
    });
  }

  // Fire the delete API for one node (no confirm, no reload) — reused by single
  // and batch delete so a bulk prune reloads the graph just once at the end.
  function deleteNodeRequest(node) {
    if (!node) return Promise.resolve({ ok: false, error: 'no node' });
    if (node.kind === 'finding') {
      return graphRequest('/api/findings/' + encodeURIComponent(node.projectName), 'DELETE', {
        text: node.tooltipLabel || node.fullLabel || '',
        score_key: node.scoreKey || ''
      });
    }
    if (node.kind === 'task') {
      return graphRequest('/api/tasks/remove', 'POST', {
        project: node.projectName,
        item: node.stableId || node.id || node.tooltipLabel || node.fullLabel || node.displayLabel || ''
      });
    }
    return Promise.resolve({ ok: false, error: 'unsupported' });
  }

  function deleteNode(node) {
    if (!node) return;
    if (!confirm('Delete this ' + (node.kind || 'node') + '?')) return;
    deleteNodeRequest(node).then(function(result) {
      if (!result || !result.ok) throw new Error(result && result.error ? result.error : 'Delete failed');
      graphToast(node.kind === 'task' ? 'Task removed' : 'Finding deleted', 'ok');
      return reloadGraph(null);
    }).catch(function(err) {
      graphToast('Delete failed: ' + err.message, 'err');
    });
  }

  function reAddNodes(items) {
    return Promise.all(items.map(function(it) {
      if (it.kind === 'finding') {
        return graphRequest('/api/findings/' + encodeURIComponent(it.projectName), 'POST', { text: it.text }).catch(function() { return { ok: false }; });
      }
      if (it.kind === 'task') {
        return graphRequest('/api/tasks/add', 'POST', { project: it.projectName, item: it.item }).catch(function() { return { ok: false }; });
      }
      return Promise.resolve({ ok: false });
    })).then(function() {
      graphToast('Restored ' + items.length + ' items', 'ok');
      return reloadGraph(null);
    });
  }

  function deleteNodes(nodes) {
    if (!nodes || !nodes.length) return;
    if (nodes.length === 1) { deleteNode(nodes[0]); return; }
    if (!confirm('Delete ' + nodes.length + ' items?')) return;
    var undoItems = nodes.map(function(n) {
      return {
        kind: n.kind, projectName: n.projectName,
        text: n.tooltipLabel || n.fullLabel || '',
        item: n.stableId || n.id || n.tooltipLabel || n.fullLabel || n.displayLabel || ''
      };
    });
    Promise.all(nodes.map(function(n) {
      return deleteNodeRequest(n).catch(function() { return { ok: false }; });
    })).then(function(results) {
      var ok = results.filter(function(r) { return r && r.ok; }).length;
      return reloadGraph(null).then(function() {
        graphUndoToast('Deleted ' + ok + ' item' + (ok === 1 ? '' : 's'), function() { reAddNodes(undoItems); });
      });
    });
  }

  // Merge two same-project findings into one: remove the originals, then add
  // their joined text as a single finding. Offers an Undo that restores the
  // originals and removes the merged bullet.
  function mergeNodes(nodes) {
    if (!nodes || nodes.length !== 2) return;
    var proj = nodes[0].projectName;
    var t1 = nodes[0].tooltipLabel || nodes[0].fullLabel || '';
    var t2 = nodes[1].tooltipLabel || nodes[1].fullLabel || '';
    if (!t1 || !t2) return;
    if (!confirm('Merge these 2 findings into one?')) return;
    var merged = t1 + '\\n' + t2;
    Promise.all(nodes.map(function(n) {
      return deleteNodeRequest(n).catch(function() { return { ok: false }; });
    })).then(function() {
      return graphRequest('/api/findings/' + encodeURIComponent(proj), 'POST', { text: merged });
    }).then(function(r) {
      if (!r || !r.ok) throw new Error(r && r.error ? r.error : 'add failed');
      return reloadGraph(null).then(function() {
        graphUndoToast('Merged 2 findings', function() { undoMerge(proj, merged, t1, t2); });
      });
    }).catch(function(err) {
      graphToast('Merge failed: ' + err.message, 'err');
      return reloadGraph(null);
    });
  }

  // Reverse a merge: remove the combined bullet, restore the two originals.
  function undoMerge(proj, merged, t1, t2) {
    graphRequest('/api/findings/' + encodeURIComponent(proj), 'DELETE', { text: merged, score_key: '' })
      .then(function() { return graphRequest('/api/findings/' + encodeURIComponent(proj), 'POST', { text: t1 }); })
      .then(function() { return graphRequest('/api/findings/' + encodeURIComponent(proj), 'POST', { text: t2 }); })
      .then(function() {
        graphToast('Merge undone', 'ok');
        return reloadGraph(null);
      }).catch(function(err) {
        graphToast('Undo failed: ' + err.message, 'err');
        return reloadGraph(null);
      });
  }

  function deleteCurrentNode() {
    deleteNode(currentNode);
  }

  // Persist edits made directly inside the project pane. The pane owns the
  // editor; the host only performs the mutation and updates the rendered node.
  function saveInlineFromPane(node) {
    if (!node || (node.kind !== 'finding' && node.kind !== 'task')) return;
    var nextText = (node.editedText || '').trim();
    if (!nextText) return;
    if (node.kind === 'finding') {
      graphRequest('/api/findings/' + encodeURIComponent(node.projectName), 'PUT', {
        old_text: node.tooltipLabel || node.fullLabel || '',
        new_text: nextText,
        score_key: node.scoreKey || ''
      }).then(function(result) {
        if (!result || !result.ok) throw new Error(result && result.error ? result.error : 'Save failed');
        var api = graphApi();
        if (api && api.updateNode) api.updateNode(node.id, { text: nextText });
        graphToast('Finding updated', 'ok');
      }).catch(function(err) { graphToast('Update failed: ' + err.message, 'err'); });
      return;
    }
    graphRequest('/api/tasks/update', 'POST', {
      project: node.projectName,
      item: node.stableId || node.id || node.tooltipLabel || node.fullLabel || node.displayLabel || '',
      text: nextText,
      section: node.editedSection || node.section || 'Queue',
      priority: typeof node.editedPriority === 'string' ? node.editedPriority : (node.priority || '')
    }).then(function(result) {
      if (!result || !result.ok) throw new Error(result && result.error ? result.error : 'Save failed');
      var api = graphApi();
      if (api && api.updateNode) api.updateNode(node.id, {
        text: nextText,
        section: node.editedSection || node.section || 'Queue',
        priority: typeof node.editedPriority === 'string' ? node.editedPriority : (node.priority || '')
      });
      graphToast('Task updated', 'ok');
    }).catch(function(err) { graphToast('Update failed: ' + err.message, 'err'); });
  }

  function completeCurrentTask() {
    if (!currentNode) return;
    graphRequest('/api/tasks/complete', 'POST', {
      project: currentNode.projectName,
      item: currentNode.stableId || currentNode.id || currentNode.tooltipLabel || currentNode.fullLabel || currentNode.displayLabel || ''
    }).then(function(result) {
      if (!result || !result.ok) throw new Error(result && result.error ? result.error : 'Update failed');
      graphToast('Task completed', 'ok');
      return reloadGraph(null);
    }).catch(function(err) {
      graphToast('Update failed: ' + err.message, 'err');
    });
  }

  function moveCurrentTask(section) {
    if (!currentNode) return;
    graphRequest('/api/tasks/update', 'POST', {
      project: currentNode.projectName,
      item: currentNode.stableId || currentNode.id || currentNode.tooltipLabel || currentNode.fullLabel || currentNode.displayLabel || '',
      section: section
    }).then(function(result) {
      if (!result || !result.ok) throw new Error(result && result.error ? result.error : 'Update failed');
      graphToast('Task moved to ' + section, 'ok');
      return reloadGraph({ kind: 'task', projectName: currentNode.projectName, tooltipLabel: currentNode.tooltipLabel || currentNode.fullLabel || currentNode.displayLabel || '' });
    }).catch(function(err) {
      graphToast('Update failed: ' + err.message, 'err');
    });
  }

  function bindPopoverActions() {
    // Doc reference chips — click to search for the document
    document.querySelectorAll('[data-doc-click]').forEach(function(chip) {
      chip.addEventListener('click', function() {
        var doc = chip.getAttribute('data-doc-click') || '';
        if (!doc) return;
        // Search for this doc in the graph by updating the search filter
        var searchInput = document.querySelector('input[data-search-filter]');
        if (searchInput) {
          searchInput.value = doc.replace(/FINDINGS\\.md$/, '').replace(/\\/$/, '');
          searchInput.dispatchEvent(new Event('input', { bubbles: true }));
        }
      });
    });

    // Related-node chips — fly to the referenced node
    document.querySelectorAll('[data-graph-goto]').forEach(function(chipEl) {
      chipEl.addEventListener('click', function() {
        var target = chipEl.getAttribute('data-graph-goto') || '';
        var api = graphApi();
        if (target && api && api.focusNode) api.focusNode(target);
      });
    });

    document.querySelectorAll('[data-graph-action]').forEach(function(button) {
      button.addEventListener('click', function() {
        var action = button.getAttribute('data-graph-action');
        if (action === 'prev-node' || action === 'next-node') {
          if (!currentNode) return;
          var api = graphApi();
          if (!api || !api.stepDossier) return;
          var nextId = api.stepDossier(currentNode.id, action === 'next-node' ? 1 : -1);
          if (nextId && api.focusNode) api.focusNode(nextId);
          return;
        }
        if (action === 'close') {
          hidePopover();
          return;
        }
        if (action === 'edit') {
          editMode = currentNode && (currentNode.kind === 'finding' || currentNode.kind === 'task') ? currentNode.kind : null;
          var point = currentPopoverPoint();
          renderPopover(currentNode, point.x, point.y);
        } else if (action === 'cancel-edit') {
          editMode = null;
          var point = currentPopoverPoint();
          renderPopover(currentNode, point.x, point.y);
        } else if (action === 'save-edit') {
          if (editMode === 'task') saveTaskEdit();
          else saveFindingEdit();
        } else if (action === 'delete') {
          deleteCurrentNode();
        } else if (action === 'complete') {
          completeCurrentTask();
        } else if (action === 'move-active') {
          moveCurrentTask('Active');
        } else if (action === 'move-queue') {
          moveCurrentTask('Queue');
        }
      });
    });
  }

  function ensureHostBindings() {
    var api = graphApi();
    if (!api || !api.onNodeSelect) return false;
    api.onNodeSelect(function(node, x, y) {
      if (!node) {
        currentNode = null;
        editMode = null;
        var popover = document.getElementById('graph-node-popover');
        if (popover) popover.style.display = 'none';
        return;
      }
      editMode = null;
      renderPopover(node, x, y);
    });
    if (typeof api.onSelectionClear === 'function') {
      api.onSelectionClear(function() {
        currentNode = null;
        editMode = null;
        var popover = document.getElementById('graph-node-popover');
        if (popover) popover.style.display = 'none';
      });
    }
    if (typeof api.onItemAction === 'function') {
      api.onItemAction(function(payload, action) {
        if (action === 'delete') deleteNode(payload);
        else if (action === 'delete-batch') deleteNodes(payload);
        else if (action === 'save-inline') saveInlineFromPane(payload);
        else if (action === 'merge') mergeNodes(payload);
      });
    }
    return true;
  }

  function onOutsidePointer(event) {
    var popover = document.getElementById('graph-node-popover-card');
    if (!currentNode || !popover) return;
    if (popover.contains(event.target)) return;
    // Renderer-owned HUD overlays are legitimate UI, not a click-away dismiss.
    var t = event.target;
    if (t && t.closest && t.closest('.phren-project-panel, .phren-project-nav, .phren-pp-reopen, #graph-filter, .graph-controls, .phren-hud-legend, .phren-hud-stats')) return;
    hidePopover();
  }

  function onEscape(event) {
    if (event.key !== 'Escape' || !currentNode) return;
    if (editMode) {
      editMode = null;
      var point = currentPopoverPoint();
      renderPopover(currentNode, point.x, point.y);
      return;
    }
    hidePopover();
  }

  document.addEventListener('pointerdown', onOutsidePointer);
  document.addEventListener('keydown', onEscape);

  if (!ensureHostBindings()) {
    var tries = 0;
    var timer = setInterval(function() {
      tries++;
      if (ensureHostBindings() || tries > 40) clearInterval(timer);
    }, 100);
  }
})();`;
}
export function renderReviewQueueKeyboardScript(_authToken) {
    return `(function() {
    var _reviewHighlightIndex = -1;
    var _currentTab = '';
    var esc = window._phrenEsc;
    var authUrl = window._phrenAuthUrl;
    var authBody = window._phrenAuthBody;
    var fetchCsrfToken = window._phrenFetchCsrfToken;

    function updateReviewHighlight() {
      var cards = document.querySelectorAll('#review-cards-list [data-review-card]');
      cards.forEach(function(card, i) {
        if (i === _reviewHighlightIndex) {
          card.classList.add('review-card-highlight');
          card.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
        } else {
          card.classList.remove('review-card-highlight');
        }
      });
    }

    function getApproveButton(card) {
      return card ? card.querySelector('[data-review-type="approve"]') : null;
    }

    function getRejectButton(card) {
      return card ? card.querySelector('[data-review-type="reject"]') : null;
    }

    function triggerCardAction(card, action) {
      if (!card) return;
      if (action === 'approve') {
        var approveBtn = getApproveButton(card);
        if (approveBtn) approveBtn.click();
      } else if (action === 'reject') {
        var rejectBtn = getRejectButton(card);
        if (rejectBtn) rejectBtn.click();
      }
    }

    var baseWindowSwitchTab = window.switchTab;
    if (typeof baseWindowSwitchTab === 'function') {
      window.switchTab = function(tab) {
        baseWindowSwitchTab(tab);
        _currentTab = tab;
        if (tab === 'review') {
          _reviewHighlightIndex = -1;
          setTimeout(function() { updateReviewHighlight(); }, 100);
        }
      };
    }

    document.addEventListener('keydown', function(e) {
      if (_currentTab !== 'review') return;
      var cards = document.querySelectorAll('#review-cards-list [data-review-card]');
      if (cards.length === 0) return;

      if (e.key === 'j' || e.key === 'ArrowDown') {
        e.preventDefault();
        _reviewHighlightIndex = Math.min(_reviewHighlightIndex + 1, cards.length - 1);
        updateReviewHighlight();
      } else if (e.key === 'k' || e.key === 'ArrowUp') {
        e.preventDefault();
        _reviewHighlightIndex = Math.max(_reviewHighlightIndex - 1, -1);
        updateReviewHighlight();
      } else if (e.key === 'a') {
        e.preventDefault();
        if (_reviewHighlightIndex >= 0) {
          triggerCardAction(cards[_reviewHighlightIndex], 'approve');
        }
      } else if (e.key === 'd') {
        e.preventDefault();
        if (_reviewHighlightIndex >= 0) {
          triggerCardAction(cards[_reviewHighlightIndex], 'reject');
        }
      } else if (e.key === 'Enter') {
        e.preventDefault();
        if (_reviewHighlightIndex >= 0) {
          cards[_reviewHighlightIndex].click();
        }
      }
    });
  })();`;
}
/**
 * Live "Activity" tab: streams memory lookups from /api/lookups/stream (SSE)
 * and prepends them to #activity-feed in real time, so you can watch phren land
 * on memories as it searches. Falls back gracefully if EventSource is missing.
 */
export function renderActivityStreamScript(_authToken) {
    return `(function() {
    var esc = window._phrenEsc;
    var authUrl = window._phrenAuthUrl;
    var MAX_ITEMS = 80;
    var feed = null, statusEl = null, ledEl = null;
    var emptyShown = true;

    function el() {
      feed = document.getElementById('activity-feed');
      statusEl = document.getElementById('activity-status');
      ledEl = document.getElementById('activity-led');
      return !!feed;
    }

    function relTime(iso) {
      var t = Date.parse(iso);
      if (isNaN(t)) return '';
      var s = Math.max(0, Math.round((Date.now() - t) / 1000));
      if (s < 5) return 'just now';
      if (s < 60) return s + 's ago';
      var m = Math.round(s / 60);
      if (m < 60) return m + 'm ago';
      var h = Math.round(m / 60);
      if (h < 24) return h + 'h ago';
      return Math.round(h / 24) + 'd ago';
    }

    function setStatus(text, state) {
      if (statusEl) statusEl.textContent = text;
      if (ledEl) ledEl.className = 'activity-led' + (state ? ' activity-led-' + state : '');
    }

    function renderItem(ev, isNew) {
      if (!feed) return;
      if (emptyShown) { feed.innerHTML = ''; emptyShown = false; }
      var li = document.createElement('li');
      li.className = 'activity-item' + (isNew ? ' activity-item-new' : '');
      var loc = esc((ev.project || '?') + '/' + (ev.filename || '?'));
      var type = esc(ev.type || '');
      var q = ev.query ? esc(ev.query) : '';
      var snip = ev.snippet ? esc(ev.snippet) : '';
      li.innerHTML =
        '<div class="activity-row">' +
          '<span class="activity-type activity-type-' + type + '">' + type + '</span>' +
          '<span class="activity-loc">' + loc + '</span>' +
          '<span class="activity-time">' + esc(relTime(ev.at)) + '</span>' +
        '</div>' +
        (q ? '<div class="activity-query">found via &ldquo;' + q + '&rdquo;</div>' : '') +
        (snip ? '<div class="activity-snippet">' + snip + '</div>' : '');
      feed.insertBefore(li, feed.firstChild);
      while (feed.children.length > MAX_ITEMS) feed.removeChild(feed.lastChild);
      if (isNew) {
        setTimeout(function() { li.classList.remove('activity-item-new'); }, 1500);
      }
    }

    function loadInitial() {
      fetch(authUrl('/api/lookups')).then(function(r) { return r.json(); }).then(function(d) {
        var items = (d && d.lookups) || [];
        // Leave the feed placeholder and let the SSE connection drive the status
        // (avoids clobbering 'Live' when the snapshot is empty).
        if (!items.length) return;
        // API returns newest-first; render oldest-first so newest ends up on top.
        for (var i = items.length - 1; i >= 0; i--) renderItem(items[i], false);
      }).catch(function() { /* stream may still deliver */ });
    }

    function connect() {
      if (typeof EventSource === 'undefined') {
        setStatus('Live updates unsupported — reload to refresh', 'err');
        return;
      }
      var src = new EventSource(authUrl('/api/lookups/stream'));
      src.onopen = function() { setStatus('Live', 'ok'); };
      src.onmessage = function(e) {
        try {
          var ev = JSON.parse(e.data);
          renderItem(ev, true);
          // Broadcast so other surfaces (e.g. the graph walk) can react without
          // opening a second SSE connection.
          try { window.dispatchEvent(new CustomEvent('phren:lookup', { detail: ev })); } catch (err2) {}
        } catch (err) {}
        setStatus('Live', 'ok');
      };
      src.onerror = function() {
        setStatus('Reconnecting…', 'warn');
        // EventSource auto-reconnects using the server's retry hint.
      };
    }

    function init() {
      if (!el()) return;
      setStatus('Connecting…', 'warn');
      loadInitial();
      connect();
    }

    if (document.readyState === 'loading') {
      document.addEventListener('DOMContentLoaded', init);
    } else {
      init();
    }
  })();`;
}
/**
 * Drives the graph mascot from the live lookup feed: each 'phren:lookup' event
 * (dispatched by the activity stream) maps to a graph node and sends phren
 * walking to it, so you can watch phren traverse the knowledge graph in real
 * time. No-ops gracefully until the graph is mounted (Graph tab opened).
 */
export function renderGraphWalkScript() {
    return `(function() {
    function api() { return window.phrenGraph; }

    function nodeIdForLookup(ev) {
      var g = api();
      if (!g || typeof g.getNodeDetail !== 'function' || typeof g.getData !== 'function') return null;
      if (!ev || !ev.project) return null;

      // Precomputed node id (e.g. a specific finding) wins when it resolves.
      if (ev.nodeId && g.getNodeDetail(ev.nodeId)) return ev.nodeId;

      // Reference lookups: prefer the specific reference node when present.
      if (ev.type === 'reference' && ev.filename) {
        var base = String(ev.filename).split('/').pop() || ev.filename;
        var candidates = [ev.project + ':ref:' + ev.filename, ev.project + ':ref:' + base];
        for (var i = 0; i < candidates.length; i++) {
          if (g.getNodeDetail(candidates[i])) return candidates[i];
        }
        var stem = base.replace(/\\.md$/, '');
        var refMatch = (g.getData().nodes || []).find(function(n) {
          return n.project === ev.project && n.group === 'reference' && String(n.label || '').indexOf(stem) !== -1;
        });
        if (refMatch) return refMatch.id;
      }

      // Project node id is the project name — the reliable, always-present hop.
      if (g.getNodeDetail(ev.project)) return ev.project;

      // Last resort: any visible node belonging to the project.
      var any = (g.getData().nodes || []).find(function(n) { return n.project === ev.project; });
      return any ? any.id : null;
    }

    window.addEventListener('phren:lookup', function(e) {
      var g = api();
      if (!g || typeof g.walkTo !== 'function') return;
      var id = nodeIdForLookup(e.detail);
      if (id) { try { g.walkTo(id); } catch (err) {} }
    });
  })();`;
}