// ══════════════════════════════════════════════════════════════
// nterm — Application state, UI wiring, utilities
// ══════════════════════════════════════════════════════════════
// Loaded first (after perf.js). Provides state and helpers
// that all other modules depend on.

const state = {
  sessions: {},      // tabId → { term, fitAddon, socket, config, status, stats, snifferEnabled }
  activeTab: null,
  diagOpen: false,
  diagView: 'stream',
  snifferDefault: true,  // new sessions start with sniffer on
  vault: {
    initialized: false,
    unlocked: false,
    credentialCount: 0,
    credentials: [],       // credential summaries (metadata only)
    _checking: false,
  },
};

// ── Utilities ────────────────────────────────────────────────
function debounce(fn, delay) {
  let timer;
  return (...args) => { clearTimeout(timer); timer = setTimeout(() => fn(...args), delay); };
}

function escapeHtml(str) {
  const div = document.createElement('div');
  div.textContent = str;
  return div.innerHTML;
}

// ── Status bar ───────────────────────────────────────────────
function updateStatus(msg, level) {
  const el = document.getElementById('status-left');
  el.textContent = msg;
  el.style.color = level === 'error' ? 'var(--red)' : 'var(--text-dim)';
}

function updateStatusRight(tabId) {
  const session = state.sessions[tabId];
  if (!session) return;
  const c = session.config;
  let authLabel;
  if (c.use_vault) {
    authLabel = c.credential_name ? `vault(${c.credential_name})` : 'vault';
  } else {
    authLabel = c.key_content ? 'key(paste)' : c.key_file ? `key(${c.key_file.split('/').pop()})` : 'password';
  }
  document.getElementById('status-right').textContent =
    `SSH · ${c.hostname}:${c.port} · ${authLabel}${c.legacy_mode ? ' · legacy' : ''}`;
}

function updateGlobalStatus() {
  const count = Object.keys(state.sessions).length;
  const connected = Object.values(state.sessions).filter(s => s.status === 'connected').length;
  document.getElementById('global-status').textContent =
    count === 0 ? 'No active sessions' : `${connected}/${count} connected`;
}

// ── Diagnostics panel controls ───────────────────────────────
function toggleDiag() {
  state.diagOpen = !state.diagOpen;
  const panel = document.getElementById('diag-panel');
  const panels = document.getElementById('terminal-panels');
  const btn = document.getElementById('toggle-diag');

  panel.classList.toggle('open', state.diagOpen);
  panels.classList.toggle('diag-open', state.diagOpen);
  btn.classList.toggle('active', state.diagOpen);

  if (state.diagOpen) diag.render();

  setTimeout(() => {
    if (state.activeTab && state.sessions[state.activeTab]) {
      state.sessions[state.activeTab].fitAddon.fit();
      scheduleGutterRender(state.activeTab);
    }
  }, 50);
}

// ── Sniffer toggle ───────────────────────────────────────────
function toggleSniffer(tabId) {
  tabId = tabId || state.activeTab;
  if (!tabId || !state.sessions[tabId]) return;

  const session = state.sessions[tabId];
  if (session.snifferEnabled === undefined) session.snifferEnabled = true;
  session.snifferEnabled = !session.snifferEnabled;

  // Show/hide gutter for this tab
  const gutter = document.querySelector(`#pane-${tabId} .block-gutter`);
  if (gutter) {
    gutter.style.display = session.snifferEnabled ? '' : 'none';
  }

  // Refit terminal to reclaim/yield gutter space
  setTimeout(() => {
    session.fitAddon.fit();
    if (session.snifferEnabled) scheduleGutterRender(tabId);
  }, 50);

  updateSnifferIndicator(tabId);
  const label = session.snifferEnabled ? 'ON' : 'OFF';
  updateStatus('Sniffer ' + label + ' (Ctrl+Shift+S)');
}

function updateSnifferIndicator(tabId) {
  tabId = tabId || state.activeTab;
  if (!tabId) return;
  const session = state.sessions[tabId];
  const enabled = session ? (session.snifferEnabled !== false) : true;

  let indicator = document.getElementById('sniffer-indicator');
  if (!indicator) {
    indicator = document.createElement('span');
    indicator.id = 'sniffer-indicator';
    indicator.style.cssText = 'cursor:pointer;margin-right:8px;font-size:10px;letter-spacing:0.5px;';
    indicator.title = 'Toggle sniffer (Ctrl+Shift+S)';
    indicator.addEventListener('click', () => toggleSniffer());
    const statusRight = document.getElementById('status-right');
    statusRight.parentElement.insertBefore(indicator, statusRight);
  }

  if (enabled) {
    indicator.textContent = 'SNIFFER';
    indicator.style.color = 'var(--green)';
  } else {
    indicator.textContent = 'SNIFFER OFF';
    indicator.style.color = 'var(--text-dim)';
  }
}

// ── Vault integration ─────────────────────────────────────────

async function checkVaultStatus() {
  if (state.vault._checking) return;
  state.vault._checking = true;

  try {
    const res = await fetch('/api/vault/status');
    if (!res.ok) {
      // Vault not available (browser-only mode, 503)
      document.getElementById('vault-bar').style.display = 'none';
      return;
    }
    const data = await res.json();
    state.vault.initialized = data.initialized;
    state.vault.unlocked = data.unlocked;
    state.vault.credentialCount = data.credential_count || 0;

    const bar = document.getElementById('vault-bar');
    const el = document.getElementById('vault-status');
    bar.style.display = '';

    if (data.unlocked) {
      el.textContent = `🔓 ${data.credential_count} credential${data.credential_count !== 1 ? 's' : ''}`;
      el.style.color = 'var(--green)';
      // Show vault auth tab
      document.getElementById('auth-tab-vault').style.display = '';
      // Fetch credential list for session tree badges
      loadVaultCredentials();
    } else if (data.initialized) {
      el.textContent = '🔒 Vault locked';
      el.style.color = 'var(--text-dim)';
      state.vault.credentials = [];
      // Hide vault auth tab, switch away if active
      document.getElementById('auth-tab-vault').style.display = 'none';
      if (document.querySelector('#auth-tabs button.active')?.dataset.auth === 'vault') {
        document.querySelector('#auth-tabs button[data-auth="password"]').click();
      }
    } else {
      el.textContent = '🔒 No vault';
      el.style.color = 'var(--text-dim)';
      state.vault.credentials = [];
      document.getElementById('auth-tab-vault').style.display = 'none';
      if (document.querySelector('#auth-tabs button.active')?.dataset.auth === 'vault') {
        document.querySelector('#auth-tabs button[data-auth="password"]').click();
      }
    }
  } catch (e) {
    // Vault not available — hide bar silently
    document.getElementById('vault-bar').style.display = 'none';
  } finally {
    state.vault._checking = false;
  }
}

async function loadVaultCredentials() {
  try {
    const res = await fetch('/api/vault/credentials');
    if (!res.ok) return;
    state.vault.credentials = await res.json();
  } catch (e) {
    state.vault.credentials = [];
  }
}

function showVaultOverlay(mode) {
  // mode: 'unlock' or 'init'
  const overlay = document.getElementById('vault-overlay');
  const title = document.getElementById('vault-dialog-title');
  const label = document.getElementById('vault-dialog-label');
  const btn = document.getElementById('vault-submit-btn');
  const errEl = document.getElementById('vault-dialog-error');
  const input = document.getElementById('vault-password');

  if (mode === 'init') {
    title.textContent = 'Initialize Vault';
    label.textContent = 'Create a master password';
    btn.textContent = 'Create Vault';
  } else {
    title.textContent = 'Unlock Vault';
    label.textContent = 'Master password';
    btn.textContent = 'Unlock';
  }

  errEl.style.display = 'none';
  input.value = '';
  overlay.style.display = 'flex';
  overlay.dataset.mode = mode || 'unlock';
  setTimeout(() => input.focus(), 50);
}

function hideVaultOverlay() {
  document.getElementById('vault-overlay').style.display = 'none';
  document.getElementById('vault-password').value = '';
  document.getElementById('vault-dialog-error').style.display = 'none';
}

async function submitVaultPassword() {
  const overlay = document.getElementById('vault-overlay');
  const mode = overlay.dataset.mode || 'unlock';
  const password = document.getElementById('vault-password').value;
  const errEl = document.getElementById('vault-dialog-error');
  const btn = document.getElementById('vault-submit-btn');

  if (!password) {
    errEl.textContent = 'Password required';
    errEl.style.display = '';
    return;
  }

  btn.disabled = true;
  btn.textContent = mode === 'init' ? 'Creating...' : 'Unlocking...';

  try {
    const endpoint = mode === 'init' ? '/api/vault/init' : '/api/vault/unlock';
    const res = await fetch(endpoint, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ password }),
    });

    if (res.ok) {
      hideVaultOverlay();
      await checkVaultStatus();
      updateStatus(mode === 'init' ? 'Vault created and unlocked' : 'Vault unlocked');
      // Refresh session tree to show vault badges
      if (typeof sessionTree !== 'undefined') sessionTree.refresh();
    } else {
      const data = await res.json().catch(() => ({}));
      errEl.textContent = data.detail || 'Invalid password';
      errEl.style.display = '';
    }
  } catch (e) {
    errEl.textContent = 'Connection error';
    errEl.style.display = '';
  } finally {
    btn.disabled = false;
    btn.textContent = mode === 'init' ? 'Create Vault' : 'Unlock';
  }
}

async function lockVault() {
  try {
    await fetch('/api/vault/lock', { method: 'POST' });
    await checkVaultStatus();
    updateStatus('Vault locked');
  } catch (e) {
    // silent
  }
}

async function vaultMatchHost(hostname, port) {
  /**
   * Check if the vault has a matching credential for a hostname.
   * Returns the match response or null.
   */
  if (!state.vault.unlocked) return null;
  try {
    const res = await fetch(`/api/vault/match?host=${encodeURIComponent(hostname)}&port=${port || 22}`);
    if (!res.ok) return null;
    const data = await res.json();
    return data.matched ? data : null;
  } catch (e) {
    return null;
  }
}

function vaultHostHasCredential(hostname) {
  /**
   * Quick client-side check: does any vault credential pattern match this hostname?
   * Uses the cached credential list from loadVaultCredentials().
   * This is for badges — actual resolution happens server-side.
   */
  if (!state.vault.unlocked || !state.vault.credentials.length) return false;
  for (const cred of state.vault.credentials) {
    if (cred.is_default) continue;  // don't badge default creds
    for (const pattern of (cred.match_hosts || [])) {
      // Simple fnmatch-style check: * matches anything
      const regex = new RegExp('^' + pattern.replace(/\*/g, '.*').replace(/\?/g, '.') + '$', 'i');
      if (regex.test(hostname)) return true;
    }
  }
  // Check if there's a default credential (match any host)
  return state.vault.credentials.some(c => c.is_default);
}

let _vaultAuthDebounce = null;

async function updateVaultAuthInfo() {
  /**
   * Update the vault auth panel with match info for the current hostname.
   * Called when: vault tab is selected, host field changes, vault unlocks.
   */
  const infoEl = document.getElementById('vault-auth-info');
  if (!infoEl) return;

  const hostname = document.getElementById('f-host').value.trim();

  if (!state.vault.unlocked) {
    infoEl.innerHTML = '<span style="color:var(--yellow);">Vault is locked. <a href="#" onclick="showVaultOverlay(\'unlock\');return false;" style="color:var(--cyan);">Unlock</a></span>';
    return;
  }

  if (!hostname) {
    infoEl.textContent = 'Enter a hostname to check vault credentials';
    infoEl.style.color = 'var(--text-dim)';
    return;
  }

  infoEl.textContent = 'Checking...';
  infoEl.style.color = 'var(--text-dim)';

  const match = await vaultMatchHost(hostname, parseInt(document.getElementById('f-port').value) || 22);

  if (match) {
    infoEl.innerHTML = `<span style="color:var(--green);">✓ Matched: <strong style="color:var(--cyan);">${escapeHtml(match.credential_name || '?')}</strong></span>`
      + `<br><span style="color:var(--text-dim);">User: ${escapeHtml(match.username || '?')} · Auth: ${escapeHtml(match.auth_method || '?')}`
      + `${match.has_jump_host ? ' · Jump: ' + escapeHtml(match.jump_host) : ''}`
      + `${match.requires_touch ? ' · 🔑 Touch required' : ''}</span>`;
    // Auto-fill username from vault match
    const usernameField = document.getElementById('f-username');
    if (!usernameField.value.trim() && match.username) {
      usernameField.value = match.username;
    }
  } else {
    // Check for default credential
    const hasDefault = state.vault.credentials.some(c => c.is_default);
    if (hasDefault) {
      const def = state.vault.credentials.find(c => c.is_default);
      infoEl.innerHTML = `<span style="color:var(--yellow);">No specific match — will use default: <strong style="color:var(--cyan);">${escapeHtml(def.name)}</strong></span>`;
    } else {
      infoEl.innerHTML = `<span style="color:var(--text-dim);">No vault credential matches ${escapeHtml(hostname)}</span>`;
    }
  }
}

// Desktop wrapper callbacks — called via runJavaScript() from PyQt6
window._vaultStatus = function(unlocked) {
  checkVaultStatus();
};

window._vaultChanged = function() {
  checkVaultStatus();
  if (typeof sessionTree !== 'undefined') sessionTree.refresh();
};

/**
 * Programmatic device connect — called from PyQt6 via runJavaScript().
 *
 * Used by Wirlwind telemetry's "Connect SSH" button on LLDP neighbors.
 * Tries vault resolution first, falls back to populating Quick Connect.
 *
 * @param {Object} config
 * @param {string} config.hostname   — IP or DNS name (required)
 * @param {number} [config.port=22]  — SSH port
 * @param {string} [config.vendor]   — vendor hint for parse chain
 * @param {string} [config.device_id] — display name (for tab label)
 * @param {string} [config.username] — override username
 * @param {string} [config.credential_name] — explicit vault credential
 * @param {boolean} [config.legacy_mode=false]
 */
window.ntermConnectDevice = async function(config) {
  if (!config || !config.hostname) {
    console.warn('ntermConnectDevice: hostname required');
    return;
  }

  const hostname = config.hostname;
  const port = config.port || 22;
  const vendor = config.vendor || '';
  const deviceId = config.device_id || hostname;
  const legacyMode = config.legacy_mode || false;

  // ── Try vault-aware connect first ─────────────────────────
  if (state.vault.unlocked) {
    const connected = await vaultConnect(hostname, port, {
      credential_name: config.credential_name || null,
      username: config.username || '',
      vendor: vendor,
      legacy_mode: legacyMode,
    });

    if (connected) {
      updateStatus('Connected to ' + deviceId + ' (via LLDP neighbor)');
      return;
    }
  }

  // ── Fallback: reuse existing Quick Connect credentials ─────
  // When crawling, creds are usually the same — just swap the host
  // and auto-connect if we already have username + auth filled in.
  const existingUsername = document.getElementById('f-username').value.trim();
  const activeAuth = document.querySelector('#auth-tabs button.active')?.dataset.auth;
  const hasCredentials = existingUsername && (
    (activeAuth === 'password' && document.getElementById('f-password').value) ||
    (activeAuth === 'key_file' && document.getElementById('f-keyfile').value.trim()) ||
    (activeAuth === 'key_paste' && document.getElementById('f-keycontent').value.trim())
  );

  // Always update host/port/vendor — but leave auth fields alone
  document.getElementById('f-host').value = hostname;
  document.getElementById('f-port').value = port;

  // Set vendor dropdown if we have a hint
  if (vendor) {
    const vendorSelect = document.getElementById('f-vendor');
    if (vendorSelect) {
      for (const opt of vendorSelect.options) {
        if (opt.value === vendor) {
          vendorSelect.value = vendor;
          break;
        }
      }
    }
  }

  if (hasCredentials) {
    // Auto-connect using existing form credentials
    const tabId = crypto.randomUUID();
    const sessionConfig = {
      hostname: hostname,
      port: port,
      username: existingUsername,
      legacy_mode: document.getElementById('f-legacy').checked,
    };

    if (activeAuth === 'password') {
      sessionConfig.password = document.getElementById('f-password').value;
    } else if (activeAuth === 'key_file') {
      sessionConfig.key_file = document.getElementById('f-keyfile').value.trim();
      sessionConfig.key_passphrase = document.getElementById('f-keyfile-pass').value || null;
    } else if (activeAuth === 'key_paste') {
      sessionConfig.key_content = document.getElementById('f-keycontent').value;
      sessionConfig.key_passphrase = document.getElementById('f-keycontent-pass').value || null;
    }

    const btn = document.getElementById('btn-connect');
    btn.disabled = true;
    btn.textContent = 'Connecting...';
    btn.classList.add('connecting');

    createSession(tabId, sessionConfig, () => {
      btn.disabled = false;
      btn.textContent = 'Connect';
      btn.classList.remove('connecting');
    });

    // Set vendor hint on session
    if (vendor && state.sessions[tabId]) {
      state.sessions[tabId].vendorOverride = vendor;
    }
    if (state.sessions[tabId]) {
      state.sessions[tabId].snifferEnabled = state.snifferDefault;
      updateSnifferIndicator(tabId);
    }

    updateStatus('Connected to ' + deviceId + ' (via LLDP neighbor)');
    return;
  }

  // No credentials yet — just populate form and let user fill in auth
  // Flash the Connect button to draw attention
  const btn = document.getElementById('btn-connect');
  btn.style.transition = 'box-shadow 0.3s';
  btn.style.boxShadow = '0 0 12px var(--cyan)';
  setTimeout(() => { btn.style.boxShadow = ''; }, 1500);

  // If vault auth tab is active, trigger match check
  if (activeAuth === 'vault') updateVaultAuthInfo();

  updateStatus('Quick Connect populated for ' + deviceId + ' — enter credentials and connect');
};

// ── Event wiring (runs after DOM ready) ──────────────────────
document.getElementById('toggle-diag').addEventListener('click', toggleDiag);

// Vault UI events
document.getElementById('vault-status').addEventListener('click', () => {
  if (state.vault.unlocked) {
    lockVault();
  } else if (state.vault.initialized) {
    showVaultOverlay('unlock');
  } else {
    showVaultOverlay('init');
  }
});

document.getElementById('vault-submit-btn').addEventListener('click', submitVaultPassword);
document.getElementById('vault-password').addEventListener('keydown', (e) => {
  if (e.key === 'Enter') { e.preventDefault(); submitVaultPassword(); }
  if (e.key === 'Escape') hideVaultOverlay();
});
document.getElementById('vault-overlay').addEventListener('click', (e) => {
  if (e.target === e.currentTarget) hideVaultOverlay();
});

// Diag tab switching
document.querySelector('.diag-tabs').addEventListener('click', (e) => {
  const btn = e.target.closest('.diag-tab');
  if (!btn) return;
  state.diagView = btn.dataset.diag;
  document.querySelectorAll('.diag-tab').forEach(b => b.classList.remove('active'));
  btn.classList.add('active');
  document.querySelectorAll('.diag-view').forEach(v => v.style.display = 'none');
  document.getElementById('diag-' + state.diagView).style.display = '';
  diag.render();
});

// Auth method tab switching
document.getElementById('auth-tabs').addEventListener('click', (e) => {
  const btn = e.target.closest('button[data-auth]');
  if (!btn) return;
  document.querySelectorAll('#auth-tabs button').forEach(b => b.classList.remove('active'));
  btn.classList.add('active');
  document.querySelectorAll('.auth-fields').forEach(f => f.classList.remove('active'));
  document.getElementById('auth-' + btn.dataset.auth).classList.add('active');
  // If vault tab selected, refresh match info
  if (btn.dataset.auth === 'vault') updateVaultAuthInfo();
});

// Host field change → update vault auth info (debounced)
document.getElementById('f-host').addEventListener('input', debounce(() => {
  const activeAuth = document.querySelector('#auth-tabs button.active')?.dataset.auth;
  if (activeAuth === 'vault') updateVaultAuthInfo();
}, 400));

// Sidebar toggle
document.getElementById('toggle-sidebar').addEventListener('click', () => {
  document.getElementById('sidebar').classList.toggle('collapsed');
  setTimeout(() => {
    if (state.activeTab && state.sessions[state.activeTab]) {
      state.sessions[state.activeTab].fitAddon.fit();
      scheduleGutterRender(state.activeTab);
    }
  }, 200);
});

// Connect form
document.getElementById('connect-form').addEventListener('submit', (e) => {
  e.preventDefault();

  const btn = document.getElementById('btn-connect');
  if (btn.disabled) return;

  const activeAuth = document.querySelector('#auth-tabs button.active').dataset.auth;
  const tabId = crypto.randomUUID();

  const config = {
    hostname: document.getElementById('f-host').value.trim(),
    port: parseInt(document.getElementById('f-port').value) || 22,
    username: document.getElementById('f-username').value.trim(),
    legacy_mode: document.getElementById('f-legacy').checked,
  };

  if (activeAuth === 'password') {
    config.password = document.getElementById('f-password').value;
  } else if (activeAuth === 'key_file') {
    config.key_file = document.getElementById('f-keyfile').value.trim();
    config.key_passphrase = document.getElementById('f-keyfile-pass').value || null;
  } else if (activeAuth === 'key_paste') {
    config.key_content = document.getElementById('f-keycontent').value;
    config.key_passphrase = document.getElementById('f-keycontent-pass').value || null;
  } else if (activeAuth === 'vault') {
    config.use_vault = true;
    // Username may have been auto-filled from vault match
  }

  if (!config.hostname) {
    updateStatus('Hostname is required', 'error');
    return;
  }
  if (!config.username && !config.use_vault) {
    updateStatus('Username is required (or select Vault auth)', 'error');
    return;
  }

  btn.disabled = true;
  btn.textContent = 'Connecting...';
  btn.classList.add('connecting');

  createSession(tabId, config, () => {
    btn.disabled = false;
    btn.textContent = 'Connect';
    btn.classList.remove('connecting');
  });

  // Set vendor hint on session (getVendorHint checks this at priority 1)
  const vendorVal = document.getElementById('f-vendor').value;
  if (vendorVal && state.sessions[tabId]) {
    state.sessions[tabId].vendorOverride = vendorVal;
  }
  // Initialize sniffer toggle for new session
  if (state.sessions[tabId]) {
    state.sessions[tabId].snifferEnabled = state.snifferDefault;
    updateSnifferIndicator(tabId);
  }
});

// Window resize / zoom → refit all terminals
function refitAllTerminals() {
  for (const [tabId, session] of Object.entries(state.sessions)) {
    try {
      if (session.fitAddon) session.fitAddon.fit();
    } catch (e) { /* terminal may be disposed */ }
  }
  if (state.activeTab) scheduleGutterRender(state.activeTab);
}
const _debouncedRefit = debounce(refitAllTerminals, 150);
window.addEventListener('resize', _debouncedRefit);

// ResizeObserver catches zoom changes that don't fire window.resize
// (common in PyQt/QWebEngineView and some browser zoom implementations)
if (typeof ResizeObserver !== 'undefined') {
  const _termRO = new ResizeObserver(_debouncedRefit);
  const _roTarget = document.getElementById('terminal-panels');
  if (_roTarget) _termRO.observe(_roTarget);
}

// Keyboard shortcuts
document.addEventListener('keydown', (e) => {
  if (e.key === 'Escape') {
    if (blockCtx._menu) {
      blockCtx.hide();
      return;
    }
  }
  if (e.ctrlKey && e.key === '`') {
    e.preventDefault();
    toggleDiag();
  }
  if (e.ctrlKey && e.key === 'b') {
    e.preventDefault();
    document.getElementById('toggle-sidebar').click();
  }
  if (e.ctrlKey && e.shiftKey && (e.key === 'S' || e.key === 's')) {
    e.preventDefault();
    toggleSniffer();
  }
  if (e.ctrlKey && (e.key === 'u' || e.key === 'U') && !e.shiftKey) {
    e.preventDefault();
    if (state.vault.unlocked) lockVault();
    else if (state.vault.initialized) showVaultOverlay('unlock');
    else showVaultOverlay('init');
  }
});

// ── Periodic timers ──────────────────────────────────────────

// Global status update
setInterval(updateGlobalStatus, 2000);

// Check vault status on load, then every 30s (catches external unlock/lock)
checkVaultStatus();
setInterval(checkVaultStatus, 30000);

// Refresh diag views + stats polling
setInterval(() => {
  if (typeof diag === 'undefined') return;

  if (state.diagOpen && state.diagView === 'stats') {
    fetch('/api/diag').then(r => r.json()).then(data => {
      for (const s of data.sessions) {
        const fullId = Object.keys(state.sessions).find(k => k.startsWith(s.tab_id));
        if (fullId && state.sessions[fullId]) {
          state.sessions[fullId].stats = s.stats;
        }
      }
      diag.renderStats();
    }).catch(() => {});
  }
  if (state.diagOpen && state.diagView === 'sniffer') diag.renderSniffer();
  if (state.diagOpen && state.diagView === 'prompts') diag.renderPrompts();
  if (state.diagOpen && state.diagView === 'blocks') diag.renderBlocks();
  if (state.diagOpen && state.diagView === 'perf') perf.renderPerfTab();

  // Drive quiet-period prompt confirmation
  const activeTab = state.activeTab;
  if (activeTab && diag.snifferPrompts[activeTab]) {
    const acc = diag.snifferAccumulators[activeTab];
    const det = diag.snifferDetectors[activeTab];
    const pdet = diag.snifferPrompts[activeTab];
    if (acc && det && pdet && acc.pending) {
      pdet.checkPending(acc.pending, det.isInterrupted);
    }
  }
}, 2000);

// Fast interval for prompt quiet-period detection
setInterval(() => {
  for (const tabId of Object.keys(state.sessions)) {
    if (typeof diag === 'undefined') return;

    if (!diag.snifferPrompts[tabId]) continue;
    const acc = diag.snifferAccumulators[tabId];
    const det = diag.snifferDetectors[tabId];
    const pdet = diag.snifferPrompts[tabId];
    const bext = diag.snifferBlocks[tabId];
    if (acc && det && pdet && acc.pending) {
      const prevState = pdet.state;
      const prevMatchCount = pdet.matchCount;
      pdet.checkPending(acc.pending, det.isInterrupted);

      if (bext && pdet.matchCount > prevMatchCount) {
        const block = bext.notifyPrompt(acc.pending, acc.lineCount);
        if (block) {
          scheduleGutterRender(tabId);
          if (state.diagOpen && state.diagView === 'blocks' && tabId === state.activeTab) {
            diag.renderBlocks();
          }
        }
        diag.updateBlockCount(tabId);
      }

      if (pdet.state !== prevState && tabId === state.activeTab
          && state.diagOpen && state.diagView === 'prompts') {
        diag.renderPrompts();
      }
    }
  }
}, 400);

// ── Debug helper: window.ntermGutter() ───────────────────────
window.ntermGutter = function(verbose) {
  const tabId = state.activeTab;
  if (!tabId) { console.log('[gutter] no active tab'); return; }

  const session = state.sessions[tabId];
  const bext = diag.snifferBlocks[tabId];
  const acc = diag.snifferAccumulators[tabId];

  if (!session || !session.term) { console.log('[gutter] no terminal'); return; }

  const buf = session.term.buffer.active;
  const snifferTotal = acc ? acc.lineCount : 0;
  const xtermCursorRow = buf.baseY + buf.cursorY;
  const offset = snifferTotal > 0 ? (xtermCursorRow - (snifferTotal - 1)) : 0;
  const viewportY = (typeof buf.viewportY === 'number') ? buf.viewportY : buf.baseY;

  console.log('[gutter] Terminal state:');
  console.log('  baseY:', buf.baseY, ' cursorY:', buf.cursorY, ' cursorAbsolute:', xtermCursorRow);
  console.log('  viewportY:', viewportY, ' (typeof:', typeof buf.viewportY + ')');
  console.log('  rows:', session.term.rows, ' cols:', session.term.cols);
  console.log('  buffer.length:', buf.length);
  console.log('[gutter] Sniffer state:');
  console.log('  snifferLines:', snifferTotal, ' xtermRows:', xtermCursorRow + 1, ' offset:', offset);
  console.log('  viewport: rows', viewportY, '-', viewportY + session.term.rows - 1);

  if (!bext || bext.blocks.length === 0) { console.log('[gutter] no blocks'); return; }

  const viewportTop = viewportY;
  const viewportBottom = viewportY + session.term.rows - 1;

  console.log('[gutter] Blocks (' + bext.blocks.length + '):');
  for (const b of bext.blocks) {
    const bStart = (b.startLine - 1) + offset;
    const bEnd = (b.endLine - 1) + offset;
    const inView = !(bEnd < viewportTop || bStart > viewportBottom);
    console.log('  ' + b.command.substring(0, 30).padEnd(30) +
      ' sniffer=' + b.startLine + '-' + b.endLine +
      ' → xterm=' + bStart + '-' + bEnd +
      ' lines=' + b.lineCount +
      (inView ? ' [VISIBLE]' : ' [offscreen]'));
  }

  if (verbose) {
    const container = document.querySelector(`#pane-${tabId} .xterm-container`);
    const vpEl = container ? container.querySelector('.xterm-viewport') : null;
    if (vpEl) {
      console.log('[gutter] DOM viewport: scrollTop:', vpEl.scrollTop,
        ' scrollHeight:', vpEl.scrollHeight, ' clientHeight:', vpEl.clientHeight);
    }
  }
};