#!/usr/bin/env node
'use strict';

const fs = require('fs');
const os = require('os');
const path = require('path');
const readline = require('readline');

const { ensureConfig } = require('../config');
const { createState } = require('../core/state');
const { buildSystemPrompt } = require('../core/context');
const { getModelByKey } = require('../config');
const { streamChat } = require('../model/client');
const { executeTool } = require('../tools/dispatcher');
const { openCommandMenu, hasCommandMatch } = require('../ui/command_menu');
const { handleCommand } = require('./commands');
const { Spinner, truncateThinking } = require('../ui/spinner');
const { renderBanner } = require('../ui/banner');
const { buildStartLine, buildFinalLine, startToolDisplay, formatResultLines, printResultLines } = require('../ui/tool_display');
const { createConversation, updateConversation } = require('../storage/conversation_store');
const { applyUsage, normalizeTokenUsage, normalizeUsagePayload } = require('../utils/token_usage');
const { gray, cyan, green, red, blue } = require('../utils/colors');
const { createIndentedWriter } = require('../ui/indented_writer');
const { createStatusBar } = require('../ui/status_bar');
const { visibleWidth } = require('../utils/text_width');
const { readMediafileTool } = require('../tools/read_mediafile');

const WORKSPACE = process.cwd();
const WORKSPACE_NAME = path.basename(WORKSPACE);
const USERNAME = os.userInfo().username || 'user';
const PROMPT = `${USERNAME}@${WORKSPACE_NAME} % `;
const MENU_PAGE_SIZE = 6;
const SLASH_COMMANDS = new Set(['/new', '/resume', '/allow', '/model', '/status', '/compact', '/config', '/help', '/exit']);
const IMAGE_EXTS = new Set(['.png', '.jpg', '.jpeg', '.gif', '.webp', '.bmp', '.tiff', '.heic']);
const VIDEO_EXTS = new Set(['.mp4', '.mov', '.avi', '.mkv', '.webm', '.m4v']);

const config = ensureConfig();
if (!config.valid_models || config.valid_models.length === 0) {
  console.log('');
  console.log(`未找到可用模型，请先在 ${config.path} 填写完整模型信息。`);
  console.log('');
  process.exit(1);
}
const state = createState(config, WORKSPACE);
state.conversation = createConversation(WORKSPACE, {
  model_key: state.modelKey,
  model_id: state.modelId,
  thinking_mode: state.thinkingMode,
  allow_mode: state.allowMode,
  token_usage: state.tokenUsage,
  cwd: WORKSPACE,
});

const systemPrompt = fs.readFileSync(path.join(__dirname, '../../prompts/system.txt'), 'utf8');
const tools = JSON.parse(fs.readFileSync(path.join(__dirname, '../tools/tools.json'), 'utf8'));

renderBanner({
  modelKey: state.modelKey,
  workspace: WORKSPACE,
  conversationId: state.conversation?.id,
});
console.log('');
console.log('');

let rl = null;
let statusBar = null;
let isRunning = false;
let escPendingCancel = false;
let activeStreamController = null;
let activeToolController = null;
let currentMedia = { tokens: [], text: '' };
let commandMenuActive = false;
let menuSearchTerm = '';
let menuLastSearchTerm = '';
let menuJustClosedAt = 0;
let menuInjectedCommand = null;
let menuAbortController = null;
let menuJustClosedInjected = false;
let suppressSlashMenuUntil = 0;
let pendingSlashTimer = null;

function printNotice(message) {
  console.log('');
  console.log(message);
  console.log('');
}

function printNoticeInline(message) {
  console.log(message);
}

function getPathExt(p) {
  const idx = p.lastIndexOf('.');
  if (idx === -1) return '';
  return p.slice(idx).toLowerCase();
}

function decodeEscapedPath(p) {
  let text = String(p || '');
  if (text.startsWith('file://')) {
    text = text.replace(/^file:\/\//, '');
  }
  try {
    text = decodeURIComponent(text);
  } catch (_) {}
  return text.replace(/\\ /g, ' ').replace(/\\\\/g, '\\');
}

function findMediaMatches(line) {
  const matches = [];
  const quoted = /'([^']+)'/g;
  let m;
  while ((m = quoted.exec(line)) !== null) {
    matches.push({ raw: m[0], path: m[1], index: m.index });
  }
  const extGroup = Array.from(new Set([...IMAGE_EXTS, ...VIDEO_EXTS]))
    .map((e) => e.replace('.', '\\.'))
    .join('|');
  const unquoted = new RegExp(`/((?:\\\\ |[^\\s])+?)\\.(${extGroup})`, 'g');
  while ((m = unquoted.exec(line)) !== null) {
    const raw = `/${m[1]}.${m[2]}`;
    matches.push({ raw, path: raw, index: m.index });
  }
  matches.sort((a, b) => a.index - b.index);
  return matches;
}

function applyMediaTokens(line) {
  if (!line) return { line: '', tokens: [] };
  const matches = findMediaMatches(line);
  if (!matches.length) return { line, tokens: [] };

  let images = 0;
  let videos = 0;
  let cursor = 0;
  let out = '';
  const tokens = [];

  for (const match of matches) {
    if (match.index < cursor) continue;
    const before = line.slice(cursor, match.index);
    out += before;
    const decoded = decodeEscapedPath(match.path);
    if (!fs.existsSync(decoded)) {
      out += match.raw;
      cursor = match.index + match.raw.length;
      continue;
    }
    const ext = getPathExt(decoded);
    const isImage = IMAGE_EXTS.has(ext);
    const isVideo = VIDEO_EXTS.has(ext);
    if (!isImage && !isVideo) {
      out += match.raw;
      cursor = match.index + match.raw.length;
      continue;
    }
    if (isImage && images >= 9) {
      out += match.raw;
      cursor = match.index + match.raw.length;
      continue;
    }
    if (isVideo && videos >= 1) {
      out += match.raw;
      cursor = match.index + match.raw.length;
      continue;
    }
    const token = isImage ? `[图片 #${images + 1}]` : `[视频 #${videos + 1}]`;
    tokens.push({ token, path: decoded, type: isImage ? 'image' : 'video' });
    out += token;
    if (isImage) images += 1;
    if (isVideo) videos += 1;
    cursor = match.index + match.raw.length;
  }
  out += line.slice(cursor);
  return { line: out, tokens };
}

function applySingleMediaPath(line, rawPath, tokens) {
  if (!line || !rawPath) return { line, tokens };
  const idx = line.indexOf(rawPath);
  if (idx === -1) return { line, tokens };
  const decoded = decodeEscapedPath(rawPath);
  if (!fs.existsSync(decoded)) return { line, tokens };
  const ext = getPathExt(decoded);
  const isImage = IMAGE_EXTS.has(ext);
  const isVideo = VIDEO_EXTS.has(ext);
  if (!isImage && !isVideo) return { line, tokens };
  const images = tokens.filter((t) => t.type === 'image').length;
  const videos = tokens.filter((t) => t.type === 'video').length;
  if (isImage && images >= 9) return { line, tokens };
  if (isVideo && videos >= 1) return { line, tokens };
  const token = isImage ? `[图片 #${images + 1}]` : `[视频 #${videos + 1}]`;
  const nextLine = line.slice(0, idx) + token + line.slice(idx + rawPath.length);
  const nextTokens = tokens.concat([{ token, path: decoded, type: isImage ? 'image' : 'video' }]);
  return { line: nextLine, tokens: nextTokens };
}

function colorizeTokens(line) {
  return line.replace(/\[(图片|视频) #\d+\]/g, (t) => blue(t));
}

function refreshInputLine() {
  if (!rl || !process.stdout.isTTY || commandMenuActive) return;
  const line = rl.line || '';
  const colorLine = colorizeTokens(line);
  readline.clearLine(process.stdout, 0);
  readline.cursorTo(process.stdout, 0);
  process.stdout.write(PROMPT + colorLine);
  const cursorCol = visibleWidth(PROMPT) + visibleWidth(line.slice(0, rl.cursor || 0));
  readline.cursorTo(process.stdout, cursorCol);
}

function removeTokenAtCursor(line, cursor, allowStart = false) {
  const tokenRe = /\[(图片|视频) #\d+\]/g;
  let m;
  while ((m = tokenRe.exec(line)) !== null) {
    const start = m.index;
    const end = m.index + m[0].length;
    if ((allowStart ? cursor >= start : cursor > start) && cursor <= end) {
      const nextLine = line.slice(0, start) + line.slice(end);
      return { line: nextLine, cursor: start, removed: true };
    }
  }
  return { line, cursor, removed: false };
}

function isSlashCommand(line) {
  if (!line || line[0] !== '/') return false;
  const cmd = line.split(/\s+/)[0];
  return SLASH_COMMANDS.has(cmd);
}

readline.emitKeypressEvents(process.stdin);
if (process.stdin.isTTY) process.stdin.setRawMode(true);

process.stdin.on('data', (chunk) => {
  if (isRunning || commandMenuActive || !rl) return;
  const text = chunk ? chunk.toString() : '';
  if (!text) return;
  const looksLikePath = text.includes('file://') || /\.(png|jpe?g|gif|webp|bmp|tiff|heic|mp4|mov|avi|mkv|webm|m4v)\b/i.test(text);
  if (!looksLikePath) return;
  suppressSlashMenuUntil = Date.now() + 200;
  setImmediate(() => {
    const line = rl.line || '';
    const raw = text.replace(/\r?\n/g, '').trim();
    const decoded = raw ? decodeEscapedPath(raw) : '';
    let applied = applyMediaTokens(line);
    if (raw) {
      if (line.includes(raw)) {
        applied = applySingleMediaPath(applied.line, raw, applied.tokens);
      } else if (decoded && line.includes(decoded)) {
        applied = applySingleMediaPath(applied.line, decoded, applied.tokens);
      }
    }
    if (applied.tokens.length) {
      rl.line = applied.line;
      rl.cursor = rl.line.length;
      currentMedia = { tokens: applied.tokens, text: applied.line };
      refreshInputLine();
    }
  });
});

initReadline();

statusBar = createStatusBar({
  getTokens: () => normalizeTokenUsage(state.tokenUsage).total || 0,
  getMaxTokens: () => {
    const model = getModelByKey(config, state.modelKey);
    return model && Number.isFinite(model.max_context) ? model.max_context : null;
  },
});
statusBar.render();

process.stdin.on('keypress', (str, key) => {
  if (commandMenuActive) {
    if (key && key.name === 'backspace' && menuSearchTerm === '') {
      if (menuAbortController && !menuAbortController.signal.aborted) {
        menuAbortController.abort();
      }
    }
    if (key && key.name === 'return') {
      const term = String(menuSearchTerm || '').replace(/^\/+/, '').trim();
      if (term && !hasCommandMatch(term)) {
        menuInjectedCommand = `/${term}`;
        if (menuAbortController && !menuAbortController.signal.aborted) {
          menuAbortController.abort();
        }
      }
    }
    return;
  }
  if (!isRunning && key && (key.name === 'backspace' || key.name === 'delete')) {
    const line = rl.line || '';
    const cursor = rl.cursor || 0;
    const updated = removeTokenAtCursor(line, cursor, key.name === 'delete');
    if (updated.removed) {
      rl.line = updated.line;
      rl.cursor = updated.cursor;
      refreshInputLine();
      if (statusBar) statusBar.render();
      return;
    }
    if (statusBar) statusBar.render();
  }
  if (key && key.name === 'escape' && isRunning) {
    escPendingCancel = true;
    if (activeStreamController && typeof activeStreamController.abort === 'function') {
      activeStreamController.abort();
    }
    if (activeToolController && typeof activeToolController.abort === 'function') {
      activeToolController.abort();
    }
    return;
  }
  if (str === '/' && Date.now() < suppressSlashMenuUntil) {
    return;
  }
  if (pendingSlashTimer && (str !== '/' || (rl.line && rl.line !== '/'))) {
    clearTimeout(pendingSlashTimer);
    pendingSlashTimer = null;
  }
  if (!isRunning) {
    const rawLine = rl.line || '';
    let applied = { line: rawLine, tokens: [] };
    if (!isSlashCommand(rawLine)) {
      const hasToken = /\[(图片|视频) #\d+\]/.test(rawLine);
      if (hasToken && currentMedia.tokens.length && currentMedia.text === rawLine) {
        applied = { line: rawLine, tokens: currentMedia.tokens };
      } else {
        applied = applyMediaTokens(rawLine);
      }
      if (hasToken && !applied.tokens.length && currentMedia.tokens.length) {
        applied = { line: rawLine, tokens: currentMedia.tokens };
      }
    }
    if (applied.line !== (rl.line || '')) {
      rl.line = applied.line;
      rl.cursor = rl.line.length;
      currentMedia = { tokens: applied.tokens, text: applied.line };
    } else if (currentMedia.text !== applied.line) {
      currentMedia = { tokens: applied.tokens, text: applied.line };
    }
    refreshInputLine();
  }
  if (statusBar) statusBar.render();
  if (str === '/' && (rl.line === '' || rl.line === '/')) {
    if (pendingSlashTimer) {
      clearTimeout(pendingSlashTimer);
      pendingSlashTimer = null;
    }
    pendingSlashTimer = setTimeout(() => {
      pendingSlashTimer = null;
      if (rl.line === '' || rl.line === '/') {
    commandMenuActive = true;
    if (rl) {
      rl.pause();
      rl.line = '';
      rl.cursor = 0;
      readline.clearLine(process.stdout, 0);
      readline.cursorTo(process.stdout, 0);
    }
        menuSearchTerm = '';
        menuAbortController = new AbortController();
        void openCommandMenu({
          rl,
          prompt: PROMPT,
          pageSize: MENU_PAGE_SIZE,
          colorEnabled: process.stdout.isTTY,
          resetAnsi: '\x1b[0m',
          onInput: (input) => {
            menuSearchTerm = input || '';
          },
          abortSignal: menuAbortController.signal,
        })
          .then((result) => {
            if (result && result.chosen && !result.cancelled) {
              menuInjectedCommand = result.chosen;
            }
          })
          .finally(() => {
            commandMenuActive = false;
            menuAbortController = null;
            menuJustClosedAt = menuInjectedCommand ? Date.now() : 0;
            menuJustClosedInjected = !!menuInjectedCommand;
            menuLastSearchTerm = menuSearchTerm;
            drainStdin();
            rl.line = '';
            rl.cursor = 0;
            menuSearchTerm = '';
            if (process.stdout.isTTY) {
              readline.clearScreenDown(process.stdout);
            }
            // Clear possible echoes from the base readline line (current + previous line).
            if (process.stdout.isTTY) {
              readline.clearLine(process.stdout, 0);
              readline.cursorTo(process.stdout, 0);
              readline.moveCursor(process.stdout, 0, -1);
              readline.clearLine(process.stdout, 0);
              readline.cursorTo(process.stdout, 0);
            } else {
              readline.clearLine(process.stdout, 0);
              readline.cursorTo(process.stdout, 0);
            }
            promptWithStatus(true);
            if (menuInjectedCommand) {
              const injected = menuInjectedCommand;
              menuInjectedCommand = null;
              setImmediate(() => injectLine(injected));
            }
          });
      }
    }, 80);
    return;
  }
});

function initReadline() {
  if (rl) {
    try {
      rl.removeAllListeners();
    } catch (_) {}
  }
  rl = readline.createInterface({
    input: process.stdin,
    output: process.stdout,
    terminal: true,
  });

  process.stdin.resume();
  rl.setPrompt(PROMPT);
  promptWithStatus();

  rl.on('line', async (line) => {
    if (rl) {
      rl.line = '';
      rl.cursor = 0;
    }
    if (commandMenuActive) {
      return;
    }
    let applied = { line, tokens: [] };
    if (!isSlashCommand(line)) {
      const hasToken = /\[(图片|视频) #\d+\]/.test(line);
      if (hasToken && currentMedia.tokens.length && currentMedia.text === line) {
        applied = { line, tokens: currentMedia.tokens };
      } else {
        applied = applyMediaTokens(line);
      }
      if (hasToken && !applied.tokens.length && currentMedia.tokens.length) {
        applied = { line, tokens: currentMedia.tokens };
      }
    }
    const normalizedLine = applied.line;
    currentMedia = { tokens: applied.tokens, text: normalizedLine };
    const input = normalizedLine.trim();
    if (menuJustClosedAt) {
      if (!menuJustClosedInjected) {
        const tooOld = Date.now() - menuJustClosedAt > 800;
        const normalizedMenu = String(menuLastSearchTerm).trim().replace(/^\/+/, '');
        const normalizedInput = input.replace(/^\/+/, '');
        if (!tooOld && (!normalizedMenu ? input === '' : normalizedInput === normalizedMenu)) {
          menuJustClosedAt = 0;
          menuLastSearchTerm = '';
          if (process.stdout.isTTY) {
            readline.moveCursor(process.stdout, 0, -1);
            readline.clearLine(process.stdout, 0);
            readline.cursorTo(process.stdout, 0);
          }
          promptWithStatus();
          return;
        }
      }
      menuJustClosedAt = 0;
      menuLastSearchTerm = '';
      menuJustClosedInjected = false;
    }
    if (!input) {
      promptWithStatus();
      return;
    }

    if (input.startsWith('/') && isSlashCommand(input)) {
      const result = await handleCommand(input, { rl, state, config, workspace: WORKSPACE, statusBar });
      if (result && result.exit) return;
      promptWithStatus();
      return;
    }
    if (input.startsWith('/') && !isSlashCommand(input)) {
      printNoticeInline(`无效的命令“${input}”`);
      promptWithStatus();
      return;
    }

    console.log('');
    const userWriter = createIndentedWriter('   ');
    const displayLine = colorizeTokens(normalizedLine);
    userWriter.writeLine(`${cyan('用户：')}${displayLine}`);
    const content = buildUserContent(normalizedLine, currentMedia.tokens);
    state.messages.push({ role: 'user', content });
    persistConversation();
    await runAssistantLoop();
    promptWithStatus();
  });

  rl.on('close', () => {
    if (statusBar) statusBar.destroy();
    process.stdout.write('\n');
    process.exit(0);
  });
}

function drainStdin() {
  if (!process.stdin.isTTY) return;
  process.stdin.pause();
  while (process.stdin.read() !== null) {}
  process.stdin.resume();
}

function injectLine(text) {
  if (!rl) return;
  rl.write(text);
  rl.write('\n');
}

function persistConversation() {
  if (!state.conversation) return;
  state.conversation = updateConversation(WORKSPACE, state.conversation, state.messages, {
    model_key: state.modelKey,
    model_id: state.modelId,
    thinking_mode: state.thinkingMode,
    allow_mode: state.allowMode,
    token_usage: state.tokenUsage,
    cwd: WORKSPACE,
  });
}

function promptWithStatus(force = false) {
  if (!rl) return;
  if (process.stdout.isTTY) {
    const rows = process.stdout.rows || 24;
    if (rows >= 3) {
      readline.cursorTo(process.stdout, 0, rows - 3);
      readline.clearLine(process.stdout, 0);
    }
  }
  rl.prompt(force);
  if (statusBar) statusBar.render();
}

function buildApiMessages() {
  const system = buildSystemPrompt(systemPrompt, {
    workspace: WORKSPACE,
    allowMode: state.allowMode,
    modelId: state.modelId || state.modelKey,
  });
  const messages = [{ role: 'system', content: system }];
  for (const msg of state.messages) {
    const m = { role: msg.role };
    if (msg.role === 'tool') {
      m.tool_call_id = msg.tool_call_id;
      m.content = msg.content;
    } else if (msg.role === 'assistant' && Array.isArray(msg.tool_calls)) {
      m.content = msg.content || null;
      m.tool_calls = msg.tool_calls;
      if (Object.prototype.hasOwnProperty.call(msg, 'reasoning_content')) {
        m.reasoning_content = msg.reasoning_content;
      } else if (state.thinkingMode) {
        m.reasoning_content = '';
      }
    } else {
      m.content = msg.content;
      if (msg.role === 'assistant' && Object.prototype.hasOwnProperty.call(msg, 'reasoning_content')) {
        m.reasoning_content = msg.reasoning_content;
      }
    }
    messages.push(m);
  }
  return messages;
}

function buildUserContent(line, tokens) {
  if (!tokens.length) return line;
  const parts = [{ type: 'text', text: line }];
  for (const info of tokens) {
    const media = readMediafileTool(WORKSPACE, { path: info.path });
    if (media && media.success) {
      const url = `data:${media.mime};base64,${media.b64}`;
      parts.push({
        type: media.type === 'image' ? 'image_url' : 'video_url',
        [media.type === 'image' ? 'image_url' : 'video_url']: { url },
      });
    }
  }
  return parts;
}

function printCancelLine() {
  console.log('');
  process.stdout.write(`  ${red('已取消本次响应')}\n\n`);
}

function stopSpinnerForCancel(spinner, thinkingActive, showThinkingLabel, thinkingMode) {
  if (!spinner) return;
  if (thinkingMode && (thinkingActive || showThinkingLabel)) {
    spinner.stop('∙ 停止思考');
  } else {
    spinner.stopSilent();
  }
}

async function runAssistantLoop() {
  let continueLoop = true;
  let firstLoop = true;
  const hideCursor = () => process.stdout.write('\x1b[?25l');
  const showCursor = () => process.stdout.write('\x1b[?25h');
  isRunning = true;
  escPendingCancel = false;
  if (statusBar) statusBar.setMode('running');
  try {
    while (continueLoop) {
    hideCursor();
    const spinner = new Spinner(' ', firstLoop ? 1 : 0);
    firstLoop = false;
    let thinkingBuffer = '';
    let fullThinkingBuffer = '';
    let thinkingActive = false;
    let showThinkingLabel = false;
    let gotAnswer = false;
    let assistantContent = '';
    let toolCalls = {};
    let usageTotal = null;
    let usagePrompt = null;
    let usageCompletion = null;
    let firstContent = true;
    let assistantWriter = null;
    let cancelled = false;

    const thinkingDelay = setTimeout(() => {
      if (state.thinkingMode) showThinkingLabel = true;
    }, 400);

    spinner.start(() => {
      if (!state.thinkingMode) return '';
      if (!showThinkingLabel) return '';
      return { label: ' 思考中...', thinking: thinkingBuffer, colorThinking: true };
    });

    const messages = buildApiMessages();
    const streamController = new AbortController();
    activeStreamController = streamController;
    try {
      const currentContextTokens = normalizeTokenUsage(state.tokenUsage).total || 0;
      for await (const chunk of streamChat({
        config,
        modelKey: state.modelKey,
        messages,
        tools,
        thinkingMode: state.thinkingMode,
        currentContextTokens,
        abortSignal: streamController.signal,
      })) {
        const choice = chunk.choices && chunk.choices[0];
        const usage = (chunk && chunk.usage)
          || (choice && choice.usage)
          || (choice && choice.delta && choice.delta.usage);
        const normalizedUsage = normalizeUsagePayload(usage);
        if (normalizedUsage) {
          if (Number.isFinite(normalizedUsage.prompt_tokens)) usagePrompt = normalizedUsage.prompt_tokens;
          if (Number.isFinite(normalizedUsage.completion_tokens)) usageCompletion = normalizedUsage.completion_tokens;
          if (Number.isFinite(normalizedUsage.total_tokens)) usageTotal = normalizedUsage.total_tokens;
        }
        if (!choice) continue;
        const delta = choice.delta || {};

        if (delta.reasoning_content || delta.reasoning_details || choice.reasoning_details) {
          thinkingActive = true;
          showThinkingLabel = true;
          let rc = '';
          if (delta.reasoning_content) {
            rc = delta.reasoning_content;
          } else if (delta.reasoning_details) {
            if (Array.isArray(delta.reasoning_details)) {
              rc = delta.reasoning_details.map((d) => d.text || '').join('');
            } else if (typeof delta.reasoning_details === 'string') {
              rc = delta.reasoning_details;
            } else if (delta.reasoning_details && typeof delta.reasoning_details.text === 'string') {
              rc = delta.reasoning_details.text;
            }
          } else if (choice.reasoning_details) {
            if (Array.isArray(choice.reasoning_details)) {
              rc = choice.reasoning_details.map((d) => d.text || '').join('');
            } else if (typeof choice.reasoning_details === 'string') {
              rc = choice.reasoning_details;
            } else if (choice.reasoning_details && typeof choice.reasoning_details.text === 'string') {
              rc = choice.reasoning_details.text;
            }
          }
          fullThinkingBuffer += rc;
          thinkingBuffer = truncateThinking(fullThinkingBuffer);
        }

        if (delta.tool_calls) {
          delta.tool_calls.forEach((tc) => {
            const idx = tc.index;
            if (!toolCalls[idx]) toolCalls[idx] = { id: tc.id, type: tc.type, function: { name: '', arguments: '' } };
            if (tc.function?.name) toolCalls[idx].function.name = tc.function.name;
            if (tc.function?.arguments) toolCalls[idx].function.arguments += tc.function.arguments;
          });
        }

        if (delta.content) {
          if (!gotAnswer) {
            clearTimeout(thinkingDelay);
            if (state.thinkingMode) {
              spinner.stop(thinkingActive ? '∙ 思考完成' : '∙');
            } else {
              spinner.stopSilent();
            }
            console.log('');
            assistantWriter = createIndentedWriter('   ');
            assistantWriter.write(`${green('Eagent：')}`);
            gotAnswer = true;
          }
          if (!gotAnswer) return;
          if (!assistantWriter) assistantWriter = createIndentedWriter('   ');
          assistantWriter.write(delta.content);
          assistantContent += delta.content;
        }

        if (choice.finish_reason) {
          if (choice.finish_reason === 'tool_calls') {
            continueLoop = true;
          }
        }
      }
    } catch (err) {
      if (err && (err.code === 'aborted' || err.name === 'AbortError' || escPendingCancel)) {
        cancelled = true;
      } else {
      clearTimeout(thinkingDelay);
      if (state.thinkingMode) {
        spinner.stop('∙');
      } else {
        spinner.stopSilent();
      }
      showCursor();
      console.log(`错误: ${err.message || err}`);
      return;
      }
    }
    activeStreamController = null;

    clearTimeout(thinkingDelay);
    if (cancelled) {
      if (gotAnswer) {
        spinner.stopSilent();
      } else {
        stopSpinnerForCancel(spinner, thinkingActive, showThinkingLabel, state.thinkingMode);
      }
      showCursor();
      printCancelLine();
      return;
    }
    if (!gotAnswer) {
      if (state.thinkingMode) {
        spinner.stop(thinkingActive ? '∙ 思考完成' : '∙');
      } else {
        spinner.stopSilent();
      }
    } else {
      process.stdout.write('\n\n');
    }
    showCursor();

    if (usageTotal !== null || usagePrompt !== null || usageCompletion !== null) {
      state.tokenUsage = applyUsage(normalizeTokenUsage(state.tokenUsage), {
        prompt_tokens: usagePrompt,
        completion_tokens: usageCompletion,
        total_tokens: usageTotal,
      });
    }

    const toolCallList = Object.keys(toolCalls)
      .map((k) => Number(k))
      .sort((a, b) => a - b)
      .map((k) => toolCalls[k]);
    if (toolCallList.length) {
      const assistantMsg = {
        role: 'assistant',
        content: assistantContent || null,
        tool_calls: toolCallList,
      };
      if (state.thinkingMode) assistantMsg.reasoning_content = fullThinkingBuffer || '';
      state.messages.push(assistantMsg);
      persistConversation();

      for (const call of toolCallList) {
        let args = {};
        try { args = JSON.parse(call.function.arguments || '{}'); } catch (_) {}
        const startLine = buildStartLine(call.function.name, args);
        const finalLine = buildFinalLine(call.function.name, args);
        const indicator = startToolDisplay(startLine);
        const toolController = new AbortController();
        activeToolController = toolController;
        const toolResult = await executeTool({
          workspace: WORKSPACE,
          config,
          allowMode: state.allowMode,
          toolCall: call,
          abortSignal: toolController.signal,
        });
        activeToolController = null;
        indicator.stop(finalLine);
        const resultLines = formatResultLines(call.function.name, args, toolResult.raw || { success: toolResult.success, error: toolResult.error });
        printResultLines(resultLines);

        const toolMsg = {
          role: 'tool',
          tool_call_id: call.id,
          content: toolResult.tool_content || toolResult.formatted,
          tool_raw: toolResult.raw,
        };
        state.messages.push(toolMsg);
        persistConversation();
        if (escPendingCancel || (toolResult.raw && toolResult.raw.cancelled)) {
          return;
        }
      }
      continueLoop = true;
      hideCursor();
      continue;
    }

    if (assistantContent) {
      const msg = { role: 'assistant', content: assistantContent };
      if (state.thinkingMode) msg.reasoning_content = fullThinkingBuffer || '';
      state.messages.push(msg);
      persistConversation();
    }
    continueLoop = false;
    }
  } finally {
    showCursor();
    isRunning = false;
    escPendingCancel = false;
    activeStreamController = null;
    activeToolController = null;
    if (statusBar) statusBar.setMode('input');
  }
}
