// ── Context editor ────────────────────────────────────────────────
let _ctxAgentFilter = 'transcript';

let _ctxCurrentOffset = 0;
let _ctxHasMore = false;
let _ctxTotalCount = 0;

function cmdShowContext(agentName) {
  if (!conversationId) { addMsg('system', t('noConv')); return; }
  if (agentName !== undefined) _ctxAgentFilter = agentName;
  _ctxCurrentOffset = 0;
  const body = { limit: 50, offset: 0 };
  if (_ctxAgentFilter) body.agent_name = _ctxAgentFilter;
  action$('get_context', body).subscribe(data => {
    if (data.error) { addMsg('error', data.error); return; }
    data._agent_filter = _ctxAgentFilter;
    _ctxHasMore = data.has_more || false;
    _ctxTotalCount = data.message_count || 0;
    _ctxCurrentOffset = (data.context || []).length;
    showContextOverlay(data);
  });
}

let _ctxFullData = null;
let _ctxVisibleById = new Map();

function _ctxScopedAgentName() {
  return (_ctxAgentFilter && _ctxAgentFilter !== 'transcript') ? _ctxAgentFilter : '';
}

function _ctxIsReadonly() {
  return !!(_ctxAgentFilter && (
    _ctxAgentFilter.startsWith('cc_session:')
    || _ctxAgentFilter.startsWith('codex_session:')
    || _ctxAgentFilter.startsWith('gemini_session:')));
}

function _ctxEscape(value) {
  return String(value == null ? '' : value)
    .replace(/&/g, '&amp;')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;');
}

function _ctxToolCallsText(m) {
  const calls = Array.isArray(m.tool_calls) ? m.tool_calls : [];
  if (!calls.length) return '';
  return calls.map(tc => {
    let name = tc.name || tc.tool || (tc.function && tc.function.name) || 'tool';
    let args = tc.arguments !== undefined ? tc.arguments : (tc.args !== undefined ? tc.args : (tc.function && tc.function.arguments));
    if ((name === 'use_tool' || name === 'mcp_pawflow_use_tool' || name === 'mcp_pawflow.use_tool'
        || name === 'mcp__pawflow__use_tool' || name === 'mcp__pawflow__.use_tool'
        || name === 'pawflow.use_tool' || name === 'pawflow/use_tool') && args) {
      const parsed = typeof args === 'string' ? (() => { try { return JSON.parse(args); } catch (_) { return null; } })() : args;
      if (parsed && parsed.tool_name) {
        name = parsed.tool_name;
        args = parsed.arguments || parsed.parameters || {};
      } else if (parsed && parsed.parameters && parsed.parameters.tool_name) {
        name = parsed.parameters.tool_name;
        args = parsed.parameters.arguments || parsed.parameters.parameters || {};
      }
    }
    let argsText = '';
    if (args !== undefined) {
      argsText = typeof args === 'string' ? args : JSON.stringify(args);
    }
    return name + '(' + argsText + ')';
  }).join('\n');
}

function _ctxStripToolOutputEnvelope(content) {
  let text = String(content || '');
  if (!text.startsWith('<tool_output tool="')) return text;
  const firstNl = text.indexOf('\n');
  if (firstNl >= 0) text = text.slice(firstNl + 1);
  const close = text.lastIndexOf('</tool_output>');
  if (close >= 0) text = text.slice(0, close).replace(/\n$/, '');
  return text;
}

function _ctxDisplayContent(m) {
  let content = m.content || '';
  if (!String(content).trim() && m.has_tool_calls) {
    content = _ctxToolCallsText(m);
  }
  content = _ctxStripToolOutputEnvelope(content);
  return _ctxEscape(content);
}

function _ctxScopedMutation(body) {
  const scoped = Object.assign({}, body);
  const agent = _ctxScopedAgentName();
  if (agent) scoped.agent_name = agent;
  return scoped;
}

function ctxLoadFull() {
  return Promise.resolve({ error: 'Full context loading is disabled; load older pages instead.' });
}

function ctxRefresh() {
  _ctxFullData = null;
  _ctxCurrentOffset = 0;
  const body = { limit: 50, offset: 0 };
  if (_ctxAgentFilter) body.agent_name = _ctxAgentFilter;
  action$('get_context', body).subscribe(data => {
    if (data.error) { addMsg('error', data.error); return; }
    _ctxHasMore = data.has_more || false;
    _ctxTotalCount = data.message_count || 0;
    _ctxCurrentOffset = (data.context || []).length;
    showContextOverlay(data);
  });
}

async function ctxEditMessage(msgId) {
  if (_ctxIsReadonly()) { addMsg('error', t('contextReadonly')); return; }
  let msg = _ctxVisibleById.get(msgId);
  if (!msg) {
    addMsg('error', t('contextMessageNotFoundRefresh'));
    return;
  }
  if (!msg) { addMsg('error', t('contextMessageNotFoundRefresh')); return; }
  // Scope the row lookup to the context overlay — the chat timeline
  // also uses data-msgid on its message rows, and querySelector would
  // otherwise pick the chat row and render the edit form there.
  const overlay = document.getElementById('contextOverlay');
  if (!overlay) return;
  const row = overlay.querySelector('[data-msgid="' + msgId + '"]');
  if (!row) return;
  const content = typeof msg.content === 'string' ? msg.content : JSON.stringify(msg.content);
  row.innerHTML = '<div style="padding:8px">'
    + '<div style="margin-bottom:6px"><label style="color:#808090;font-size:11px;margin-right:6px">' + t('contextRole') + ':</label>'
    + '<select id="ctx-edit-role-' + msgId + '" style="background:#0d1117;color:#e0e0e0;border:1px solid #333;border-radius:4px;padding:2px 6px;font-size:12px">'
    + '<option value="system"' + (msg.role==='system'?' selected':'') + '>system</option>'
    + '<option value="user"' + (msg.role==='user'?' selected':'') + '>user</option>'
    + '<option value="assistant"' + (msg.role==='assistant'?' selected':'') + '>assistant</option>'
    + '<option value="tool"' + (msg.role==='tool'?' selected':'') + '>tool</option>'
    + '</select></div>'
    + '<textarea id="ctx-edit-ta-' + msgId + '" style="width:100%;min-height:120px;background:#0d1117;color:#c0c0d0;border:1px solid #333;border-radius:6px;padding:8px;font-size:12px;font-family:monospace;resize:vertical">' + content.replace(/</g,'&lt;').replace(/>/g,'&gt;') + '</textarea>'
    + '<div style="display:flex;gap:6px;margin-top:6px">'
    + '<button onclick="ctxSaveEdit(\'' + msgId + '\')" style="background:#2563eb;color:#fff;border:none;border-radius:6px;padding:4px 12px;cursor:pointer;font-size:12px">' + t('contextSave') + '</button>'
    + '<button onclick="ctxRefresh()" style="background:#333;color:#ccc;border:none;border-radius:6px;padding:4px 12px;cursor:pointer;font-size:12px">' + t('contextCancel') + '</button>'
    + '</div></div>';
}

let _ctxDirty = false;
function _ctxMutate(body, successMsg) {
  const _action = body.action;
  const _params = Object.fromEntries(Object.entries(body).filter(([k]) => k !== 'action'));
  return new Promise(resolve => {
    action$(_action, _params).subscribe(data => {
      if (data.error) { addMsg('error', data.error); resolve(false); return; }
      if (successMsg) addMsg('system', typeof successMsg === 'function' ? successMsg(data) : successMsg);
      _ctxFullData = null;
      _ctxDirty = true;
      ctxRefresh();
      resolve(true);
    });
  });
}
function ctxLoadMore() {
  const btn = document.getElementById('ctxLoadMore');
  if (btn) btn.innerHTML = '<span style="color:#888">Loading...</span>';
  const body = { limit: 50, offset: _ctxCurrentOffset };
  if (_ctxAgentFilter) body.agent_name = _ctxAgentFilter;
  action$('get_context', body).subscribe(data => {
    if (data.error) return;
    _ctxHasMore = data.has_more || false;
    _ctxCurrentOffset += (data.context || []).length;
    const list = document.getElementById('ctx-msg-list');
    if (!list) return;
    // Remove old load-more button
    if (btn) btn.remove();
    const roleColors = {system:'#6c6c8a',user:'#4fc3f7',assistant:'#4ecdc4',tool:'#f4a261'};
    // Append older messages at the BOTTOM (list is newest-first, older = further down)
    // Server returns chronological (oldest first) — reverse for newest-first display
    const olderMsgs = data.context || [];
    const reversed = [...olderMsgs].reverse();
    const readonly = _ctxIsReadonly();
    reversed.forEach((m) => {
      const mid = m.msg_id || m.trace_id || '';
      if (!mid) return;
      _ctxVisibleById.set(mid, m);
      const color = roleColors[m.role] || '#808090';
      const safeMid = jsStringArg(mid);
      const badge = '<span style="display:inline-block;background:' + color + '22;color:' + color + ';padding:1px 6px;border-radius:6px;font-size:11px;font-weight:600;margin-right:6px">' + escapeHtml(m.role || '') + '</span>';
      const tcTag = m.has_tool_calls ? '<span style="color:#f4a261;font-size:10px;margin-left:4px">[tool_calls]</span>' : '';
      const content = _ctxDisplayContent(m);
      const editBtn = readonly ? '' : '<button onclick="event.stopPropagation();ctxEditMessage(' + safeMid + ')" style="background:none;border:none;color:#4fc3f7;cursor:pointer;font-size:13px;padding:0 3px" title="' + escapeAttr(t('edit')) + '">&#9998;</button>';
      const delBtn = readonly ? '' : '<button onclick="event.stopPropagation();ctxDeleteMessage(' + safeMid + ')" style="background:none;border:none;color:#e74c3c;cursor:pointer;font-size:13px;padding:0 3px" title="' + escapeAttr(t('delete')) + '">&#128465;</button>';
      const row = document.createElement('div');
      row.dataset.msgid = mid;
      row.style.cssText = 'padding:6px 8px;border-bottom:1px solid #222;cursor:pointer';
      row.title = t('contextExpandTitle');
      row.onmousedown = function(event) { if (event.shiftKey || event.ctrlKey) event.preventDefault(); };
      row.onclick = function(event) {
        if (event.ctrlKey || event.shiftKey) {
          event.preventDefault();
          ctxToggleSelect(row, event);
          return;
        }
        if (event.target && event.target.closest('.ctx-preview,.ctx-full')) return;
        const full = row.querySelector('.ctx-full');
        if (full) full.style.display = full.style.display === 'block' ? 'none' : 'block';
      };
      row.innerHTML = '<div style="display:flex;align-items:center">' + badge + tcTag + '<span style="margin-left:auto">' + editBtn + delBtn + '</span></div>'
        + '<div class="ctx-preview" style="color:#c0c0d0;font-size:12px;margin-top:4px;white-space:pre-wrap;word-break:break-word;max-height:96px;overflow-y:auto;border:1px solid #1d2636;border-radius:6px;padding:6px;background:#0a0f18">' + content + '</div>'
        + '<div class="ctx-full" style="display:none;color:#a0a0c0;font-size:12px;margin-top:4px;white-space:pre-wrap;word-break:break-word;max-height:min(60vh,640px);overflow-y:auto;border:1px solid #222;border-radius:6px;padding:8px;background:#090d14">' + content + '</div>';
      list.appendChild(row);
    });
    // New load-more button
    if (_ctxHasMore) {
      const more = document.createElement('div');
      more.id = 'ctxLoadMore';
      more.style.cssText = 'text-align:center;padding:12px';
      more.innerHTML = '<button onclick="ctxLoadMore()" style="background:#1e3a5f;color:#4fc3f7;border:none;border-radius:6px;padding:6px 16px;cursor:pointer;font-size:12px">'
        + escapeHtml(t('contextLoadOlder', { shown: _ctxCurrentOffset, total: _ctxTotalCount })) + '</button>';
      list.appendChild(more);
    }
  });
}

function ctxClose() {
  const overlay = document.getElementById('contextOverlay');
  if (overlay) overlay.remove();
  if (_ctxDirty) { _ctxDirty = false; resumeConv(conversationId, true); }
}

async function ctxSaveEdit(msgId) {
  const ta = document.getElementById('ctx-edit-ta-' + msgId);
  const roleEl = document.getElementById('ctx-edit-role-' + msgId);
  if (!ta) return;
  const body = {
    action: _ctxAgentFilter === 'transcript' ? 'edit_message' : 'edit_context',
    msg_id: msgId,
    content: ta.value,
    role: roleEl ? roleEl.value : undefined
  };
  _ctxMutate(
    _ctxAgentFilter === 'transcript' ? body : _ctxScopedMutation(body),
    (d) => t('contextSaved', {n: d.message_count, tokens: d.token_estimate}));
}

async function ctxDeleteMessage(msgId) {
  if (_ctxIsReadonly()) { addMsg('error', t('contextReadonly')); return; }
  if (!confirm(t('contextDeleteConfirm'))) return;
  if (_ctxAgentFilter === 'transcript') {
    await _ctxMutate({action: 'delete_message', msg_id: msgId});
  } else {
    await _ctxMutate(
      {action: 'delete_context_message', msg_id: msgId, agent_name: _ctxAgentFilter},
      (d) => t('contextSaved', {n: d.message_count, tokens: d.token_estimate}));
  }
}

async function ctxAddMessage() {
  if (_ctxIsReadonly()) { addMsg('error', t('contextReadonly')); return; }
  const container = document.getElementById('ctx-add-form');
  if (container) { container.remove(); return; }
  const list = document.getElementById('ctx-msg-list');
  if (!list) return;
  const form = document.createElement('div');
  form.id = 'ctx-add-form';
  form.style.cssText = 'padding:10px;border-top:1px solid #333';
  form.innerHTML = '<div style="margin-bottom:6px"><label style="color:#808090;font-size:11px;margin-right:6px">' + t('contextRole') + ':</label>'
    + '<select id="ctx-add-role" style="background:#0d1117;color:#e0e0e0;border:1px solid #333;border-radius:4px;padding:2px 6px;font-size:12px">'
    + '<option value="system">system</option><option value="user" selected>user</option><option value="assistant">assistant</option></select></div>'
    + '<textarea id="ctx-add-content" style="width:100%;min-height:80px;background:#0d1117;color:#c0c0d0;border:1px solid #333;border-radius:6px;padding:8px;font-size:12px;font-family:monospace;resize:vertical" placeholder="' + t('contextContent') + '..."></textarea>'
    + '<div style="display:flex;gap:6px;margin-top:6px">'
    + '<button onclick="ctxSaveNewMessage()" style="background:#2563eb;color:#fff;border:none;border-radius:6px;padding:4px 12px;cursor:pointer;font-size:12px">' + t('contextSave') + '</button>'
    + '<button onclick="document.getElementById(\'ctx-add-form\').remove()" style="background:#333;color:#ccc;border:none;border-radius:6px;padding:4px 12px;cursor:pointer;font-size:12px">' + t('contextCancel') + '</button>'
    + '</div>';
  list.parentNode.appendChild(form);
}

async function ctxSaveNewMessage() {
  if (_ctxIsReadonly()) { addMsg('error', t('contextReadonly')); return; }
  if (_ctxAgentFilter === 'transcript') {
    addMsg('error', t('contextAddSharedRequired'));
    return;
  }
  const role = document.getElementById('ctx-add-role')?.value || 'user';
  const content = document.getElementById('ctx-add-content')?.value || '';
  if (!content.trim()) return;
  _ctxMutate(
    _ctxScopedMutation({action: 'add_context_message', role, content}),
    (d) => t('contextSaved', {n: d.message_count, tokens: d.token_estimate}));
}

async function ctxReplaceAll() {
  if (_ctxIsReadonly()) { addMsg('error', t('contextReadonly')); return; }
  if (_ctxAgentFilter === 'transcript') {
    addMsg('error', t('contextTranscriptReplaceBlocked'));
    return;
  }
  addMsg('error', 'Full context replacement is disabled; edit or delete loaded rows only.');
}

async function ctxSaveReplaceAll() {
  addMsg('error', 'Full context replacement is disabled; edit or delete loaded rows only.');
}

function _buildCtxAgentDropdown(data) {
  const agents = data.agent_contexts || {};
  const names = Object.keys(agents).filter(n => n !== '*').sort();
  if (_ctxAgentFilter && _ctxAgentFilter !== 'transcript' && !names.includes(_ctxAgentFilter)) {
    names.push(_ctxAgentFilter);
    names.sort();
  }
  const sharedStatus = agents['*'] || 'messages';
  const sharedLabel = t('contextShared') + (sharedStatus === 'diverged' ? ' \u2733' : '');
  let html = '<select id="ctxAgentFilter" onchange="ctxAgentChanged()" style="background:#1e1e3a;color:#c0c0d0;border:1px solid #444;border-radius:6px;padding:3px 8px;font-size:12px">';
  html += '<option value="transcript"' + (_ctxAgentFilter === 'transcript' ? ' selected' : '') + '>' + escapeHtml(t('contextTranscript')) + '</option>';
  html += '<option value=""' + (!_ctxAgentFilter ? ' selected' : '') + '>' + escapeHtml(sharedLabel) + '</option>';
  for (const n of names) {
    const status = agents[n] || 'messages';
    let label;
    if (n.startsWith('cc_session:')) {
      label = n.replace('cc_session:', '') + ' (' + t('contextCcSession') + ' \uD83D\uDC33)';
    } else if (n.startsWith('codex_session:')) {
      label = n.replace('codex_session:', '') + ' (' + t('contextCodexSession') + ')';
    } else if (n.startsWith('gemini_session:')) {
      label = n.replace('gemini_session:', '') + ' (' + t('contextGeminiSession') + ')';
    } else {
      label = n + (status === 'diverged' ? ' \u2733' : '');
    }
    html += '<option value="' + escapeAttr(n) + '"' + (_ctxAgentFilter === n ? ' selected' : '') + '>' + escapeHtml(label) + '</option>';
  }
  html += '</select>';
  return html;
}
async function ctxAgentChanged() {
  _ctxAgentFilter = document.getElementById('ctxAgentFilter').value;
  _ctxFullData = null;
  await cmdShowContext(_ctxAgentFilter);
}
async function ctxDeleteContext() {
  if (!_ctxAgentFilter) return;
  const msg = _ctxIsReadonly()
    ? t('contextInvalidateRuntimeConfirm', { name: _ctxAgentFilter })
    : t('contextDeleteContextConfirm', { name: _ctxAgentFilter });
  if (!confirm(msg)) return;
  const name = _ctxAgentFilter;
  _ctxAgentFilter = 'transcript';  // switch to transcript after delete
  _ctxMutate({action: 'delete_agent_context', agent_name: name}, t('contextDeleted', { name: name }));
}

const _ctxSelected = new Set();
function ctxToggleSelect(row, event) {
  const mid = row.dataset.msgid;
  if (!mid) return;
  // Prevent the browser's native text selection that mousedown already
  // started — the inline onclick handler fires too late for that, but
  // clearing the live Selection here undoes the highlight.
  try { window.getSelection().removeAllRanges(); } catch (e) {}
  if (event.shiftKey && _ctxSelected.size > 0) {
    const overlay = document.getElementById('contextOverlay');
    const rows = Array.from((overlay || document).querySelectorAll('[data-msgid]'));
    const lastSel = rows.find(r => r.classList.contains('ctx-selected'));
    const lastIdx = lastSel ? rows.indexOf(lastSel) : -1;
    const curIdx = rows.indexOf(row);
    if (lastIdx >= 0 && curIdx >= 0) {
      const [from, to] = lastIdx < curIdx ? [lastIdx, curIdx] : [curIdx, lastIdx];
      for (let i = from; i <= to; i++) {
        rows[i].classList.add('ctx-selected');
        rows[i].style.outline = '2px solid #6c5ce7';
        _ctxSelected.add(rows[i].dataset.msgid);
      }
    }
  } else if (event.ctrlKey) {
    if (_ctxSelected.has(mid)) {
      _ctxSelected.delete(mid);
      row.classList.remove('ctx-selected');
      row.style.outline = '';
    } else {
      _ctxSelected.add(mid);
      row.classList.add('ctx-selected');
      row.style.outline = '2px solid #6c5ce7';
    }
  } else {
    const overlay = document.getElementById('contextOverlay');
    (overlay || document).querySelectorAll('.ctx-selected').forEach(r => { r.classList.remove('ctx-selected'); r.style.outline = ''; });
    _ctxSelected.clear();
    _ctxSelected.add(mid);
    row.classList.add('ctx-selected');
    row.style.outline = '2px solid #6c5ce7';
  }
  const bar = document.getElementById('ctxSelectBar');
  if (bar) bar.style.display = _ctxSelected.size > 0 ? 'flex' : 'none';
  if (bar) bar.querySelector('span').textContent = t('contextSelectedCount', { n: _ctxSelected.size });
}
async function ctxDeleteSelected() {
  if (_ctxIsReadonly()) { addMsg('error', t('contextReadonly')); return; }
  if (!_ctxSelected.size) return;
  const mids = Array.from(_ctxSelected);
  _ctxSelected.clear();
  if (_ctxAgentFilter === 'transcript') {
    await _ctxMutate({action: 'delete_message', msg_ids: mids});
  } else {
    await _ctxMutate(
      _ctxScopedMutation({action: 'delete_context_messages', msg_ids: mids}),
      (d) => t('contextSaved', {n: d.message_count, tokens: d.token_estimate}));
  }
}

function showContextOverlay(data) {
  _ctxVisibleById = new Map();
  let overlay = document.getElementById('contextOverlay');
  if (overlay) overlay.remove();
  overlay = document.createElement('div');
  overlay.id = 'contextOverlay';
  overlay.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,0.7);display:flex;align-items:center;justify-content:center;z-index:9999';
  const _isTranscript = _ctxAgentFilter === 'transcript';
  const _isReadonly = !!data.readonly || _ctxIsReadonly();
  const statusBadge = _isTranscript ? ''
    : data.diverged
      ? '<span style="background:#5a3e00;color:#f4a261;padding:2px 8px;border-radius:8px;font-size:11px;font-weight:600">' + t('contextDiverged') + '</span>'
      : '<span style="background:#1b4332;color:#52b788;padding:2px 8px;border-radius:8px;font-size:11px;font-weight:600">' + t('contextSynced') + '</span>';
  const usage = data.context_usage || null;
  let usageHtml = '';
  if (usage && usage.max) {
    const used = Number(usage.used || 0);
    const max = Number(usage.max || 0);
    const pct = max > 0 ? used / max : 0;
    const pctTxt = Math.round(pct * 1000) / 10;
    const updated = usage.updated_at ? new Date(Number(usage.updated_at) * 1000).toLocaleTimeString() : '';
    usageHtml = '<div style="margin:-4px 0 10px 0;padding:8px 10px;border:1px solid #26324a;border-radius:8px;background:#101827;color:#aeb7d8;font-size:12px;font-family:monospace">'
      + t('contextGauge') + ': ' + used.toLocaleString() + ' / ' + max.toLocaleString()
      + ' = ' + pctTxt + '%'
      + ' · ' + t('contextUsageMessages') + '=' + (usage.message_count || data.message_count || 0)
      + ' · ' + t('contextUsageSource') + '=' + _ctxEscape(usage.source || 'context_command')
      + (updated ? ' · ' + t('contextUsageUpdated') + '=' + _ctxEscape(updated) : '')
      + '</div>';
  }
  const roleColors = {system:'#6c6c8a',user:'#4fc3f7',assistant:'#4ecdc4',tool:'#f4a261'};
  let msgsHtml = '';
  if (!data.context || data.context.length === 0) {
    msgsHtml = '<div style="color:#6c6c8a;text-align:center;padding:20px">' + t('noContext') + '</div>';
  } else {
    // Reverse: newest first
    const reversed = [...data.context].reverse();
    reversed.forEach((m) => {
      const mid = m.msg_id || m.trace_id || '';
      if (!mid) return;  // cannot edit/delete a message without a stable id
      _ctxVisibleById.set(mid, m);
      const color = roleColors[m.role] || '#808090';
      const safeMid = jsStringArg(mid);
      const badge = '<span style="display:inline-block;background:' + color + '22;color:' + color + ';padding:1px 6px;border-radius:6px;font-size:11px;font-weight:600;margin-right:6px">' + escapeHtml(m.role || '') + '</span>';
      const tcTag = m.has_tool_calls ? '<span style="color:#f4a261;font-size:10px;margin-left:4px">[tool_calls]</span>' : '';
      const src = m.source ? '<span style="color:#808090;font-size:10px;margin-left:4px">[' + escapeHtml(m.source.name || '') + ']</span>' : '';
      const content = _ctxDisplayContent(m);
      const editBtn = _isReadonly ? '' : '<button onclick="event.stopPropagation();ctxEditMessage(' + safeMid + ')" style="background:none;border:none;color:#4fc3f7;cursor:pointer;font-size:13px;padding:0 3px" title="' + escapeAttr(t('contextEdit')) + '">&#9998;</button>';
      const delBtn = _isReadonly ? '' : '<button onclick="event.stopPropagation();ctxDeleteMessage(' + safeMid + ')" style="background:none;border:none;color:#e74c3c;cursor:pointer;font-size:13px;padding:0 3px" title="' + escapeAttr(t('contextDelete')) + '">&#128465;</button>';
      msgsHtml += '<div data-msgid="' + escapeAttr(mid) + '" title="' + escapeAttr(t('contextExpandTitle')) + '" style="padding:6px 8px;border-bottom:1px solid #222;cursor:pointer" onmousedown="if(event.shiftKey||event.ctrlKey)event.preventDefault()" onclick="if(event.ctrlKey||event.shiftKey){event.preventDefault();ctxToggleSelect(this,event)}else{if(event.target&&event.target.closest(\'.ctx-preview,.ctx-full\'))return;this.querySelector(\'.ctx-full\')&&(this.querySelector(\'.ctx-full\').style.display=this.querySelector(\'.ctx-full\').style.display===\'block\'?\'none\':\'block\')}">'
        + '<div style="display:flex;align-items:center">' + badge + tcTag + src + '<span style="margin-left:auto">' + editBtn + delBtn + '</span></div>'
        + '<div class="ctx-preview" style="color:#c0c0d0;font-size:12px;margin-top:4px;white-space:pre-wrap;word-break:break-word;max-height:96px;overflow-y:auto;border:1px solid #1d2636;border-radius:6px;padding:6px;background:#0a0f18">' + content + '</div>'
        + '<div class="ctx-full" style="display:none;color:#a0a0c0;font-size:12px;margin-top:4px;white-space:pre-wrap;word-break:break-word;max-height:min(60vh,640px);overflow-y:auto;border:1px solid #222;border-radius:6px;padding:8px;background:#090d14">' + content + '</div>'
        + '</div>';
    });
    // "Load more" button if there are older messages
    if (_ctxHasMore) {
      msgsHtml += '<div id="ctxLoadMore" style="text-align:center;padding:12px">'
        + '<button onclick="ctxLoadMore()" style="background:#1e3a5f;color:#4fc3f7;border:none;border-radius:6px;padding:6px 16px;cursor:pointer;font-size:12px">'
        + escapeHtml(t('contextLoadOlder', { shown: _ctxCurrentOffset, total: _ctxTotalCount })) + '</button></div>';
    }
  }
  overlay.innerHTML = '<div style="background:#1a1a2e;border:1px solid #333;border-radius:12px;padding:20px;max-width:700px;width:90%;max-height:80vh;display:flex;flex-direction:column">'
    + '<div style="display:flex;align-items:center;gap:10px;margin-bottom:12px">'
    + '<h3 style="margin:0;color:#e0e0e0;font-size:16px">' + escapeHtml(t('contextTitle')) + '</h3>'
    + statusBadge
    + _buildCtxAgentDropdown(data)
    + '<span style="color:#6c6c8a;font-size:12px;margin-left:auto">' + escapeHtml(t('contextMessages', {n:data.message_count})) + ' &middot; ' + escapeHtml(t('contextTokens', {n:data.token_estimate})) + '</span>'
    + (_ctxAgentFilter && !_isTranscript ? '<button onclick="ctxDeleteContext()" style="background:#5a1a1a;color:#e74c3c;border:none;border-radius:6px;padding:3px 10px;cursor:pointer;font-size:11px;font-weight:600" title="' + escapeAttr(_isReadonly ? t('contextInvalidateRuntimeTitle') : t('contextDeleteContextTitle')) + '">\u{1F5D1} ' + escapeHtml(_isReadonly ? t('contextInvalidate') : t('contextDelete')) + '</button>' : '')
    + '<button onclick="ctxClose()" style="background:none;border:none;color:#aaa;cursor:pointer;font-size:18px;margin-left:4px">&times;</button>'
    + '</div>'
    + usageHtml
    + '<div id="ctx-msg-list" style="flex:1;overflow-y:auto;border:1px solid #222;border-radius:8px;background:#0d1117">' + msgsHtml + '</div>'
    + '<div id="ctxSelectBar" style="display:none;padding:6px 0;align-items:center;gap:8px;justify-content:center">'
    + '<span style="color:#6c5ce7;font-size:12px">' + t('contextSelectedCount', { n: 0 }) + '</span>'
    + (_isReadonly ? '' : '<button onclick="ctxDeleteSelected()" style="background:#e94560;color:#fff;border:none;border-radius:4px;padding:3px 10px;cursor:pointer;font-size:12px">' + t('contextDeleteSelected') + '</button>')
    + '<button onclick="document.querySelectorAll(\'.ctx-selected\').forEach(r=>{r.classList.remove(\'ctx-selected\');r.style.outline=\'\'});_ctxSelected.clear();document.getElementById(\'ctxSelectBar\').style.display=\'none\'" style="background:transparent;color:#aaa;border:1px solid #555;border-radius:4px;padding:3px 8px;cursor:pointer;font-size:12px">' + t('contextCancel') + '</button>'
    + '</div>'
    + (_isTranscript || _isReadonly ? '' : '<div style="padding:8px 0 0 0;text-align:center"><button onclick="ctxAddMessage()" style="background:#1e3a5f;color:#4fc3f7;border:none;border-radius:8px;padding:6px 18px;cursor:pointer;font-size:13px">+ ' + t('contextAdd') + '</button></div>')
    + '</div>';
  document.body.appendChild(overlay);
  const _sel = document.getElementById('ctxAgentFilter');
  if (_sel) _sel.value = _ctxAgentFilter;
}

// ── Tool Call Parser ────────────────────────────────────────────
function _parseToolCall(text) {
  // Formats supported:
  //   tool(val1, val2, val3)              — positional (mapped to schema server-side)
  //   tool(key=val, key2=val2)            — named
  //   tool(val1, key2=val2)               — mixed (positional first, then named)
  //   tool {"key": "value"}               — JSON
  //   tool()                              — no args
  const nameMatch = text.match(/^(\w+)/);
  if (!nameMatch) return { error: t('noToolNameFound') };
  const name = nameMatch[1];
  let rest = text.slice(name.length).trim();

  if (!rest || rest === '()') return { name, args: {}, positional: [] };

  // JSON object format
  if (rest.startsWith('{')) {
    try { return { name, args: JSON.parse(rest), positional: [] }; }
    catch (e) { return { error: t('invalidJsonMessage', { error: e.message }) }; }
  }

  // Strip outer parens
  if (rest.startsWith('(') && rest.endsWith(')')) {
    rest = rest.slice(1, -1).trim();
  }
  if (!rest) return { name, args: {}, positional: [] };

  // Tokenize: split on commas respecting quotes and brackets
  const tokens = _splitArgs(rest);
  const args = {};
  const positional = [];

  for (const token of tokens) {
    const eqMatch = token.match(/^(\w+)\s*=\s*([\s\S]*)$/);
    if (eqMatch) {
      // Named: key=value
      args[eqMatch[1]] = _parseValue(eqMatch[2].trim());
    } else {
      // Positional
      positional.push(_parseValue(token.trim()));
    }
  }
  return { name, args, positional };
}

function _splitArgs(s) {
  // Split on commas, respecting quotes, brackets, braces
  const result = [];
  let current = '';
  let depth = 0;  // [] {} depth
  let inStr = null;  // null, '"', "'"
  for (let i = 0; i < s.length; i++) {
    const c = s[i];
    if (inStr) {
      current += c;
      if (c === inStr && s[i - 1] !== '\\') inStr = null;
    } else if (c === '"' || c === "'") {
      current += c;
      inStr = c;
    } else if (c === '[' || c === '{') {
      current += c;
      depth++;
    } else if (c === ']' || c === '}') {
      current += c;
      depth--;
    } else if (c === ',' && depth === 0) {
      result.push(current);
      current = '';
    } else {
      current += c;
    }
  }
  if (current.trim()) result.push(current);
  return result;
}

function _parseValue(v) {
  if (!v) return '';
  // Quoted string
  if ((v.startsWith('"') && v.endsWith('"')) || (v.startsWith("'") && v.endsWith("'"))) {
    return v.slice(1, -1).replace(/\\"/g, '"').replace(/\\'/g, "'");
  }
  // JSON array or object
  if (v.startsWith('[') || v.startsWith('{')) {
    try { return JSON.parse(v); } catch(e) { return v; }
  }
  // Booleans / null
  if (v === 'true') return true;
  if (v === 'false') return false;
  if (v === 'null') return null;
  // Numbers
  if (/^\d+$/.test(v)) return parseInt(v);
  if (/^\d+\.\d+$/.test(v)) return parseFloat(v);
  // Bare string (unquoted)
  return v;
}
