  function describeAction(action) {
    if (!action || !action.type) return 'action';
    var t = action.type;
    var label = action.title || action.id || action.file || action.agent || '';
    return label ? t + ': ' + label : t;
  }
  function renderActionResults(actions, results) {
    if (!actions || !actions.length) return;
    for (var i = 0; i < actions.length; i++) {
      var action = actions[i];
      var r = (results && results[i]) || null;
      var label = describeAction(action);
      var severity = 'ok';
      var symbol = '✓';
      var detail = '';
      if (r && r.error) { severity = 'err'; symbol = '✗'; detail = ' — ' + r.error; }
      else if (r && r.warning) { severity = 'warn'; symbol = 'ℹ'; detail = ' — ' + r.warning; }
      else if (r && r.duplicate) { severity = 'warn'; symbol = '↺'; detail = ' (already exists)'; }
      var text = symbol + ' ' + label + detail;
      appendActionStatus(severity, text);
      messages.push({ role: 'action', severity: severity, text: text });
    }
    saveState();
  }

  async function sendMessage() {
    if (sending) return;
    var text = inputEl.value.trim();
    if (!text) return;

    appendBubble('user', text);
    messages.push({ role: 'user', text: text });
    saveState();

    inputEl.value = '';
    inputEl.style.height = 'auto';
    setSending(true);

    var stream = buildStreamBubble();
    var streamedText = '';
    abortController = new AbortController();
    var userAborted = false;
    var timeoutSignal = null;
    try { timeoutSignal = AbortSignal.timeout(STREAM_TIMEOUT_MS); } catch (_e) { /* old browsers */ }
    var signal = timeoutSignal && AbortSignal.any
      ? AbortSignal.any([abortController.signal, timeoutSignal])
      : abortController.signal;

    try {
      var res = await fetch('/api/command-center/stream', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ message: text, tabId: tabId, sessionId: sessionId, currentProject: currentProject || undefined }),
        signal: signal,
      });
      if (!res.ok) {
        var errBody = await res.text().catch(function() { return ''; });
        throw new Error('HTTP ' + res.status + (errBody ? ': ' + errBody.slice(0, 200) : ''));
      }
      if (!res.body || !res.body.getReader) {
        throw new Error('Streaming not supported by this browser.');
      }

      var reader = res.body.getReader();
      var decoder = new TextDecoder();
      var buf = '';
      var doneEvt = null;
      var errorEvt = null;

      while (true) {
        var r = await reader.read();
        if (r.done) break;
        buf += decoder.decode(r.value, { stream: true });
        var lines = buf.split('\n');
        buf = lines.pop();
        for (var i = 0; i < lines.length; i++) {
          var line = lines[i];
          if (!line.startsWith('data: ')) continue;
          var evt;
          try { evt = JSON.parse(line.slice(6)); } catch (_e) { continue; }
          if (evt.type === 'chunk') {
            streamedText = mergeStreamText(streamedText, evt.text || '');
            stream.setText(streamedText);
          } else if (evt.type === 'tool') {
            stream.addTool(evt.name, evt.input || {});
          } else if (evt.type === 'heartbeat') {
            /* server keep-alive — ignored on purpose */
          } else if (evt.type === 'done') {
            doneEvt = evt;
          } else if (evt.type === 'error') {
            errorEvt = evt;
          }
        }
      }

      if (errorEvt) {
        stream.replaceWithError(errorEvt.error || 'Error');
        messages.push({ role: 'error', text: errorEvt.error || 'Error' });
        saveState();
        return;
      }

      if (doneEvt) {
        var finalText = mergeStreamText(streamedText, doneEvt.text || '');
        stream.finalize(finalText || '(no response)');
        messages.push({ role: 'assistant', text: finalText || '(no response)', toolCalls: stream.getToolList() });
        if (doneEvt.sessionId !== undefined) sessionId = doneEvt.sessionId || null;
        saveState();
        if (doneEvt.actionParseError) {
          var pe = '⚠️ Actions block emitted but JSON could not be parsed — no actions executed.';
          appendActionStatus('err', pe);
          messages.push({ role: 'action', severity: 'err', text: pe });
          saveState();
        }
        if (doneEvt.actions && doneEvt.actions.length > 0) {
          renderActionResults(doneEvt.actions, doneEvt.actionResults || []);
        }
        // Refresh status + history opportunistically — actions probably
        // changed something.
        scheduleStatusRefresh(800);
      } else {
        stream.finalize(streamedText || '(stream ended before completion)');
        messages.push({ role: 'assistant', text: streamedText || '(stream ended before completion)', toolCalls: stream.getToolList() });
        saveState();
      }
    } catch (e) {
      if (e && (e.name === 'AbortError' || /aborted/i.test(String(e.message || '')))) {
        userAborted = true;
        if (streamedText) {
          stream.finalize(streamedText);
          messages.push({ role: 'assistant', text: streamedText, toolCalls: stream.getToolList() });
        } else {
          stream.replaceWithError('Stopped.');
          messages.push({ role: 'error', text: 'Stopped.' });
        }
        saveState();
      } else {
        var msg = 'Send failed: ' + (e && e.message ? e.message : e);
        stream.replaceWithError(msg);
        messages.push({ role: 'error', text: msg });
        saveState();
      }
    } finally {
      setSending(false);
      abortController = null;
      if (!userAborted) inputEl.focus();
    }
  }

  function abortInFlight() {
    if (!sending || !abortController) return;
    try {
      fetch('/api/command-center/abort', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ tabId: tabId }),
      }).catch(function() {});
    } catch (_e) { /* ignore */ }
    try { abortController.abort(); } catch (_e) { /* ignore */ }
  }

  async function slimChatNew() {
    if (sending) abortInFlight();
    sessionId = null;
    messages = [];
    saveState();
    rerenderHistory();
    try {
      fetch('/api/cc-sessions/' + encodeURIComponent(tabId), { method: 'DELETE' }).catch(function() {});
    } catch (_e) { /* ignore */ }
    inputEl.focus();
  }

  // (The header "new chat" button was replaced by "Classic dashboard" /
  // "Report Bug"; new chats are created via the "+" affordance in the chat
  // tab bar — see renderTabBar.)
  sendBtn.addEventListener('click', sendMessage);
  stopBtn.addEventListener('click', abortInFlight);
  inputEl.addEventListener('keydown', function(ev) {
    if (ev.key === 'Enter' && !ev.shiftKey) {
      ev.preventDefault();
      sendMessage();
    }
  });
  inputEl.addEventListener('input', function() {
    inputEl.style.height = 'auto';
    inputEl.style.height = Math.min(inputEl.scrollHeight, 130) + 'px';
  });

  inputEl.focus();

