const $ = (id) => document.getElementById(id);
window.__NAOMI_V = '9';
window.addEventListener('error', (e) => {
  window.__lastErr = `${e.message} @ ${e.filename}:${e.lineno}:${e.colno}`;
});
const messagesEl = $('messages');
const inputEl = $('input');
const sendBtn = $('sendBtn');

const store = {
  get chats() { return JSON.parse(localStorage.getItem('naomi_chats') ?? '[]'); },
  set chats(v) { localStorage.setItem('naomi_chats', JSON.stringify(v)); },
  get name() { return localStorage.getItem('naomi_name') ?? ''; },
  set name(v) { localStorage.setItem('naomi_name', v); },
  get drafts() { return JSON.parse(localStorage.getItem('naomi_drafts') ?? '{}'); },
  set drafts(v) { localStorage.setItem('naomi_drafts', JSON.stringify(v)); },
};

/* ─── приветствия (как у аналитика) ─── */
const GREETINGS = [
  'Привет, {name}! Чем помочь?',
  'Чем помочь?',
  '{name}, спрашивайте — ПК уже ждёт',
  'Спрашивайте — что сделать на этом компьютере?',
  'Рады видеть, {name}. Что узнать?',
  'Что спросим сегодня?',
  'Начнём, {name}?',
  'Чем помочь, {name}?',
  'Файлы и команды готовы. Что узнать?',
  'С возвращением, {name}! Задача какая?',
  '{name}, ваш компьютер к услугам',
  'Я на месте, {name}. Работаем?',
];

let greetedFromChatId = null;

function rollGreeting(instant) {
  const el = document.querySelector('.empty-greeting');
  if (!el) return;
  const name = store.name;
  const pool = name ? GREETINGS : GREETINGS.filter((x) => !x.includes('{name}'));
  let g;
  do { g = pool[Math.floor(Math.random() * pool.length)]; }
  while (g === el.dataset.tpl && pool.length > 1);
  if (instant) {
    // вход в пустой чат: текст ставим сразу, без фейда — иначе композер «улетает»
    el.dataset.tpl = g;
    el.textContent = g.replace('{name}', name);
    return;
  }
  el.classList.add('switching');
  setTimeout(() => {
    el.dataset.tpl = g;
    el.textContent = g.replace('{name}', name);
    el.classList.remove('switching');
  }, 350);
}

/* приветствие бросается один раз при входе в пустой чат;
   повторные клики «Новый чат» в том же чате его не трогают */
function setRandomGreeting() {
  if (greetedFromChatId === state.activeChatId) return;
  greetedFromChatId = state.activeChatId;
  rollGreeting(true);
}

/* смена имени: приветствие остаётся тем же, плавно подставляется только имя */
function refreshGreetingName() {
  const el = document.querySelector('.empty-greeting');
  if (!el || !el.dataset.tpl) return;
  el.classList.add('switching');
  setTimeout(() => {
    el.textContent = el.dataset.tpl.replace('{name}', store.name);
    el.classList.remove('switching');
  }, 350);
}

/* ─── черновики на чат ─── */
function getDraft(id) { return store.drafts[id] ?? ''; }
function setDraft(id, text) {
  const d = store.drafts;
  if (text) d[id] = text; else delete d[id];
  store.drafts = d;
}

function safeFocus(el) { try { el.focus({ preventScroll: true }); } catch {} }

function autoCloseSidebar() {
  if (window.innerWidth <= 900 && !document.querySelector('.sidebar').classList.contains('collapsed')) {
    document.querySelector('.sidebar').classList.add('collapsed');
    $('sidebarToggle').classList.add('active');
  }
}

const state = {
  activeChatId: null,
  busyChat: null,
  ephemeralChat: null,
  streamText: '',
  streamStartTime: null,
  tw: null,
  currentBubble: null,
  liveBotBubble: null,
  lastToolCard: null,
  pendingApproval: null,
  policy: 'safe',
  activated: false,
  drafts: {},
  levelRetried: false,
  statusBase: '',
  statusT0: 0,
  attachments: [],
};

/* ─── markdown (полный рендер, см. md-renderer.js) ─── */
function renderMarkdown(text) {
  return NaomiMD.render(text);
}

/* копирование кода из блоков */
messagesEl.addEventListener('click', (e) => {
  const btn = e.target.closest('.cb-copy');
  if (!btn) return;
  const code = btn.closest('.codeblock')?.querySelector('pre code');
  if (!code) return;
  navigator.clipboard?.writeText(code.textContent)
    .then(() => {
      btn.classList.add('ok');
      setTimeout(() => btn.classList.remove('ok'), 1200);
    })
    .catch(() => {});
});

/* действия над сообщениями: копирование и редактирование (как в ZCode) */
messagesEl.addEventListener('click', (e) => {
  const copyBtn = e.target.closest('.act-copy');
  if (copyBtn) {
    const msgEl = copyBtn.closest('.msg');
    const chat = findChat(state.activeChatId);
    const idx = Number(msgEl?.dataset.idx);
    const raw = Number.isInteger(idx) && chat?.messages[idx] && chat.messages[idx].text != null
      ? chat.messages[idx].text
      : msgEl?.querySelector('.msg-body')?.textContent ?? '';
    navigator.clipboard?.writeText(raw)
      .then(() => {
        copyBtn.classList.add('ok');
        copyBtn.title = 'Скопировано';
        setTimeout(() => { copyBtn.classList.remove('ok'); copyBtn.title = 'Копировать текст'; }, 1200);
      })
      .catch(() => {});
    return;
  }
  const editBtn = e.target.closest('.act-edit');
  if (editBtn) startEditing(editBtn.closest('.msg'));
  if (e.target.closest('.msg-edit-cancel')) renderChat();
  const saveBtn = e.target.closest('.msg-edit-save');
  if (saveBtn) submitEdit(saveBtn.closest('.msg'));
});

function startEditing(msgEl) {
  if (!msgEl || msgEl.dataset.role !== 'user') return;
  const chat = findChat(state.activeChatId);
  const idx = Number(msgEl.dataset.idx);
  const raw = chat?.messages[idx]?.text ?? msgEl.querySelector('.msg-body')?.textContent ?? '';
  if (messagesEl.querySelector('.msg-edit-area')) renderChat();
  msgEl.classList.add('msg-editing');
  const body = msgEl.querySelector('.msg-body');
  body.innerHTML = `
    <textarea class="msg-edit-area" rows="3"></textarea>
    <div class="msg-edit-bar">
      <button class="msg-edit-btn msg-edit-cancel" type="button">Отмена</button>
      <button class="msg-edit-btn primary msg-edit-save" type="button">Изменить</button>
    </div>`;
  const area = body.querySelector('.msg-edit-area');
  area.value = raw;
  area.focus();
  area.setSelectionRange(area.value.length, area.value.length);
  area.addEventListener('keydown', (e) => {
    if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); submitEdit(msgEl); }
    if (e.key === 'Escape') renderChat();
  });
}

function submitEdit(msgEl) {
  const chat = findChat(state.activeChatId);
  if (!chat || !msgEl) return;
  const idx = Number(msgEl.dataset.idx);
  const m = chat.messages[idx];
  const area = msgEl.querySelector('.msg-edit-area');
  const newText = (area?.value ?? '').trim();
  if (!m || m.role !== 'user' || !newText) { renderChat(); return; }
  m.text = newText;
  chat.messages = chat.messages.slice(0, idx + 1);
  chat.updatedAt = Date.now();
  chat.ignoreEvents = true;
  persistChat(chat);
  stopTypewriter();
  state.streamText = '';
  state.streamStartTime = null;
  state.currentBubble = null;
  state.liveBotBubble = null;
  renderChat();
  setBusy(chat.id, true);
  setAgentStatus('думает…');
  fetch('/api/chat', {
    method: 'POST',
    headers: { 'content-type': 'application/json' },
    body: JSON.stringify({ chatId: chat.id, text: newText, history: chat.messages.slice(-30, -1), resend: true }),
  }).then(async (res) => {
    if (!res.ok) {
      const json = await res.json().catch(() => ({}));
      addErrBanner(json.error ?? `ошибка ${res.status}`);
      chat.ignoreEvents = false;
      setBusy(null, false);
      renderChat();
    }
  }).catch((err) => {
    addErrBanner(`Не удалось отправить: ${err.message}`);
    chat.ignoreEvents = false;
    setBusy(null, false);
    renderChat();
  });
}


/* формулы LaTeX (KaTeX) — прогон по готовому сообщению */
function finalizeRich(el) {
  if (!window.renderMathInElement) return;
  try {
    renderMathInElement(el, {
      delimiters: [
        { left: '$$', right: '$$', display: true },
        { left: '\\[', right: '\\]', display: true },
        { left: '$', right: '$', display: false },
        { left: '\\(', right: '\\)', display: false },
      ],
      throwOnError: false,
    });
  } catch {}
}

/* ─── тайпрайтер: ответ «печатается» плавно, как у Gemini,
       даже если модель отдала текст рывком ─── */
function stopTypewriter() {
  if (state.tw) { clearInterval(state.tw.timer); state.tw = null; }
}

function startTypewriter(el) {
  if (state.tw) return;
  state.tw = {
    el,
    shown: 0,
    finished: false,
    timer: setInterval(typewriterTick, 35),
  };
}

/* рендер во время стрима: курсор ставим внутрь последнего текстового элемента,
   сразу после последнего символа — иначе он падает под абзац/список/блок */
function renderStreamWithCursor(text) {
  const md = renderMarkdown(text);
  const cur = '<span class="stream-cursor"></span>';
  const openFence = ((text.match(/```/g) || []).length) % 2 === 1;
  if (openFence && md.endsWith('</code></pre></div>')) {
    return md.slice(0, -'</code></pre></div>'.length) + cur + '</code></pre></div>';
  }
  let head = md;
  const wrappers = [];
  while (/<\/(?:ul|ol|div|table|tbody|tr|blockquote)>$/.test(head)) {
    const tag = head.match(/<\/(?:ul|ol|div|table|tbody|tr|blockquote)>$/)[0];
    wrappers.unshift(tag);
    head = head.slice(0, -tag.length);
  }
  for (const tail of ['</p>', '</li>', '</span>', '</strong>', '</em>', '</del>', '</a>', '</td>', '</th>', '</code>']) {
    if (head.endsWith(tail)) {
      return head.slice(0, head.length - tail.length) + cur + tail + wrappers.join('');
    }
  }
  return md + cur;
}

function typewriterTick() {
  const tw = state.tw;
  if (!tw) return;
  const target = state.streamText;
  if (tw.shown < target.length) {
    // плавный набор: старт медленно, догоняет ускоренно (как Gemini)
    tw.shown = Math.min(target.length, tw.shown + Math.min(24, Math.max(3, Math.round((target.length - tw.shown) / 9))));
    tw.el.innerHTML = renderStreamWithCursor(target.slice(0, tw.shown));
    scrollDown();
  }
  if (tw.shown >= target.length && tw.finished) {
    stopTypewriter();
    tw.el.innerHTML = renderMarkdown(target);
    finalizeRich(tw.el);
    state.currentBubble = null;
    state.streamText = '';
    state.streamStartTime = null;
  }
}

/* ─── чаты (localStorage + рендер) ─── */
const MSG_COPY_ICON = '<svg viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>';
const MSG_EDIT_ICON = '<svg viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 3a2.85 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z"/></svg>';
function persistChat(chat) {
  if (promoteEphemeral(chat)) return;
  const chats = store.chats;
  const i = chats.findIndex((c) => c.id === chat.id);
  if (i >= 0) chats[i] = chat;
  else chats.unshift(chat);
  store.chats = chats;
}

function findChat(id) {
  return store.chats.find((c) => c.id === id)
    ?? (state.ephemeralChat?.id === id ? state.ephemeralChat : undefined);
}

/* «Новый чат» живёт в памяти и не попадает в историю, пока не отправлено первое сообщение */
function createChat(silent) {
  const chat = { id: crypto.randomUUID(), title: 'Новый чат', messages: [], updatedAt: Date.now() };
  state.ephemeralChat = chat;
  state.activeChatId = chat.id;
  localStorage.setItem('naomi_active_chat', chat.id);
  if (!silent) { renderHistory(); renderChat(); }
  return chat;
}

/* эпемерный чат становится обычным при первом сохранении */
function promoteEphemeral(chat) {
  if (state.ephemeralChat?.id !== chat.id) return false;
  state.ephemeralChat = null;
  const chats = store.chats;
  chats.unshift(chat);
  store.chats = chats;
  return true;
}

/* пустой чат убираем из истории обратно в эпемерный (например, если отправка не удалась) */
function makeEphemeralIfEmpty(chat) {
  if (chat.messages.length) return;
  store.chats = store.chats.filter((c) => c.id !== chat.id);
  state.ephemeralChat = chat;
  renderHistory();
}

function isNewChat(chat) {
  return !chat.messages.length;
}

function renderHistory() {
  const el = $('chatHistory');
  el.innerHTML = '';
  for (const chat of store.chats) {
    const item = document.createElement('div');
    item.className = 'history-item' + (chat.id === state.activeChatId ? ' active' : '');
    // «Новый чат» удаляется только не находясь в нём: когда он открыт — крестика нет
    const locked = chat.id === state.activeChatId && isNewChat(chat);
    item.innerHTML = `<span class="history-title"></span>` + (locked ? '' : `<span class="history-delete" title="Удалить">×</span>`);
    item.querySelector('.history-title').textContent = chat.title;
    item.addEventListener('click', async (e) => {
      if (e.target.classList.contains('history-delete')) {
        if (!(await showConfirm('Удалить чат?'))) return;
        deleteChat(chat.id);
        return;
      }
      if (chat.id !== state.activeChatId) switchChat(chat.id);
    });
    el.appendChild(item);
  }
}

function deleteChat(id) {
  const chat = store.chats.find((c) => c.id === id);
  // «Новый чат» не удаляется, только находясь в нём (крестика нет)
  if (!chat || (chat.id === state.activeChatId && isNewChat(chat))) return;
  const wasActive = state.activeChatId === id;
  store.chats = store.chats.filter((c) => c.id !== id);
  setDraft(id, '');
  fetch('/api/chats/' + id + '/delete', { method: 'POST' }).catch(() => {});
  if (wasActive) {
    // удалили чат, в котором находились, — перекидываем в «Новый чат»
    const empty = store.chats.find((c) => isNewChat(c));
    if (empty) switchChat(empty.id);
    else createChat();
  } else {
    renderHistory();
  }
}

function showConfirm(text) {
  return new Promise((resolve) => {
    const existing = document.getElementById('confirmModal');
    if (existing) existing.remove();
    const overlay = document.createElement('div');
    overlay.id = 'confirmModal';
    overlay.className = 'confirm-overlay';
    overlay.innerHTML = `
      <div class="confirm-modal">
        <div class="confirm-text"></div>
        <div class="confirm-buttons">
          <button class="confirm-btn cancel">Отмена</button>
          <button class="confirm-btn ok">Удалить</button>
        </div>
      </div>`;
    overlay.querySelector('.confirm-text').textContent = text;
    overlay.querySelector('.cancel').addEventListener('click', () => { overlay.remove(); resolve(false); });
    overlay.querySelector('.ok').addEventListener('click', () => { overlay.remove(); resolve(true); });
    overlay.addEventListener('click', (e) => { if (e.target === overlay) { overlay.remove(); resolve(false); } });
    document.body.appendChild(overlay);
  });
}

function switchChat(id) {
  state.activeChatId = id;
  localStorage.setItem('naomi_active_chat', id);
  renderHistory();
  renderChat();
  refreshBusyUi();
}

function renderChat() {
  messagesEl.innerHTML = '';
  state.run = null;
  const chat = findChat(state.activeChatId);
  const chatEl = document.querySelector('.chat');
  const empty = !chat || !chat.messages.length;
  chatEl.classList.toggle('chat-empty', empty);
  inputEl.value = chat ? getDraft(chat.id) : '';
  fitInput();
  if (empty) setRandomGreeting();
  if (!chat) return;
  const busy = state.busyChat === chat.id;
  let lastUserIdx = -1;
  chat.messages.forEach((m, i) => { if (m.role === 'user') lastUserIdx = i; });
  chat.messages.forEach((m, i) => {
    if (m.role === 'user') appendMsg('user', m.text, { ts: m.ts, idx: i, editable: i === lastUserIdx, attachments: m.attachments });
    else if (m.role === 'assistant') {
      if (m.steps?.length) runMountHistory(m);
      appendMsg('bot', m.text, { ts: m.ts, idx: i });
    }
    // старые role:'tool' записи не рисуем — инструменты живут в ленте работы
  });
  const draft = state.drafts[state.activeChatId];
  if (draft && (draft.steps.length || draft.text)) {
    runRebuild(state.activeChatId, draft);
    if (draft.text) {
      state.streamText = draft.text;
      state.streamStartTime = Date.now();
      appendMsg('bot', '', { stream: true });
      state.currentBubble.innerHTML = renderMarkdown(draft.text) + '<span class="stream-cursor"></span>';
    }
  } else if (!chat.messages.length || chat.messages[chat.messages.length - 1].role !== 'assistant' || true) {
    state.streamText = '';
    state.currentBubble = null;
  }
  const busyHere = state.busyChat === state.activeChatId;
  setAgentStatus(busyHere ? (draft?.statusText || 'выполняет задачу…') : '', draft?.t0);
  messagesEl.style.scrollBehavior = 'smooth';
  scrollDown();
  setTimeout(() => { messagesEl.style.scrollBehavior = 'auto'; }, 600);
  autoCloseSidebar();
  safeFocus(inputEl);
}

function fmtTime(ts) {
  return new Date(ts).toLocaleTimeString('ru-RU', { hour: '2-digit', minute: '2-digit' });
}

function appendMsg(role, text, { stream, ts, idx, editable, attachments } = {}) {
  const chat = findChat(state.activeChatId);
  const sameAuthor = messagesEl.lastElementChild?.dataset.role === role;
  const div = document.createElement('div');
  div.className = `msg ${role}${sameAuthor ? ' same-author' : ''} arrive`;
  div.dataset.role = role;
  if (Number.isInteger(idx)) div.dataset.idx = String(idx);
  const name = state.name || (role === 'user' ? 'Вы' : 'NAOMI');
  const time = ts ? fmtTime(ts) : '';
  // вложения в отправленном сообщении: фото — сетка-коллаж, файлы — чипы
  const media = role === 'user' ? (attachments ?? []).filter((a) => a.thumb) : [];
  const docs = role === 'user' ? (attachments ?? []).filter((a) => !a.thumb) : [];
  let attHtml = '';
  if (media.length) {
    const show = media.slice(0, 4);
    const more = media.length - show.length;
    attHtml += `<div class="media-grid mg-${show.length}">${show.map((a, i) => {
      const last = i === show.length - 1;
      return `<div class="mg-cell"><img src="${a.thumb}" alt="">${last && more > 0 ? `<span class="mg-more">+${more}</span>` : ''}</div>`;
    }).join('')}</div>`;
  }
  if (docs.length) {
    attHtml += `<div class="attach-row in-msg">${docs.map((a) => `<div class="attach-doc"><span class="ad-ico">${a.kind === 'image' ? ATTACH_IMG_ICON : ATTACH_FILE_ICON}</span><span class="ad-texts"><span class="ad-name"></span><span class="ad-size"></span></span></div>`).join('')}</div>`;
  }
  div.innerHTML = `
    ${role === 'bot' ? '<img src="/img/naomi-logo.webp" class="msg-avatar-sm" alt="">' : ''}
    <div class="msg-content">
      <div class="msg-header">
        ${role === 'bot' ? '<img src="/img/naomi-logo.webp" class="msg-avatar-sm" alt="">' : ''}
        <span class="msg-sender"></span>
        <span class="msg-time"></span>
      </div>
      ${attHtml}
      ${role === 'bot' || (text ?? '').trim() ? '<div class="msg-body"></div>' : ''}
      <div class="msg-actions">
        <button class="msg-act act-copy" type="button" title="Копировать текст">${MSG_COPY_ICON}</button>
        ${role === 'user' && editable ? `<button class="msg-act act-edit" type="button" title="Изменить сообщение">${MSG_EDIT_ICON}</button>` : ''}
      </div>
    </div>`;
  div.querySelector('.msg-sender').textContent = role === 'user' ? name : 'NAOMI';
  div.querySelector('.msg-time').textContent = time;
  if (media.length) {
    const thumbs = media.map((a) => a.thumb);
    div.querySelectorAll('.media-grid .mg-cell').forEach((cell, i) => {
      const img = cell.querySelector('img');
      if (img) img.alt = media[i]?.name ?? '';
      // клик по любой клетке, включая «+N», открывает галерею всех фото с этого места
      cell.addEventListener('click', () => openLightbox(thumbs, i));
    });
  }
  div.querySelectorAll('.attach-doc').forEach((el, i) => {
    el.querySelector('.ad-name').textContent = docs[i]?.name ?? '';
    el.querySelector('.ad-size').textContent = formatSize(docs[i]?.size ?? 0);
  });
  const body = div.querySelector('.msg-body');
  if (body) {
    if (stream) body.innerHTML = renderMarkdown(text) + '<span class="stream-cursor"></span>';
    else if (role === 'user') body.textContent = text;
    else {
      body.innerHTML = renderMarkdown(text);
      finalizeRich(body);
    }
  }
  messagesEl.appendChild(div);
  state.currentBubble = stream ? body : null;
  if (stream && role === 'bot') state.liveBotBubble = body;
  document.querySelector('.chat').classList.remove('chat-empty');
  scrollDown();
  return body;
}

function appendToolCard({ name, args, result, approved }, fromHistory) {
  const div = document.createElement('div');
  div.className = 'tool-card';
  div.innerHTML = `
    <div class="tool-card-inner">
      <div class="tool-head">
        <span class="tool-icon">${TOOL_ICONS[name] ?? '▪'}</span>
        <span class="tool-name"></span>
        <span class="tool-args"></span>
        <span class="tool-status running">выполняется…</span>
      </div>
      <div class="tool-body"><pre></pre></div>
    </div>`;
  div.querySelector('.tool-name').textContent = name;
  div.querySelector('.tool-args').textContent = toolArgsPreview(name, args ?? {});
  const status = div.querySelector('.tool-status');
  const pre = div.querySelector('.tool-body pre');
  if (approved === false || result?.denied) {
    div.classList.add('denied');
    status.textContent = 'отклонено';
    status.className = 'tool-status denied';
    pre.textContent = 'Отклонено пользователем';
  } else if (result) {
    status.textContent = result?.error ? 'ошибка' : 'готово';
    status.className = `tool-status ${result?.error ? 'err' : 'ok'}`;
    pre.textContent = result?.error ?? formatToolResult(result);
  } else if (!fromHistory) {
    status.textContent = 'выполняется…';
    status.className = 'tool-status running';
  } else {
    status.textContent = 'готово';
    status.className = 'tool-status ok';
  }
  div.querySelector('.tool-head').addEventListener('click', () => div.classList.toggle('open'));
  messagesEl.appendChild(div);
  state.lastToolCard = div;
  scrollDown();
}

const TOOL_ICONS = {
  run_command: '⌘', read_file: '▤', write_file: '✎', edit_file: '✂',
  list_dir: '▦', search_files: '◎', search_code: '⌕',
};

function toolArgsPreview(name, args) {
  if (name === 'run_command') return args.command ?? '';
  if (['write_file', 'edit_file', 'read_file'].includes(name)) return args.path ?? '';
  if (name === 'search_code') return `${args.query ?? ''} ${args.path ?? ''}`.trim();
  if (name === 'search_files') return args.pattern ?? '';
  if (name === 'list_dir') return args.path ?? '.';
  return JSON.stringify(args);
}

function formatToolResult(result) {
  if (result?.stdout !== undefined) {
    const parts = [];
    if (result.stdout) parts.push(result.stdout);
    if (result.stderr) parts.push(`[stderr]\n${result.stderr}`);
    if (result.exitCode != null && result.exitCode !== 0) parts.push(`[exit: ${result.exitCode}]`);
    return parts.join('\n') || '(пусто)';
  }
  if (result?.content) return result.content;
  return JSON.stringify(result ?? {}, null, 2);
}

function addErrBanner(message) {
  const div = document.createElement('div');
  div.className = 'err-banner';
  div.innerHTML = '<div class="err-banner-inner"></div>';
  div.querySelector('.err-banner-inner').textContent = message;
  messagesEl.appendChild(div);
  scrollDown();
}

function scrollDown() { messagesEl.scrollTop = messagesEl.scrollHeight; }

/* ─── живой черновик ответа на чат (переживает переключение чатов) ─── */
function draftFor(chatId) {
  if (!state.drafts[chatId]) state.drafts[chatId] = { statusText: '', steps: [], text: '', t0: Date.now() };
  return state.drafts[chatId];
}

/* ─── лента работы агента (Agentic Execution Timeline, как в ZCode) ─── */
const RUN_ICONS = {
  brain: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 5a3 3 0 1 0-5.997.125 4 4 0 0 0-2.526 5.77 4 4 0 0 0 .556 6.588A4 4 0 1 0 12 18Z"/><path d="M12 5a3 3 0 1 1 5.997.125 4 4 0 0 1 2.526 5.77 4 4 0 0 1-.556 6.588A4 4 0 1 1 12 18Z"/><path d="M15 13a4.5 4.5 0 0 1-3-4 4.5 4.5 0 0 1-3 4"/><path d="M17.599 6.5a3 3 0 0 0 .399-1.375"/><path d="M6.003 5.125A3 3 0 0 0 6.401 6.5"/><path d="M3.477 10.896a4 4 0 0 1 .585-.396"/><path d="M19.938 10.5a4 4 0 0 1 .585.396"/><path d="M6 18a4 4 0 0 1-1.967-.516"/><path d="M19.967 17.484A4 4 0 0 1 18 18"/></svg>',
  search: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/></svg>',
  pencil: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 3a2.85 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z"/><path d="m15 5 4 4"/></svg>',
  terminal: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="4 17 10 11 4 5"/><line x1="12" x2="20" y1="19" y2="19"/></svg>',
};

function toolStepInfo(name, args) {
  const base = (args?.path ?? '').split(/[\\/]/).pop();
  switch (name) {
    case 'run_command': return { icon: 'terminal', title: 'Terminal', meta: '· 1 command' };
    case 'read_file': return { icon: 'search', title: 'Explore', meta: base ? `· ${base}` : '· 1 file' };
    case 'list_dir': return { icon: 'search', title: 'Explore', meta: '· 1 folder' };
    case 'search_code':
    case 'search_files': return { icon: 'search', title: 'Search', meta: '· 1 search' };
    case 'write_file':
    case 'edit_file': return { icon: 'pencil', title: 'Edited', meta: base ? `· ${base}` : '· 1 file' };
    default: return { icon: 'terminal', title: 'Tool', meta: '' };
  }
}

function formatWorked(ms) {
  const s = Math.max(1, Math.round(ms / 1000));
  return s < 60 ? `${s} s` : `${Math.floor(s / 60)}m ${s % 60} s`;
}

/* живой блок ленты активного чата */
function runEnsure(chatId) {
  if (state.run?.chatId === chatId && state.run.el?.isConnected) return state.run;
  const draft = state.drafts[chatId];
  if (draft && (draft.steps.length || draft.text)) return runRebuild(chatId, draft);
  const block = document.createElement('div');
  block.className = 'run-block live open';
  block.innerHTML = `
    <button class="run-bar" type="button">
      <span class="run-bar-text">Worked for 0 s</span>
      <span class="run-caret">›</span>
    </button>
    <div class="run-steps"></div>
    <div class="run-now"><span class="run-spin"></span><span class="run-now-text">думает…</span></div>`;
  block.querySelector('.run-bar').addEventListener('click', () => block.classList.toggle('open'));
  messagesEl.appendChild(block);
  state.run = { chatId, el: block, stepsEl: block.querySelector('.run-steps'), nowEl: block.querySelector('.run-now-text') };
  return state.run;
}

/* что ИИ делает прямо сейчас — строка под лентой */
function runSetNow(chatId, text) {
  if (state.activeChatId !== chatId || state.run?.chatId !== chatId) return;
  if (state.run.nowEl) state.run.nowEl.textContent = text;
}

/* пересборка ленты из черновика (возврат в чат посреди работы) */
function runRebuild(chatId, draft) {
  messagesEl.querySelectorAll('.run-block').forEach((b) => b.remove());
  const block = document.createElement('div');
  block.className = 'run-block live open';
  block.innerHTML = `
    <button class="run-bar" type="button">
      <span class="run-bar-text">Worked for ${formatWorked(Date.now() - draft.t0)}</span>
      <span class="run-caret">›</span>
    </button>
    <div class="run-steps"></div>
    <div class="run-now"><span class="run-spin"></span><span class="run-now-text">${draft.statusText || 'думает…'}</span></div>`;
  block.querySelector('.run-bar').addEventListener('click', () => block.classList.toggle('open'));
  const stepsEl = block.querySelector('.run-steps');
  for (const step of draft.steps) {
    if (step.kind === 'text') {
      stepsEl.append(runNoteEl(step));
    } else {
      stepsEl.append(runStepItem(step));
    }
  }
  messagesEl.append(block);
  state.run = { chatId, el: block, stepsEl, nowEl: block.querySelector('.run-now-text') };
  return state.run;
}

/* шаг ленты: кликабельная строка + раскрываемые детали */
function runStepItem(step) {
  const item = document.createElement('div');
  item.className = 'run-item';
  const row = document.createElement('div');
  row.className = 'run-row' + (step.status === 'err' ? ' err' : '');
  const note = document.createElement('div');
  note.className = 'run-note' + (step.kind === 'thought' ? ' thought-note' : ' tool-detail');

  if (step.kind === 'thought') {
    row.innerHTML = `<span class="run-ico">${RUN_ICONS.brain}</span><span class="run-name">Thinking</span><span class="run-meta">${step.dur ? `· ${step.dur} s` : ''}</span>`;
    note.textContent = step.text ?? '';
    row.classList.add('clickable');
    row.addEventListener('click', () => item.classList.toggle('open'));
  } else {
    const info = toolStepInfo(step.name, step.args);
    row.innerHTML = `<span class="run-ico">${RUN_ICONS[info.icon]}</span><span class="run-name">${info.title}</span><span class="run-meta">${step.meta || info.meta || ''}</span>`;
    note.textContent = toolDetailText(step.name, step.args);
    row.classList.add('clickable');
    row.addEventListener('click', () => item.classList.toggle('open'));
  }

  item.append(row, note);
  step.itemEl = item;
  step.rowEl = row;
  step.noteEl = note;
  return item;
}

function toolDetailText(name, args) {
  switch (name) {
    case 'run_command': return args?.command ?? '';
    case 'read_file':
    case 'write_file':
    case 'edit_file': return args?.path ?? '';
    case 'search_code': return `${args?.query ?? ''} → ${args?.path ?? '.'}`;
    case 'search_files': return args?.pattern ?? '';
    case 'list_dir': return args?.path ?? '.';
    default: return JSON.stringify(args ?? {}).slice(0, 300);
  }
}

function runNoteEl(step) {
  const note = document.createElement('div');
  note.className = 'run-note' + (step.kind === 'thought' ? ' thought-note' : '');
  note.textContent = step.text ?? '';
  return note;
}

function runAddReasoning(chatId, chunk) {
  if (!chunk) return;
  const draft = state.drafts[chatId];
  let last = draft.steps[draft.steps.length - 1];
  if (!last || last.kind !== 'thought') {
    runCloseThought(chatId);
    last = { kind: 'thought', text: '', t0: Date.now() };
    draft.steps.push(last);
    if (state.activeChatId === chatId) {
      const run = runEnsure(chatId);
      run.stepsEl.append(runStepItem(last));
      // мысль показывается одной строкой; целиком — только по клику
    }
  }
  last.text += chunk;
  if (last.noteEl) {
    last.noteEl.textContent = last.text;
    last.noteEl.scrollTop = last.noteEl.scrollHeight;
  }
  scrollDown();
}

function runCloseThought(chatId) {
  const draft = state.drafts[chatId];
  const last = draft?.steps[draft.steps.length - 1];
  if (last?.kind === 'thought' && !last.dur) {
    last.dur = Math.max(1, Math.round((Date.now() - last.t0) / 1000));
    if (last.rowEl) {
      const meta = last.rowEl.querySelector('.run-meta');
      if (meta) meta.textContent = `· ${last.dur} s`;
    }
    // досчитало — сворачиваем детали (клик по строке раскроет снова)
    last.itemEl?.classList.remove('open');
  }
}

function runAddCommentary(chatId, text) {
  runCloseThought(chatId);
  const draft = state.drafts[chatId];
  const step = { kind: 'text', text };
  draft.steps.push(step);
  if (state.activeChatId === chatId) {
    const run = runEnsure(chatId);
    run.stepsEl.append(runNoteEl(step));
  }
}

function runAddTool(chatId, name, args) {
  runCloseThought(chatId);
  const draft = state.drafts[chatId];
  const step = { kind: 'tool', name, args, status: 'running', t0: Date.now() };
  draft.steps.push(step);
  if (state.activeChatId === chatId) {
    const run = runEnsure(chatId);
    run.stepsEl.append(runStepItem(step));
  }
}

function runToolDone(chatId, result, approved) {
  const draft = state.drafts[chatId];
  const last = [...(draft?.steps ?? [])].reverse().find((s) => s.kind === 'tool' && s.status === 'running');
  if (!last) return;
  last.status = approved === false ? 'denied' : result?.error ? 'err' : 'ok';
  last.dur = Math.max(1, Math.round((Date.now() - last.t0) / 1000));

  // мета: путь + сколько строк добавлено/удалено (как в ZCode)
  const info = toolStepInfo(last.name, last.args);
  let metaText = info.meta || '';
  if (last.status === 'ok' && result) {
    if (result.addedLines != null) metaText = `${info.meta || ''} · +${result.addedLines} −${result.removedLines ?? 0}`.replace(/^ · /, '· ');
    else if (result.lines != null) metaText = `${info.meta || ''} · ${result.lines} строк`;
  }
  last.meta = metaText;

  if (state.activeChatId !== chatId || state.run?.chatId !== chatId) return;
  const row = last.rowEl;
  if (!row) return;
  const meta = row.querySelector('.run-meta');
  if (!meta) return;
  if (last.status === 'denied') { meta.textContent = '· отклонено'; }
  else if (last.status === 'err') { meta.textContent = '· error'; row.classList.add('err'); }
  else if (result?.addedLines != null) {
    meta.innerHTML = `${info.meta || ''} · <span class="run-add">+${result.addedLines}</span> <span class="run-rem">−${result.removedLines ?? 0}</span>`;
  } else if (result?.lines != null) {
    meta.textContent = `${info.meta || ''} · ${result.lines} строк`;
  } else {
    meta.textContent = `· ${last.dur} s`;
  }
}

/* завершение: бар замирает в «Worked for Xm Ys», лента сворачивается */
function runFinalize(chatId, workedMs) {
  runCloseThought(chatId);
  if (state.activeChatId !== chatId || state.run?.chatId !== chatId) return;
  const run = state.run;
  if (!run.el.isConnected) { state.run = null; return; }
  run.el.classList.remove('live', 'open');
  const text = run.el.querySelector('.run-bar-text');
  if (text) text.textContent = `Worked for ${formatWorked(workedMs)}`;
  state.run = null;
}

/* свёрнутая лента из истории сообщения */
function runMountHistory(m) {
  const block = document.createElement('div');
  block.className = 'run-block';
  block.innerHTML = `
    <button class="run-bar" type="button">
      <span class="run-bar-text">Worked for ${formatWorked(m.workedMs ?? 0)}</span>
      <span class="run-caret">›</span>
    </button>
    <div class="run-steps"></div>`;
  const stepsEl = block.querySelector('.run-steps');
  for (const step of m.steps) {
    if (step.kind === 'text') {
      stepsEl.append(runNoteEl(step));
    } else {
      stepsEl.append(runStepItem(step));
    }
  }
  block.querySelector('.run-bar').addEventListener('click', () => block.classList.toggle('open'));
  messagesEl.appendChild(block);
}

/* тикающий бар живой ленты */
setInterval(() => {
  const bar = messagesEl.querySelector('.run-block.live .run-bar-text');
  if (!bar) return;
  const draft = state.drafts[state.activeChatId];
  if (draft?.t0) bar.textContent = `Worked for ${formatWorked(Date.now() - draft.t0)}`;
}, 500);

/* ─── статусная строка «что делает агент» ─── */
const TOOL_VERBS = {
  run_command: 'выполняет команду',
  read_file: 'читает файл',
  write_file: 'записывает файл',
  edit_file: 'редактирует файл',
  list_dir: 'смотрит папку',
  search_files: 'ищет файлы',
  search_code: 'ищет по коду',
};

function setAgentStatus(text, t0) {
  const el = $('agentStatus');
  if (!el) return;
  if (text) {
    state.statusBase = text;
    state.statusT0 = t0 ?? state.statusT0 ?? Date.now();
    const inputRect = $('input').getBoundingClientRect();
    const chatRect = document.querySelector('.chat').getBoundingClientRect();
    el.style.paddingLeft = `${Math.max(24, Math.round(inputRect.left - chatRect.left))}px`;
    el.hidden = false;
    el.textContent = `⚙ ${text}`;
  } else {
    state.statusBase = '';
    state.statusT0 = 0;
    el.hidden = true;
  }
}

setInterval(() => {
  const el = $('agentStatus');
  if (el && !el.hidden && state.statusBase && state.statusT0) {
    const sec = Math.round((Date.now() - state.statusT0) / 1000);
    el.textContent = `⚙ ${state.statusBase} · ${sec} с`;
  }
  // сторож: статус/лента живут, а сервер давно не занят этим чатом → SSE-финал потерян
  if (state.busyChat && state.statusT0 && Date.now() - state.statusT0 > 25000) {
    if (state.watchdogBusy) return;
    state.watchdogBusy = true;
    recoverStuckChat(state.busyChat).finally(() => { state.watchdogBusy = false; });
  }
}, 1000);

/* SSE-финал потерян: берём историю с сервера и довосстанавливаем ленту */
async function recoverStuckChat(chatId) {
  try {
    const res = await fetch(`/api/chats/${chatId}/messages`);
    if (!res.ok) return;
    const data = await res.json();
    if (data.busy) return; // сервер ещё работает — не вмешиваемся
    const chat = findChat(chatId);
    if (!chat) return;
    chat.messages = data.history.map((m) => ({
      role: m.role, text: m.text,
      ts: chat.messages.find((x) => x.role === m.role && x.text === m.text)?.ts ?? Date.now(),
    }));
    persistChat(chat);
    delete state.drafts[chatId];
    setBusy(chatId, false);
    setAgentStatus('');
    stopTypewriter();
    state.streamText = '';
    state.currentBubble = null;
    state.liveBotBubble = null;
    messagesEl.querySelectorAll('.run-block').forEach((b) => b.remove());
    if (chatId === state.activeChatId) renderChat();
    refreshBalance();
  } catch {}
}

/* ─── hero для неактивированных ─── */
function showHero() {
  messagesEl.innerHTML = `
    <div class="auth-hero auth-hero-static">
      <img src="/img/naomi-logo.webp" alt="NAOMI" class="hero-logo">
      <div class="hero-sub">Агент для вашего ПК: выполняет команды, создаёт и правит файлы, ищет по коду.<br>Войдите по ключу доступа, чтобы начать.</div>
      <button class="hero-btn" id="heroLogin">Войти по ключу</button>
    </div>`;
  $('heroLogin').addEventListener('click', openLogin);
  $('emptyGreeting').style.display = 'none';
}

/* ─── вход ─── */
function openLogin() {
  setAuthMode('login');
  $('loginOverlay').classList.add('open');
}

function closeLogin() { $('loginOverlay').classList.remove('open'); }

function parseActivation(raw) {
  const input = String(raw ?? '').trim();
  if (input.startsWith('naomi_')) return { apiKey: input };
  try {
    const url = new URL(input);
    const params = new URLSearchParams(url.search + '&' + url.hash.replace(/^#/, ''));
    const hashKey = (url.hash.match(/naomi_[a-z0-9]+/i) ?? [])[0];
    const pathKey = (url.pathname.match(/naomi_[a-z0-9]+/i) ?? [])[0];
    const apiKey = params.get('key') ?? hashKey ?? pathKey;
    if (!apiKey?.startsWith('naomi_')) return { error: 'в ссылке не найден ключ naomi_…' };
    return { apiKey, server: url.origin };
  } catch {
    return { error: 'это не ключ и не ссылка. Ключ выглядит так: naomi_ab12cd…' };
  }
}

async function login(apiKey, server) {
  const errEl = $('loginError');
  errEl.hidden = true;
  $('doLoginBtn').disabled = true;
  $('doLoginBtn').textContent = 'Проверяем…';
  try {
    await fetch('/api/config', {
      method: 'POST',
      headers: { 'content-type': 'application/json' },
      body: JSON.stringify({ ...(server ? { server } : {}), apiKey }),
    });
    const res = await fetch('/api/balance');
    const json = await res.json().catch(() => ({}));
    if (!res.ok) {
      await fetch('/api/config', {
        method: 'POST',
        headers: { 'content-type': 'application/json' },
        body: JSON.stringify({ apiKey: null, resetSessions: false }),
      }).catch(() => {});
      errEl.textContent = json.error?.message ?? 'Не удалось активировать ключ';
      errEl.hidden = false;
      return false;
    }
    const st = await (await fetch('/api/state')).json();
    if (!json.models?.includes(st.config.model) && json.models?.length) {
      await fetch('/api/config', {
        method: 'POST',
        headers: { 'content-type': 'application/json' },
        body: JSON.stringify({ model: json.models[0], resetSessions: false }),
      });
    }
    state.activated = true;
    if (!store.name) {
      const email = localStorage.getItem('naomi_email');
      store.name = email ? email.split('@')[0] : '';
    }
    applyIdentity(json.label || 'активен');
    closeLogin();
    // createChat уже ставит activeChatId сам; store.chats[0] существует только
    // при непустой истории — у свежего аккаунта её нет, и обращение к [0].id падало
    if (store.chats.length) state.activeChatId = store.chats[0].id;
    else createChat(true);
    renderHistory();
    renderChat();
    refreshBalance();
    return true;
  } catch (err) {
    errEl.textContent = `Ошибка соединения: ${err.message}`;
    errEl.hidden = false;
    return false;
  } finally {
    $('doLoginBtn').disabled = false;
    $('doLoginBtn').textContent = 'Войти';
  }
}

$('loginOverlay').addEventListener('click', (e) => { if (e.target === e.currentTarget) closeLogin(); });

/* ─── переключение вход / регистрация + соц-кнопки ─── */
function setAuthMode(mode) {
  const reg = mode === 'register';
  $('loginTitle').textContent = reg ? 'Создание аккаунта' : 'Вход в NAOMI';
  $('loginView').hidden = reg;
  $('regView').hidden = !reg;
  $('switchToRegRow').hidden = reg;
  $('switchToLoginRow').hidden = !reg;
  $('loginError').hidden = true;
  setTimeout(() => (reg ? $('regName') : $('loginId')).focus(), 80);
}

$('switchToReg').addEventListener('click', (e) => { e.preventDefault(); setAuthMode('register'); });
$('switchToLogin').addEventListener('click', (e) => { e.preventDefault(); setAuthMode('login'); });

$('doLoginBtn').addEventListener('click', async (e) => {
  e.preventDefault();
  const errEl = $('loginError');
  errEl.hidden = true;
  $('doLoginBtn').disabled = true;
  try {
    const res = await fetch('/api/account/login', {
      method: 'POST',
      headers: { 'content-type': 'application/json' },
      body: JSON.stringify({ login: $('loginId').value.trim(), password: $('loginPass').value }),
    });
    const json = await res.json();
    if (!res.ok) { errEl.textContent = json.error?.message ?? 'Ошибка входа'; errEl.hidden = false; return; }
    await applyKeyFromAccount(json, true);
  } catch (err) {
    errEl.textContent = err.message;
    errEl.hidden = false;
  } finally {
    $('doLoginBtn').disabled = false;
  }
});

$('doRegBtn').addEventListener('click', async (e) => {
  e.preventDefault();
  const errEl = $('loginError');
  errEl.hidden = true;
  $('doRegBtn').disabled = true;
  $('doRegBtn').textContent = 'Создаём…';
  try {
    const res = await fetch('/api/account/register', {
      method: 'POST',
      headers: { 'content-type': 'application/json' },
      body: JSON.stringify({
        name: $('regName').value.trim(),
        email: $('regEmail').value.trim(),
        phone: $('regPhone').value.trim(),
        password: $('regPass').value,
      }),
    });
    const json = await res.json();
    if (!res.ok) { errEl.textContent = json.error?.message ?? 'Ошибка регистрации'; errEl.hidden = false; return; }
    if (json.name) store.name = json.name;
    await applyKeyFromAccount(json, true);
  } catch (err) {
    errEl.textContent = err.message;
    errEl.hidden = false;
  } finally {
    $('doRegBtn').disabled = false;
    $('doRegBtn').textContent = 'Создать аккаунт';
  }
});

const SOCIALS = [
  { id: 'socialGoogle', provider: 'google', label: 'Google' },
  { id: 'socialYandex', provider: 'yandex', label: 'Яндекс ID' },
  { id: 'socialVk', provider: 'vk', label: 'VK ID' },
];

for (const social of SOCIALS) {
  $(social.id).addEventListener('click', async () => {
    const cfg = await fetch('/api/reg-config').then((r) => r.json()).catch(() => null);
    const url = cfg?.oauth_urls?.[social.provider];
    if (url) {
      window.open(`${url}?origin=${encodeURIComponent(location.origin)}`, '_blank');
      return;
    }
    const errEl = $('loginError');
    errEl.hidden = false;
    errEl.textContent = `${social.label} подключается: войдите по email/телефону — данные сохранятся`;
  });
}

async function applyKeyFromAccount(json, rememberEmail) {
  if (rememberEmail) {
    localStorage.setItem('naomi_email', json.email);
    $('ppRotate').hidden = false;
    if (!store.name) store.name = json.email.split('@')[0];
  }
  const ok = await login(json.key);
  if (ok && json.created) {
    addNotice(`Аккаунт создан! На балансе ${json.balance} ₡ для знакомства 🎉`);
  }
}

function addNotice(text, { type = 'ok' } = {}) {
  const warn = type === 'warn';
  const host = $('noticeHost');
  if (!host) return;
  const div = document.createElement('div');
  div.className = warn ? 'ok-notice warn' : 'ok-notice';
  const span = document.createElement('span');
  span.className = 'notice-text';
  span.textContent = text;
  const close = document.createElement('button');
  close.type = 'button';
  close.className = 'notice-close';
  close.setAttribute('aria-label', 'Закрыть уведомление');
  close.textContent = '✕';
  close.addEventListener('click', () => div.remove());
  div.append(span, close);
  // живёт в отдельном контейнере над полем ввода: перерисовка ленты сообщений
  // (renderChat) его не затирает — уведомление можно спокойно прочитать
  host.appendChild(div);
  scrollDown();
  // предупреждения не исчезают сами — читаются спокойно и закрываются крестиком;
  // успешные — ненавязчиво уходят сами через 8с (крестик тоже есть)
  if (!warn) setTimeout(() => div.remove(), 8000);
}

/* ─── перевыпуск ключа (ротация) ─── */
$('ppRotate').addEventListener('click', (e) => {
  e.stopPropagation();
  $('profilePopover').classList.remove('open');
  const email = localStorage.getItem('naomi_email');
  const pass = prompt(`Перевыпустить ключ для ${email ?? 'аккаунта'}?\nВведите пароль для подтверждения:`);
  if (pass === null) return;
  rotateKey(email ?? '', pass);
});

async function rotateKey(email, password) {
  try {
    const res = await fetch('/api/account/rotate', {
      method: 'POST',
      headers: { 'content-type': 'application/json' },
      body: JSON.stringify({ email, password }),
    });
    const json = await res.json();
    if (!res.ok) { alert(json.error?.message ?? 'Не удалось перевыпустить ключ'); return; }
    const ok = await login(json.key);
    if (ok) addNotice(`Ключ перевыпущен: ${json.key.slice(0, 16)}…\nСтарый ключ удалён, баланс ${json.balance} ₡ сохранён`);
  } catch (err) {
    alert(err.message);
  }
}



/* ─── профиль: состояние гость / активен ─── */
let shownApiKey = null;

function applyIdentity(licenseText) {
  const active = state.activated;
  const name = store.name || '';
  $('profileName').textContent = active ? name : '';
  $('profileAvatar').textContent = active ? (name.slice(0, 1).toUpperCase() || '•') : '?';
  $('ppName').textContent = active ? name : '';
  $('ppAvatar').textContent = active ? (name.slice(0, 1).toUpperCase() || '•') : '?';
  $('ppLicense').textContent = active ? (licenseText ?? '') : 'не выполнен вход';
  $('widgetGear').style.display = active ? '' : 'none';
  $('ppLoginBtn').hidden = active;
  $('ppShowKey').hidden = !active;
  $('ppShowKey').textContent = 'Показать ключ';
  shownApiKey = null;
  $('ppKeyLine').hidden = true;
  $('ppBalance').hidden = !active;
  $('ppRotate').hidden = !(active && localStorage.getItem('naomi_email'));
  $('ppLogout').hidden = !active;
  $('btnLogin').hidden = active;
  $('btnTopup').hidden = !active;
  $('ppNameInput').value = store.name;
}

$('btnLogin').addEventListener('click', () => openLogin());
$('ppLoginBtn').addEventListener('click', (e) => {
  e.stopPropagation();
  $('profilePopover').classList.remove('open');
  openLogin();
});

$('ppShowKey').addEventListener('click', async (e) => {
  e.stopPropagation();
  const line = $('ppKeyLine');
  if (shownApiKey) {
    shownApiKey = null;
    line.hidden = true;
    $('ppShowKey').textContent = 'Показать ключ';
    return;
  }
  const st = await (await fetch('/api/state')).json().catch(() => null);
  if (!st?.config?.apiKey) return;
  shownApiKey = st.config.apiKey;
  line.hidden = false;
  line.textContent = `${shownApiKey}  (нажмите, чтобы скопировать)`;
  $('ppShowKey').textContent = 'Скрыть ключ';
  line.onclick = async (ev) => {
    ev.stopPropagation();
    try {
      await navigator.clipboard.writeText(shownApiKey);
      line.textContent = 'Скопировано ✓';
      setTimeout(() => { line.textContent = shownApiKey; }, 1200);
    } catch {}
  };
});

$('ppNameSave').addEventListener('click', () => {
  const prev = store.name;
  store.name = $('ppNameInput').value.trim();
  applyIdentity('активен');
  // приветствие остаётся тем же — плавно подставляется только новое имя
  if (store.name !== prev) {
    const cur = findChat(state.activeChatId);
    if (cur && !cur.messages.length) refreshGreetingName();
  }
});

/* ─── поповеры: профиль (клик по блоку) и настройки (шестерёнка) ─── */
function positionPopover(popId, anchor) {
  const pop = $(popId);
  if (!pop || !anchor) return;
  const r = anchor.getBoundingClientRect();
  pop.classList.add('open');
  const h = pop.offsetHeight;
  const w = pop.offsetWidth;
  pop.classList.remove('open');
  pop.style.left = `${Math.max(10, Math.min(Math.round(r.left), window.innerWidth - w - 10))}px`;
  pop.style.top = `${Math.max(66, Math.round(r.top - h - 12))}px`;
}

function togglePopover(popId, force) {
  const pop = $(popId);
  const open = force ?? !pop.classList.contains('open');
  for (const other of ['profilePopover']) {
    if (other !== popId) $(other).classList.remove('open');
  }
  if (open) {
    positionPopover(popId, $('profileWidget'));
    requestAnimationFrame(() => pop.classList.add('open'));
  } else {
    pop.classList.remove('open');
  }
}

$('profileWidget').addEventListener('click', (e) => {
  e.stopPropagation();
  if (!state.activated) { openLogin(); return; }
  togglePopover('profilePopover');
});

$('widgetGear').addEventListener('click', (e) => {
  e.stopPropagation();
  $('profilePopover').classList.remove('open');
  openSettings();
});

/* ─── настройки: большое модальное окно ─── */
let settingsKeyShown = false;

function openSettings() {
  fillSettings();
  $('settingsOverlay').classList.add('open');
}

async function fillSettings() {
  const email = localStorage.getItem('naomi_email') || store.name || '';
  $('setEmail').textContent = state.activated ? (email || 'аккаунт по ключу') : 'не выполнен вход';
  $('setBalance').textContent = state.activated ? ($('balanceText')?.textContent?.trim() || '—') : '—';
  $('setVersion').textContent = 'NAOMI Cloud v0.1.0';
  $('setLogout').style.display = state.activated ? '' : 'none';
  settingsKeyShown = false;
  $('setShowKey').textContent = 'Показать ключ';
  $('setKey').textContent = '…';
  $('setKey').dataset.full = '';
  try {
    const st = await (await fetch('/api/state')).json();
    const key = st.config.apiKey || '';
    $('setKey').textContent = key ? `${key.slice(0, 12)}…${key.slice(-4)}` : 'не настроен';
    // полный ключ — отдельный локальный эндпоинт (state отдаёт замаскированный)
    const full = await fetch('/api/key').then((r) => r.json()).catch(() => ({}));
    $('setKey').dataset.full = full.key || key;
  } catch {
    $('setKey').textContent = 'не удалось получить';
  }
}

$('setClose').addEventListener('click', () => $('settingsOverlay').classList.remove('open'));
$('settingsOverlay').addEventListener('click', (e) => {
  if (e.target === $('settingsOverlay')) $('settingsOverlay').classList.remove('open');
});
$('setLogout').addEventListener('click', async () => {
  $('settingsOverlay').classList.remove('open');
  await fetch('/api/config', {
    method: 'POST',
    headers: { 'content-type': 'application/json' },
    body: JSON.stringify({ apiKey: null }),
  });
  location.reload();
});

$('setShowKey').addEventListener('click', (e) => {
  e.stopPropagation();
  const el = $('setKey');
  if (!el.dataset.full) return;
  settingsKeyShown = !settingsKeyShown;
  el.textContent = settingsKeyShown ? el.dataset.full : `${el.dataset.full.slice(0, 12)}…${el.dataset.full.slice(-4)}`;
  $('setShowKey').textContent = settingsKeyShown ? 'Скрыть ключ' : 'Показать ключ';
});

async function copyText(text, btn) {
  if (!text) return;
  // clipboard API бывает недоступен/зависает в http-контекстах и вебвью —
  // гоним с таймаутом, при неудаче запасной путь через скрытый textarea
  let ok = false;
  try {
    await Promise.race([
      navigator.clipboard.writeText(text),
      new Promise((_, rej) => setTimeout(() => rej(new Error('timeout')), 1200)),
    ]);
    ok = true;
  } catch {}
  if (!ok) {
    try {
      const ta = document.createElement('textarea');
      ta.value = text;
      ta.style.cssText = 'position:fixed;opacity:0;pointer-events:none';
      document.body.appendChild(ta);
      ta.select();
      ok = document.execCommand('copy');
      ta.remove();
    } catch {}
  }
  const old = 'Скопировать ключ';
  btn.disabled = true;
  btn.textContent = ok ? 'Скопировано ✓' : 'Не удалось — выдели вручную';
  setTimeout(() => { btn.textContent = old; btn.disabled = false; }, 1500);
}

$('setCopyKey').addEventListener('click', (e) => {
  e.stopPropagation();
  copyText($('setKey').dataset.full || '', e.currentTarget);
});

document.addEventListener('click', (e) => {
  if (!e.target.closest('.profile-widget') && !e.target.closest('.profile-popover')) {
    $('profilePopover').classList.remove('open');
  }
});
window.addEventListener('resize', () => {
  if ($('profilePopover').classList.contains('open')) positionPopover('profilePopover', $('profileWidget'));
});
$('btnTopup').addEventListener('click', () => openTopup());

/* ─── пополнение ─── */
const topup = { config: null, selected: null, requestId: null, pollTimer: null };

async function openTopup() {
  $('profilePopover').classList.remove('open');
  $('topupError').hidden = true;
  $('topupDone').hidden = true;
  $('topupManualBtn').disabled = false;
  const res = await fetch('/api/gw/payments/config').catch(() => null);
  const json = res ? await res.json().catch(() => null) : null;
  if (!json || json.error) {
    $('topupError').textContent = json?.error ?? 'Не удалось загрузить способы оплаты';
    $('topupError').hidden = false;
    return;
  }
  topup.config = json;
  const packsEl = $('topupPacks');
  packsEl.innerHTML = '';
  topup.selected = json.packs[0]?.credits ?? null;
  for (const p of json.packs) {
    const btn = document.createElement('button');
    btn.className = 'topup-pack' + (p.credits === topup.selected ? ' selected' : '');
    btn.innerHTML = `<div class="pack-amount"></div><div class="pack-price"></div>`;
    btn.querySelector('.pack-amount').textContent = `${p.credits} ₡`;
    btn.querySelector('.pack-price').textContent = `${p.price} ₽`;
    btn.addEventListener('click', () => {
      topup.selected = p.credits;
      packsEl.querySelectorAll('.topup-pack').forEach((b) => b.classList.remove('selected'));
      btn.classList.add('selected');
    });
    packsEl.appendChild(btn);
  }
  const manual = json.methods.includes('manual');
  const card = json.methods.includes('yookassa');
  $('topupManualBtn').hidden = !manual;
  $('topupCardBtn').hidden = !card;
  if (manual) {
    const info = $('topupInfo');
    info.hidden = false;
    info.textContent = `${json.payment_info}\n\nПосле оплаты нажмите кнопку ниже — владелец подтвердит заявку, и баланс пополнится автоматически.`;
  }
  $('topupOverlay').classList.add('open');
}

function closeTopup() {
  $('topupOverlay').classList.remove('open');
  if (topup.pollTimer) { clearInterval(topup.pollTimer); topup.pollTimer = null; }
}

$('topupOverlay').addEventListener('click', (e) => { if (e.target === e.currentTarget) closeTopup(); });

$('topupManualBtn').addEventListener('click', async () => {
  const errEl = $('topupError');
  errEl.hidden = true;
  $('topupManualBtn').disabled = true;
  try {
    const res = await fetch('/api/gw/topup/request', {
      method: 'POST',
      headers: { 'content-type': 'application/json' },
      body: JSON.stringify({ credits: topup.selected }),
    });
    const json = await res.json();
    if (!res.ok) { errEl.textContent = json.error?.message ?? 'Ошибка заявки'; errEl.hidden = false; return; }
    $('topupDone').hidden = false;
    $('topupDone').textContent = `Заявка №${json.id} на ${json.credits} ₡ отправлена ✓\nКак только владелец подтвердит платёж — баланс пополнится сам.`;
    topup.requestId = json.id;
    topup.pollTimer = setInterval(pollTopup, 10000);
  } catch (err) {
    errEl.textContent = err.message;
    errEl.hidden = false;
  } finally {
    $('topupManualBtn').disabled = false;
  }
});

$('topupCardBtn').addEventListener('click', async () => {
  const errEl = $('topupError');
  errEl.hidden = true;
  try {
    const res = await fetch('/api/gw/payments/yookassa/create', {
      method: 'POST',
      headers: { 'content-type': 'application/json' },
      body: JSON.stringify({ credits: topup.selected }),
    });
    const json = await res.json();
    if (!res.ok) { errEl.textContent = json.error?.message ?? 'Ошибка создания платежа'; errEl.hidden = false; return; }
    if (json.confirmation_url) window.open(json.confirmation_url, '_blank');
    topup.requestId = json.id;
    topup.pollTimer = setInterval(pollTopup, 10000);
  } catch (err) {
    errEl.textContent = err.message;
    errEl.hidden = false;
  }
});

async function pollTopup() {
  if (!topup.requestId) return;
  try {
    const res = await fetch(`/api/gw/topup/status?id=${topup.requestId}`);
    const json = await res.json();
    if (json.status === 'paid') {
      if (topup.pollTimer) clearInterval(topup.pollTimer);
      topup.pollTimer = null;
      $('topupDone').hidden = false;
      $('topupDone').textContent = 'Баланс пополнен ✓';
      refreshBalance();
      setTimeout(closeTopup, 1500);
    }
  } catch {}
}
$('ppLogout').addEventListener('click', async () => {
  $('profilePopover').classList.remove('open');
  await fetch('/api/config', {
    method: 'POST',
    headers: { 'content-type': 'application/json' },
    body: JSON.stringify({ apiKey: null }),
  });
  location.reload();
});

/* ─── пилюли у поля ввода: режим подтверждений и уровень интеллекта ─── */
const POLICY_META = {
  safe: { text: 'Об опасном' },
  ask: { text: 'Обо всём' },
  auto: { text: 'Не спрашивать' },
};

const MODEL_LEVELS = [
  { model: 'glm-4.5-air', name: 'Эконом' },
  { model: 'glm-5.3-flash', name: 'Стандарт' },
  { model: 'glm-5.3', name: 'Максимум' },
];

// бесплатные flash у z.ai хронически деградируют (TTFT до 30с+) — «Эконом»
// переведён на дешёвую платную glm-4.5-air (~$0.07/1M, отвечает за ~0.5с);
// старый сохранённый выбор мигрируем на неё же
function normalizeModel(model) {
  return (model === 'glm-4.5-flash' || model === 'glm-4.7-flash') ? 'glm-4.5-air' : model;
}

function levelForModel(model) {
  return MODEL_LEVELS.find((l) => l.model === model) ?? MODEL_LEVELS[0];
}

function togglePillMenu(menuId, anchorBtn) {
  const menu = $(menuId);
  const willOpen = menu.hidden;
  $('policyMenu').hidden = true;
  $('modelMenu').hidden = true;
  $('attachMenu').hidden = true;
  if (!willOpen) return;
  const r = anchorBtn.getBoundingClientRect();
  menu.hidden = false;
  menu.style.left = `${Math.max(10, Math.min(r.left, window.innerWidth - menu.offsetWidth - 10))}px`;
  menu.style.top = `${Math.max(10, r.top - menu.offsetHeight - 10)}px`;
}

function updatePolicyRow() {
  const meta = POLICY_META[state.policy] ?? POLICY_META.safe;
  $('pillPolicyText').textContent = meta.text;
  document.querySelectorAll('#policyMenu .sm-option').forEach((b) => {
    b.classList.toggle('active', b.dataset.value === state.policy);
  });
}

function updateModelPill() {
  const cfgModel = normalizeModel(localStorage.getItem('naomi_model') ?? 'glm-4.5-air');
  const level = levelForModel(cfgModel);
  $('pillModelText').textContent = level.name;
  document.querySelectorAll('#modelMenu .sm-option').forEach((b) => {
    b.classList.toggle('active', b.dataset.model === level.model);
  });
}

$('pillPolicy').addEventListener('click', (e) => {
  e.stopPropagation();
  togglePillMenu('policyMenu', $('pillPolicy'));
});
$('pillModel').addEventListener('click', (e) => {
  e.stopPropagation();
  togglePillMenu('modelMenu', $('pillModel'));
});
document.addEventListener('click', (e) => {
  if (!e.target.closest('.pill-popover') && !e.target.closest('.pill')) {
    $('policyMenu').hidden = true;
      $('modelMenu').hidden = true;
  }
});

document.querySelectorAll('#policyMenu .sm-option').forEach((b) => {
  b.addEventListener('click', async () => {
    state.policy = b.dataset.value;
    updatePolicyRow();
    $('policyMenu').hidden = true;
    await fetch('/api/config', {
      method: 'POST',
      headers: { 'content-type': 'application/json' },
      body: JSON.stringify({ policy: state.policy, resetSessions: false }),
    });
  });
});

document.querySelectorAll('#modelMenu .sm-option').forEach((b) => {
  b.addEventListener('click', async () => {
    $('modelMenu').hidden = true;
    localStorage.setItem('naomi_model', b.dataset.model);
    updateModelPill();
    await fetch('/api/config', {
      method: 'POST',
      headers: { 'content-type': 'application/json' },
      body: JSON.stringify({ model: b.dataset.model, resetSessions: false }),
    });
    const badge = document.getElementById('modelBadge');
    if (badge) badge.textContent = b.dataset.name;
  });
});

/* ─── баланс ─── */
async function refreshBalance() {
  const dot = $('connDot');
  if (!state.activated) {
    dot.className = 'balance-dot';
    $('balanceText').textContent = '0 ₡';
    return;
  }
  try {
    const res = await fetch('/api/balance');
    const json = await res.json();
    if (!res.ok) {
      dot.className = 'balance-dot err';
      $('balanceText').textContent = json.error?.slice(0, 40) ?? 'нет связи';
      return;
    }
    dot.className = 'balance-dot ok';
    $('balanceText').textContent = `${Number(json.balance).toFixed(2)} ₡`;
    const u = json.usage_30d ?? {};
    $('ppBalance').textContent = `Баланс: ${Number(json.balance).toFixed(2)} ₡ · за 30 дней: ${u.requests ?? 0} запросов`;
  } catch {
    dot.className = 'balance-dot err';
    $('balanceText').textContent = 'шлюз недоступен';
  }
}

/* ─── подтверждения ─── */
function showApproval(info) {
  state.pendingApproval = info;
  $('approvalCmd').textContent = `${info.name}: ${info.reason ?? JSON.stringify(info.args)}`;
  $('approvalBar').hidden = false;
}
function hideApproval() {
  state.pendingApproval = null;
  $('approvalBar').hidden = true;
}
$('btnAllow').addEventListener('click', () => resolveApproval(true));
$('btnDeny').addEventListener('click', () => resolveApproval(false));
async function resolveApproval(approved) {
  if (!state.pendingApproval) return;
  const id = state.pendingApproval.id;
  hideApproval();
  await fetch('/api/approve', {
    method: 'POST',
    headers: { 'content-type': 'application/json' },
    body: JSON.stringify({ id, approved }),
  });
}

/* ─── отправка ─── */
/* занятость и статус привязаны к активному чату: пока ИИ отвечает в одном чате,
   в остальных можно писать — кнопка активна, статус пуст */
function refreshBusyUi() {
  const busyHere = state.busyChat != null && state.busyChat === state.activeChatId;
  sendBtn.disabled = busyHere;
  setAgentStatus(busyHere ? 'выполняет задачу…' : '');
}

function setBusy(chatId, busy) {
  if (chatId) state.busyChat = busy ? chatId : null;
  refreshBusyUi();
}

/* ─── вложения: «+», выбор файлов, чипы ─── */
const ATTACH_FILE_ICON = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M13 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z"/><polyline points="13 2 13 9 20 9"/></svg>';
const ATTACH_IMG_ICON = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="9" cy="9" r="2"/><path d="m21 15-3.09-3.09a2 2 0 0 0-2.82 0L6 21"/></svg>';
const ATTACH_X_ICON = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>';
const TEXT_ATTACH_LIMIT = 64 * 1024;

/* капсула ввода: подгон высоты + форма (пилюля ↔ скруглённый прямоугольник) */
const composerWrapEl = $('composerWrap');
const singleLineH = (() => {
  const prev = inputEl.style.height;
  inputEl.style.height = 'auto';
  const h = inputEl.scrollHeight;
  inputEl.style.height = prev;
  return h;
})();
function fitInput() {
  inputEl.style.height = 'auto';
  const need = inputEl.scrollHeight;
  inputEl.style.height = Math.min(need, 200) + 'px';
  composerWrapEl.classList.toggle('multiline', need > singleLineH + 4);
}

function formatSize(bytes) {
  if (bytes < 1024) return `${bytes} Б`;
  if (bytes < 1024 * 1024) return `${Math.round(bytes / 1024)} КБ`;
  return `${(bytes / 1024 / 1024).toFixed(1)} МБ`;
}

function renderAttachments() {
  const row = document.getElementById('attachRow');
  if (!row) return;
  if (!state.attachments.length) {
    row.hidden = true;
    row.innerHTML = '';
    composerWrapEl.classList.remove('has-attaches');
    return;
  }
  row.hidden = false;
  composerWrapEl.classList.add('has-attaches');
  row.innerHTML = '';
  state.attachments.forEach((a, i) => {
    const remove = () => { state.attachments.splice(i, 1); renderAttachments(); };
    let el;
    if (a.kind === 'image' && a.thumb) {
      // миниатюра фото: квадрат 56px, крестик — круглая кнопка в правом верхнем углу
      el = document.createElement('div');
      el.className = 'attach-photo';
      el.innerHTML = `<img src="${a.thumb}" alt=""><button class="attach-x" type="button" title="Убрать">${ATTACH_X_ICON}</button>`;
    } else {
      // чип документа: иконка + имя + размер, крестик справа
      el = document.createElement('div');
      el.className = 'attach-doc';
      el.innerHTML = `<span class="ad-ico">${a.kind === 'image' ? ATTACH_IMG_ICON : ATTACH_FILE_ICON}</span><span class="ad-texts"><span class="ad-name"></span><span class="ad-size">${formatSize(a.size)}</span></span><button class="attach-x" type="button" title="Убрать">${ATTACH_X_ICON}</button>`;
    }
    el.querySelector('.attach-x').addEventListener('click', remove);
    if (el.querySelector('.ad-name')) el.querySelector('.ad-name').textContent = a.name;
    row.appendChild(el);
  });
  updateAttachFades();
}

/* градиентные края ряда вложений: fade — только там, где есть что скроллить */
function updateAttachFades() {
  const row = document.getElementById('attachRow');
  if (!row) return;
  const max = row.scrollWidth - row.clientWidth;
  row.classList.toggle('fade-l', row.scrollLeft > 4);
  row.classList.toggle('fade-r', row.scrollLeft < max - 4);
}

/* просмотр фото из истории: полноэкранная галерея со листанием (‹ ›, счётчик, Esc/стрелки) */
let lbList = [];
let lbIdx = 0;
function openLightbox(list, idx = 0) {
  const lb = ensureLightbox();
  lbList = list ?? [];
  lbIdx = Math.max(0, Math.min(idx, lbList.length - 1));
  lbShow();
  lb.classList.add('open');
}
function ensureLightbox() {
  let lb = document.getElementById('lightbox');
  if (lb) return lb;
  lb = document.createElement('div');
  lb.id = 'lightbox';
  lb.innerHTML = `
    <button class="lb-close" type="button" title="Закрыть">${ATTACH_X_ICON}</button>
    <button class="lb-nav lb-prev" type="button" title="Предыдущее"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="15 18 9 12 15 6"/></svg></button>
    <img alt="">
    <button class="lb-nav lb-next" type="button" title="Следующее"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 18 15 12 9 6"/></svg></button>
    <div class="lb-count"></div>`;
  const show = (dir) => {
    if (!lbList.length) return;
    lbIdx = (lbIdx + dir + lbList.length) % lbList.length;
    lbShow();
  };
  lb.querySelector('.lb-prev').addEventListener('click', (e) => { e.stopPropagation(); show(-1); });
  lb.querySelector('.lb-next').addEventListener('click', (e) => { e.stopPropagation(); show(1); });
  lb.querySelector('.lb-close').addEventListener('click', (e) => { e.stopPropagation(); lb.classList.remove('open'); });
  lb.addEventListener('click', () => lb.classList.remove('open'));
  document.addEventListener('keydown', (e) => {
    if (!lb.classList.contains('open')) return;
    if (e.key === 'Escape') lb.classList.remove('open');
    else if (e.key === 'ArrowLeft') show(-1);
    else if (e.key === 'ArrowRight') show(1);
  });
  document.body.appendChild(lb);
  return lb;
}
function lbShow() {
  const lb = document.getElementById('lightbox');
  lb.querySelector('img').src = lbList[lbIdx] ?? '';
  const many = lbList.length > 1;
  lb.querySelector('.lb-count').textContent = many ? `${lbIdx + 1} / ${lbList.length}` : '';
  lb.querySelector('.lb-prev').hidden = !many;
  lb.querySelector('.lb-next').hidden = !many;
}

/* картинки сжимаем до 1024px JPEG: base64 не раздувается мегабайтами,
   describe-запрос и payload /api/chat остаются компактными */
async function shrinkImage(file) {
  try {
    const bitmap = await createImageBitmap(file);
    const max = 1024;
    const scale = Math.min(1, max / Math.max(bitmap.width, bitmap.height));
    const w = Math.max(1, Math.round(bitmap.width * scale));
    const h = Math.max(1, Math.round(bitmap.height * scale));
    const canvas = document.createElement('canvas');
    canvas.width = w; canvas.height = h;
    canvas.getContext('2d').drawImage(bitmap, 0, 0, w, h);
    bitmap.close?.();
    return canvas.toDataURL('image/jpeg', 0.85);
  } catch {
    return null; // странное изображение — прочитаем как есть
  }
}

function readAttachment(file) {
  const isImage = file.type.startsWith('image/');
  return new Promise(async (resolve) => {
    if (isImage) {
      const dataUrl = await shrinkImage(file);
      if (dataUrl) return resolve({ name: file.name, size: file.size, type: file.type, kind: 'image', dataUrl, thumb: dataUrl });
      const fallback = new FileReader();
      fallback.onload = () => resolve({ name: file.name, size: file.size, type: file.type, kind: 'image', dataUrl: fallback.result, thumb: fallback.result });
      fallback.onerror = () => resolve(null);
      return fallback.readAsDataURL(file);
    }
    const reader = new FileReader();
    if (file.size <= TEXT_ATTACH_LIMIT) {
      reader.onload = () => resolve({ name: file.name, size: file.size, type: file.type || 'text/plain', kind: 'text', text: String(reader.result) });
      reader.readAsText(file);
    } else {
      resolve({ name: file.name, size: file.size, type: file.type, kind: 'big', text: null });
    }
    reader.onerror = () => resolve(null);
  });
}

async function handleFilesPicked(fileList) {
  for (const file of [...fileList]) {
    const a = await readAttachment(file);
    if (a) state.attachments.push(a);
  }
  renderAttachments();
  safeFocus(inputEl);
}

/* «+» открывает меню со скрепкой и изображением */
$('attachBtn').addEventListener('click', (e) => {
  e.stopPropagation();
  togglePillMenu('attachMenu', $('attachBtn'));
});
$('attachPickImage').addEventListener('click', () => {
  $('attachMenu').hidden = true;
  const inp = $('attachInput');
  inp.accept = 'image/*';
  inp.click();
});
$('attachPickFile').addEventListener('click', () => {
  $('attachMenu').hidden = true;
  const inp = $('attachInput');
  inp.accept = '*';
  inp.click();
});
$('attachInput').addEventListener('change', (e) => {
  if (e.target.files?.length) handleFilesPicked(e.target.files);
  e.target.value = '';
});

/* ряд вложений: вертикальное колесо мыши крутит его горизонтально */
$('attachRow').addEventListener('wheel', (e) => {
  const row = e.currentTarget;
  if (row.scrollWidth <= row.clientWidth) return;
  if (Math.abs(e.deltaY) <= Math.abs(e.deltaX)) return; // боковой жест тачпада браузер крутит сам
  e.preventDefault();
  row.scrollLeft += e.deltaY;
}, { passive: false });
$('attachRow').addEventListener('scroll', updateAttachFades, { passive: true });

/* перетаскивание файлов на капсулу ввода */
const composerWrap = $('composerWrap');['dragenter', 'dragover'].forEach((ev) => {
  composerWrap.addEventListener(ev, (e) => {
    e.preventDefault();
    composerWrap.classList.add('drag');
  });
});
['dragleave', 'drop'].forEach((ev) => {
  composerWrap.addEventListener(ev, (e) => {
    e.preventDefault();
    composerWrap.classList.remove('drag');
  });
});
composerWrap.addEventListener('drop', (e) => {
  if (e.dataTransfer?.files?.length) handleFilesPicked(e.dataTransfer.files);
});

/* вставка картинки из буфера (Ctrl+V) */
inputEl.addEventListener('paste', (e) => {
  const files = [...(e.clipboardData?.files ?? [])];
  if (files.length) {
    e.preventDefault();
    handleFilesPicked(files);
  }
});

document.addEventListener('click', (e) => {
  const menu = $('attachMenu');
  if (!menu.hidden && !menu.contains(e.target) && e.target !== $('attachBtn')) menu.hidden = true;
});

async function send() {
  const rawText = inputEl.value.trim();
  const hasAttaches = state.attachments.length > 0;
  if ((!rawText && !hasAttaches) || state.busyChat === state.activeChatId) return;
  if (!state.activated) { openLogin(); return; }
  if (!state.activeChatId || !findChat(state.activeChatId)) createChat(true);
  const chat = findChat(state.activeChatId);
  hideApproval();
  setDraft(chat.id, '');
  inputEl.value = '';
  fitInput();
  state.streamText = '';
  state.liveBotBubble = null;
  state.currentBubble = null;
  setAgentStatus('думает…');
  state.streamStartTime = null;
  const sentAt = Date.now();

  // сборка: текст + вложения
  const atts = state.attachments.splice(0);
  renderAttachments();
  const textParts = [rawText];
  for (const a of atts) {
    if (a.kind === 'text') textParts.push(`\n\n[Файл: ${a.name}]\n\`\`\`\n${a.text}\n\`\`\``);
    else if (a.kind === 'big') textParts.push(`\n\n[Файл: ${a.name} — ${formatSize(a.size)}, слишком большой для вложения]`);
    else textParts.push(`\n\n[Изображение: ${a.name}]`);
  }
  const outgoingText = textParts.join('').trim();
  const images = atts.filter((a) => a.kind === 'image').map((a) => a.dataUrl);
  const attachmentsMeta = atts.map((a) => ({ name: a.name, kind: a.kind, size: a.size, thumb: a.kind === 'image' ? a.thumb : null }));

  chat.messages.push({ role: 'user', text: rawText, attachments: attachmentsMeta, ts: sentAt });
  chat.title = chat.title === 'Новый чат' ? (rawText || atts[0]?.name || 'Вложения').slice(0, 40) : chat.title;
  chat.updatedAt = Date.now();
  persistChat(chat);
  appendMsg('user', rawText, { ts: sentAt, idx: chat.messages.length - 1, editable: true, attachments: attachmentsMeta });
  renderHistory();
  setBusy(chat.id, true);
  try {
    const res = await fetch('/api/chat', {
      method: 'POST',
      headers: { 'content-type': 'application/json' },
      body: JSON.stringify({ chatId: chat.id, text: outgoingText, images, history: chat.messages.slice(-30, -1) }),
    });
    if (!res.ok) {
      const json = await res.json().catch(() => ({}));
      addErrBanner(json.error ?? `ошибка ${res.status}`);
      chat.messages.pop();
      if (chat.messages.length) persistChat(chat); else makeEphemeralIfEmpty(chat);
      setBusy(null, false);
    }
  } catch (err) {
    addErrBanner(`Не удалось отправить: ${err.message}`);
    chat.messages.pop();
    if (chat.messages.length) persistChat(chat); else makeEphemeralIfEmpty(chat);
    setBusy(null, false);
  }
}

sendBtn.addEventListener('click', send);
inputEl.addEventListener('keydown', (e) => {
  if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); send(); }
});
inputEl.addEventListener('input', () => {
  fitInput();
  if (state.activeChatId) setDraft(state.activeChatId, inputEl.value);
});

$('newChatBtn').addEventListener('click', () => {
  if (!state.activated) { openLogin(); return; }
  const cur = findChat(state.activeChatId);
  // уже находимся в пустом чате — просто перебрасываем приветствие
  if (cur && !cur.messages.length) {
    renderChat();
  } else {
    createChat();
  }
  autoCloseSidebar();
  safeFocus(inputEl);
});

/* сайдбар */
$('sidebarToggle').addEventListener('click', () => {
  document.querySelector('.sidebar').classList.toggle('collapsed');
  $('sidebarToggle').classList.toggle('active');
});
if (window.innerWidth <= 900) {
  document.querySelector('.sidebar').classList.add('collapsed');
  $('sidebarToggle').classList.add('active');
}

/* ─── кастомный скроллбар чата: капсула на дорожке + кнопка «Вниз» ─── */
function initChatScrollbar() {
  const scrollWrap = document.querySelector('.chat-scroll-wrap');
  const scrollThumb = $('scrollThumb');
  const scrollToBottomBtn = $('scrollToBottom');
  if (!scrollWrap || !scrollThumb) return;

  function updateScrollbar() {
    const canScroll = messagesEl.scrollHeight > messagesEl.clientHeight;
    scrollThumb.style.display = canScroll ? '' : 'none';
    if (!canScroll) return;
    const ratio = messagesEl.clientHeight / messagesEl.scrollHeight;
    const padTop = 40;
    const padBot = 40;
    const trackH = scrollWrap.clientHeight - padTop - padBot;
    const thumbH = Math.max(40, trackH * ratio);
    const maxTop = trackH - thumbH;
    const scrollRatio = messagesEl.scrollTop / (messagesEl.scrollHeight - messagesEl.clientHeight);
    const top = padTop + scrollRatio * maxTop;
    scrollThumb.style.height = `${thumbH}px`;
    scrollThumb.style.top = `${top}px`;
  }

  messagesEl.addEventListener('scroll', updateScrollbar, { passive: true });
  if (window.ResizeObserver) new ResizeObserver(updateScrollbar).observe(messagesEl);
  new MutationObserver(updateScrollbar).observe(messagesEl, { childList: true, subtree: true, characterData: true });
  window.addEventListener('resize', updateScrollbar);

  // перетаскивание мышью
  let dragging = false;
  let dragStartY = 0;
  let dragStartScroll = 0;
  scrollThumb.addEventListener('mousedown', (e) => {
    dragging = true;
    dragStartY = e.clientY;
    dragStartScroll = messagesEl.scrollTop;
    scrollThumb.classList.add('dragging');
    e.preventDefault();
  });
  document.addEventListener('mousemove', (e) => {
    if (!dragging) return;
    const trackH = scrollWrap.clientHeight - 80;
    const thumbH = scrollThumb.offsetHeight;
    const maxTop = trackH - thumbH;
    if (maxTop <= 0) return;
    const delta = e.clientY - dragStartY;
    const scrollDelta = (delta / maxTop) * (messagesEl.scrollHeight - messagesEl.clientHeight);
    messagesEl.scrollTop = dragStartScroll + scrollDelta;
  });
  document.addEventListener('mouseup', () => {
    if (!dragging) return;
    dragging = false;
    scrollThumb.classList.remove('dragging');
  });

  // перетаскивание на тач-экранах
  let touchDragging = false;
  let touchStartY = 0;
  let touchStartScroll = 0;
  scrollThumb.addEventListener('touchstart', (e) => {
    if (e.touches.length !== 1) return;
    touchDragging = true;
    touchStartY = e.touches[0].clientY;
    touchStartScroll = messagesEl.scrollTop;
    scrollThumb.classList.add('dragging');
    e.preventDefault();
  }, { passive: false });
  document.addEventListener('touchmove', (e) => {
    if (!touchDragging) return;
    const trackH = scrollWrap.clientHeight - 80;
    const thumbH = scrollThumb.offsetHeight;
    const maxTop = trackH - thumbH;
    if (maxTop <= 0) return;
    const delta = e.touches[0].clientY - touchStartY;
    const scrollDelta = (delta / maxTop) * (messagesEl.scrollHeight - messagesEl.clientHeight);
    messagesEl.scrollTop = touchStartScroll + scrollDelta;
    e.preventDefault();
  }, { passive: false });
  document.addEventListener('touchend', () => {
    if (!touchDragging) return;
    touchDragging = false;
    scrollThumb.classList.remove('dragging');
  });

  // клик по дорожке — плавный прыжок к месту
  scrollWrap.addEventListener('click', (e) => {
    const track = document.querySelector('.scrollbar-track');
    if (e.target !== scrollThumb && e.target !== track) return;
    const padTop = 40;
    const trackH = scrollWrap.clientHeight - 80;
    const clickY = e.clientY - scrollWrap.getBoundingClientRect().top - padTop;
    const targetRatio = Math.max(0, Math.min(1, clickY / trackH));
    messagesEl.scrollTo({
      top: targetRatio * (messagesEl.scrollHeight - messagesEl.clientHeight),
      behavior: 'smooth',
    });
  });

  // кнопка «Вниз»
  function updateScrollButton() {
    if (messagesEl.scrollTop + messagesEl.clientHeight < messagesEl.scrollHeight - 100) {
      scrollToBottomBtn.classList.add('visible');
    } else {
      scrollToBottomBtn.classList.remove('visible');
    }
  }
  messagesEl.addEventListener('scroll', updateScrollButton, { passive: true });
  scrollToBottomBtn.addEventListener('click', () => {
    messagesEl.scrollTo({ top: messagesEl.scrollHeight, behavior: 'smooth' });
  });

  updateScrollbar();
  updateScrollButton();
}

/* ─── SSE ─── */
function connectStream() {
  const es = new EventSource('/api/stream');
  es.onmessage = (e) => handleEvent(JSON.parse(e.data));
}

function handleEvent(evt) {
  const chat = findChat(evt.chatId);
  const isActive = evt.chatId === state.activeChatId;

  switch (evt.type) {
    case 'user':
      if (chat && evt.resend) chat.ignoreEvents = false;
      break;
    case 'status':
      if (chat?.ignoreEvents) break;
      if (chat) setBusy(evt.chatId, true);
      draftFor(evt.chatId).statusText = `думает (шаг ${(evt.step ?? 0) + 1})`;
      runSetNow(evt.chatId, `думает (шаг ${(evt.step ?? 0) + 1})…`);
      if (isActive) setAgentStatus(`думает (шаг ${(evt.step ?? 0) + 1})`);
      break;
    case 'reasoning':
      if (chat?.ignoreEvents) break;
      runAddReasoning(evt.chatId, evt.chunk);
      break;
    case 'delta':
      if (chat) { runCloseThought(evt.chatId); draftFor(evt.chatId).text += evt.chunk; }
      if (!isActive) break;
      runEnsure(evt.chatId);
      state.streamText += evt.chunk;
      // пузырь создаём только когда есть видимый текст — иначе пустой «квадратик»
      if (!state.currentBubble && state.streamText.trim()) {
        state.streamStartTime = Date.now();
        appendMsg('bot', '', { stream: true, ts: state.streamStartTime });
      }
      if (state.currentBubble) startTypewriter(state.currentBubble);
      runSetNow(evt.chatId, 'пишет ответ…');
      setAgentStatus('пишет ответ…');
      break;
    case 'message_done':
      if (state.currentBubble && !state.streamText.trim()) {
        state.currentBubble.closest('.msg')?.remove();
        state.currentBubble = null;
      }
      if (state.tw && state.tw.shown < state.streamText.length) {
        // тайпер ещё печатает — финализацию сделает он
        state.tw.finished = true;
      } else if (state.currentBubble) {
        state.currentBubble.innerHTML = renderMarkdown(state.streamText);
        finalizeRich(state.currentBubble);
        state.currentBubble = null;
      }
      setAgentStatus('обрабатывает результат…');
      runSetNow(evt.chatId, 'обрабатывает результат…');
      break;
    case 'tool_start':
      if (chat?.ignoreEvents) break;
      // текст перед инструментом замораживаем в ленту как комментарий ИИ
      if (state.activeChatId === evt.chatId) {
        if (state.streamText.trim()) runAddCommentary(evt.chatId, state.streamText);
        else runCloseThought(evt.chatId);
      } else {
        const bgDraft = draftFor(evt.chatId);
        if (bgDraft.text.trim()) runAddCommentary(evt.chatId, bgDraft.text);
        else runCloseThought(evt.chatId);
      }
      draftFor(evt.chatId).statusText = `${TOOL_VERBS[evt.name] ?? 'выполняет'}: ${toolArgsPreview(evt.name, evt.args ?? {}).slice(0, 70)}`;
      runAddTool(evt.chatId, evt.name, evt.args ?? {});
      runSetNow(evt.chatId, draftFor(evt.chatId).statusText);
      if (isActive) {
        setAgentStatus(draftFor(evt.chatId).statusText);
        stopTypewriter();
        if (state.currentBubble) {
          // текст комментария уже в ленте — пузырь убираем целиком
          state.currentBubble.closest('.msg')?.remove();
          state.currentBubble = null;
        }
        state.streamText = '';
        state.liveBotBubble = null;
        setAgentStatus(`${TOOL_VERBS[evt.name] ?? 'выполняет'}: ${toolArgsPreview(evt.name, evt.args ?? {}).slice(0, 70)}`);
      }
      break;
    case 'tool_end':
      if (chat?.ignoreEvents) break;
      runToolDone(evt.chatId, evt.result, evt.approved);
      if (!isActive) break;
      scrollDown();
      break;
    case 'approval':
      if (isActive) { showApproval(evt); setAgentStatus('ждёт вашего подтверждения…'); }
      break;
    case 'done':
      if (chat?.ignoreEvents) break;
      if (evt.aborted) {
        delete state.drafts[evt.chatId];
        messagesEl.querySelectorAll('.run-block').forEach((b) => b.remove());
        setBusy(evt.chatId, false);
        setAgentStatus('');
        break;
      }
      let workedMs = 0;
      let runSteps = null;
      if (chat) {
        const draft = state.drafts[evt.chatId];
        workedMs = draft ? Math.max(1000, Date.now() - draft.t0) : 0;
        runSteps = draft ? draft.steps
          .filter((s) => s.kind !== 'thought' || (s.text ?? '').trim())
          .map((s) => s.kind === 'tool'
            ? { kind: 'tool', name: s.name, args: s.args, status: s.status, meta: toolStepInfo(s.name, s.args).meta ?? '', dur: s.dur ?? 0 }
            : { kind: s.kind, text: s.text ?? '', dur: s.dur ?? 0 }) : [];
        chat.messages.push({ role: 'assistant', text: evt.text, steps: runSteps, workedMs, ts: state.streamStartTime ?? Date.now() });
        persistChat(chat);
      }
      delete state.drafts[evt.chatId];
      runFinalize(evt.chatId, workedMs);
      // тайпер ещё допечатывает — не даём финалу затереть плавное появление
      const twBehind = state.tw && state.tw.shown < state.streamText.length;
      if (isActive && !twBehind) {
        if (state.currentBubble) {
          state.currentBubble.innerHTML = renderMarkdown(evt.text);
          finalizeRich(state.currentBubble);
          state.currentBubble = null;
        } else if (state.liveBotBubble) {
          state.liveBotBubble.innerHTML = renderMarkdown(evt.text);
          finalizeRich(state.liveBotBubble);
          state.liveBotBubble.closest('.msg').classList.add('arrive');
        } else {
          appendMsg('bot', evt.text, { ts: state.streamStartTime ?? Date.now(), idx: chat ? chat.messages.length - 1 : undefined });
        }
        document.querySelectorAll('#messages .act-edit').forEach((b) => b.remove());
      }
      if (state.liveBotBubble) {
        const bubbleMsg = state.liveBotBubble.closest('.msg');
        if (bubbleMsg && chat) bubbleMsg.dataset.idx = String(chat.messages.length - 1);
      }
      if (twBehind) {
        state.tw.finished = true; // допечатает остаток и подчистит сам
      } else {
        stopTypewriter();
        state.streamText = '';
        state.streamStartTime = null;
      }
      state.liveBotBubble = null;
      setBusy(evt.chatId, false);
      setAgentStatus('');
      refreshBalance();
      state.levelRetried = false; // запрос завершился — следующий снова сможет автопереключиться
      break;
    case 'error': {
      delete state.drafts[evt.chatId];
      // пустой пузырь ответа (ошибка пришла до текста) не оставляем висеть над лентой
      const dropEmpty = (bubble) => {
        if (!bubble) return;
        bubble.innerHTML = renderMarkdown(state.streamText ?? '');
        const msgEl = bubble.closest('.msg');
        if (!bubble.textContent.trim() && !msgEl?.querySelector('.run-block')) msgEl?.remove();
      };
      dropEmpty(state.currentBubble);
      dropEmpty(state.liveBotBubble);
      if (isActive) {
        if (/level_unavailable|Уровень временно недоступен/i.test(String(evt.error))) {
          if (!state.levelRetried) {
            // уровень недоступен у провайдера: говорим человеку и уводим запрос на доступный
            state.levelRetried = true; // защита от зацикливания, если недоступен и «Эконом»
            const failedLevel = levelForModel(localStorage.getItem('naomi_model') ?? 'glm-4.5-air').name;
            // на «Экономе» «переключаю на Эконом» звучало бы глупо — там свой текст
            addNotice(failedLevel === 'Эконом'
              ? 'Бесплатные модели у провайдера сейчас перегружены. Повторяю вашу попытку… Если не получится — попробуйте, пожалуйста, чуть позже.'
              : `Уровень «${failedLevel}» временно недоступен у провайдера — повторите попытку позже. Отправляю ваш запрос на доступный уровень «Эконом»…`, { type: 'warn' });
            localStorage.setItem('naomi_model', 'glm-4.5-air');
            updateModelPill();
            const lastUser = findChat(evt.chatId)?.messages.filter((m) => m.role === 'user').pop();
            if (lastUser) setTimeout(async () => {
              const chat = findChat(evt.chatId);
              if (!chat || state.busyChat) return;
              await fetch('/api/config', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ model: 'glm-4.5-air', resetSessions: false }) }).catch(() => {});
              // resend: true — сервер заменит хвост своей истории вместо дописывания дубля;
              // без renderChat, чтобы уведомление не стиралось через секунду после показа
              await fetch('/api/chat', {
                method: 'POST',
                headers: { 'content-type': 'application/json' },
                body: JSON.stringify({ chatId: evt.chatId, text: lastUser.text, history: chat.messages.slice(-30, -1), resend: true }),
              }).catch(() => {});
            }, 1200);
          } else {
            state.levelRetried = false;
            addNotice('Сейчас недоступны все уровни — у провайдера временные проблемы. Повторите попытку чуть позже.', { type: 'warn' });
          }
        } else {
          addErrBanner(evt.error ?? 'неизвестная ошибка');
        }
      }
      stopTypewriter();
      state.currentBubble = null;
      state.liveBotBubble = null;
      setBusy(evt.chatId, false);
      setAgentStatus('');
      break;
    }
    case 'config_updated':
      break;
  }
}

/* ─── старт ─── */
async function init() {
  try {
    await initInner();
  } catch (err) {
    addErrBanner(`Ошибка инициализации: ${err.message}`);
    console.error(err);
  }
}

async function initInner() {
  const st = await (await fetch('/api/state')).json().catch(() => null);
  if (!st) return;
  state.policy = st.config.policy ?? 'safe';
  updatePolicyRow();
  const cfgModel = normalizeModel(st.config.model || localStorage.getItem('naomi_model') || 'glm-4.5-air');
  localStorage.setItem('naomi_model', cfgModel);
  // сохранённый glm-4.7-flash (или пустой config) обновляем и на сервере,
  // чтобы агент реально ходил в glm-4.5-air, а не в замедленную модель
  if (cfgModel !== st.config.model) {
    fetch('/api/config', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ model: cfgModel, resetSessions: false }) }).catch(() => {});
  }
  updateModelPill();
  const badge = document.getElementById('modelBadge');
  if (badge) badge.textContent = levelForModel(cfgModel).name;
  const regCfg = await fetch('/api/reg-config').then((r) => r.json()).catch(() => null);
  const hashKey = (location.hash.match(/naomi_[a-z0-9]+/i) ?? [])[0];
  if (hashKey && !st.config.apiKey) {
    history.replaceState(null, '', location.pathname);
    await login(hashKey);
    return;
  }
  state.activated = !!st.config.apiKey;
  applyIdentity(st.config.apiKey ? 'ключ активен' : 'ключ не активирован');
  if (st.config.apiKey) {
    $('btnTopup').hidden = false;
    // пустые чаты в истории не храним вообще
    if (store.chats.some((c) => !c.messages.length)) {
      store.chats = store.chats.filter((c) => c.messages.length);
      const d = store.drafts;
      const ids = new Set(store.chats.map((c) => c.id));
      for (const k of Object.keys(d)) if (!ids.has(k)) delete d[k];
      store.drafts = d;
    }
    if (store.chats.length) {
      const savedActive = localStorage.getItem('naomi_active_chat');
      if (savedActive && store.chats.some((c) => c.id === savedActive)) {
        // вернулись в тот же сохранённый чат
        state.activeChatId = savedActive;
      } else if (savedActive) {
        // в прошлый раз были в новом (пустом) чате — после F5 остаёмся в новом,
        // а не перебрасывает в первый из истории
        createChat(true);
      } else {
        state.activeChatId = store.chats[0].id;
      }
      renderHistory();
      renderChat();
    } else {
      createChat(true);
      renderChat();
    }
    refreshBalance();
    if (Array.isArray(st.busyChats) && st.busyChats.includes(state.activeChatId)) {
      setBusy(state.activeChatId, true);
      setAgentStatus('выполняет задачу…');
    }
    const pending = (st.pendingApprovals ?? []).find((a) => a.chatId === state.activeChatId);
    if (pending) {
      showApproval(pending);
      setAgentStatus('ждёт вашего подтверждения…');
    }
  } else {
    showHero();
    refreshBalance();
  }
}

init();
initChatScrollbar();
connectStream();
