// @ts-nocheck
import { usePolicyStore } from '../../stores/policy';
import { useModelStore } from '../../stores/model';
import { usePersonalizationStore } from '../../stores/personalization';
import { useTutorialStore } from '../../stores/tutorial';
import { renderMarkdown as renderMarkdownHelper } from '../../composables/useMarkdownRenderer';
import {
  scrollToBottom as scrollToBottomHelper,
  conditionalScrollToBottom as conditionalScrollToBottomHelper,
  scrollThinkingToBottom as scrollThinkingToBottomHelper
} from '../../composables/useScrollControl';
import {
  startResize as startPanelResize,
  handleResize as handlePanelResize,
  stopResize as stopPanelResize
} from '../../composables/usePanelResize';
import { debugLog } from './common';

const SUB_AGENT_DONE_PREFIX_RE = /^(?:✅\s*)?子智能体\s*#?\s*(\d+)\s*任务摘要[:：]/;
const BG_RUN_COMMAND_DONE_PREFIX_RE = /^\[后台\s*run_command\s*完成\]/;
const userMDebug = (...args: any[]) => {
  console.log('[USERMDEBUG]', ...args);
};
let uiBounceTraceCount = 0;
const UI_BOUNCE_TRACE_MAX = 140;
const uiBounceTraceLastTsByKey = new Map<string, number>();

function isUiBounceTraceEnabled() {
  if (typeof window === 'undefined') return true;
  try {
    const explicit = (window as any).__SCROLL_BOUNCE_TRACE__;
    if (explicit === false || explicit === '0') return false;
    if (explicit === true || explicit === '1') return true;
    const localFlag = window.localStorage?.getItem('scrollBounceTrace');
    if (localFlag === '0' || localFlag === 'false') return false;
    if (localFlag === '1' || localFlag === 'true') return true;
    return true;
  } catch {
    return true;
  }
}

function uiBounceTrace(
  event: string,
  payload: Record<string, any> = {},
  key = event,
  throttleMs = 140
) {
  if (!isUiBounceTraceEnabled()) return;
  if (uiBounceTraceCount >= UI_BOUNCE_TRACE_MAX) return;
  const now = Date.now();
  const last = uiBounceTraceLastTsByKey.get(key) || 0;
  if (throttleMs > 0 && now - last < throttleMs) return;
  uiBounceTraceLastTsByKey.set(key, now);
  uiBounceTraceCount += 1;
  if (uiBounceTraceCount === UI_BOUNCE_TRACE_MAX) {
    console.warn('[SCROLL_BOUNCE_TRACE_UI]', 'log-limit-reached', { max: UI_BOUNCE_TRACE_MAX });
    return;
  }
  console.log('[SCROLL_BOUNCE_TRACE_UI]', event, payload);
}

function isConnectionDiagEnabled() {
  if (typeof window === 'undefined') return true;
  try {
    const explicit = (window as any).__CONN_DIAG__;
    if (explicit === false || explicit === '0') return false;
    if (explicit === true || explicit === '1') return true;
    const localFlag = window.localStorage?.getItem('connDiag');
    if (localFlag === '0' || localFlag === 'false') return false;
    if (localFlag === '1' || localFlag === 'true') return true;
    // 默认常开：无需手动启动
    return true;
  } catch {
    return true;
  }
}

function pushConnectionDiagRecord(record: Record<string, any>) {
  if (typeof window === 'undefined') return;
  try {
    const w = window as any;
    if (!Array.isArray(w.__CONN_DIAG_LOGS__)) {
      w.__CONN_DIAG_LOGS__ = [];
    }
    w.__CONN_DIAG_LOGS__.push(record);
    const max = 400;
    if (w.__CONN_DIAG_LOGS__.length > max) {
      w.__CONN_DIAG_LOGS__.splice(0, w.__CONN_DIAG_LOGS__.length - max);
    }
  } catch {
    // ignore
  }
}

function connectionDiag(
  level: 'log' | 'warn' | 'error',
  event: string,
  payload: Record<string, any> = {},
  options: { force?: boolean } = {}
) {
  const force = !!options.force;
  const enabled = isConnectionDiagEnabled();
  if (!enabled && !force) {
    return;
  }
  const record = {
    ts: new Date().toISOString(),
    event,
    ...payload
  };
  pushConnectionDiagRecord(record);
  const logger = level === 'error' ? console.error : level === 'warn' ? console.warn : console.log;
  logger('[CONN_DIAG]', event, record);
}

function parseSubAgentDoneLabel(rawContent: any): string | null {
  const content = (rawContent || '').toString().trim();
  if (!content) {
    console.log('[DEBUG_SYSTEM][ui] parseSubAgentDoneLabel: 空内容');
    return null;
  }
  const match = content.match(SUB_AGENT_DONE_PREFIX_RE);
  const isCompleted = /已完成/.test(content);
  console.log('[DEBUG_SYSTEM][ui] parseSubAgentDoneLabel', {
    content,
    regex: SUB_AGENT_DONE_PREFIX_RE.toString(),
    matched: !!match,
    matchAgentId: match?.[1],
    isCompleted
  });
  if (!match || !isCompleted) {
    return null;
  }
  const agentId = match[1];
  return `子智能体${agentId} 任务完成`;
}

function parseBackgroundRunCommandDoneLabel(rawContent: any): string | null {
  const content = (rawContent || '').toString().trim();
  if (!content) {
    return null;
  }
  if (!BG_RUN_COMMAND_DONE_PREFIX_RE.test(content)) {
    return null;
  }
  return '后台 run_command 完成';
}

function parseSystemNoticeLabel(rawContent: any): string | null {
  const content = (rawContent || '').toString().trim();
  if (!content) return null;
  return parseSubAgentDoneLabel(content) || parseBackgroundRunCommandDoneLabel(content);
}

export const uiMethods = {
  ensureScrollListener() {
    if (this._scrollListenerReady) {
      return;
    }
    const area = this.getMessagesAreaElement();
    if (!area) {
      return;
    }
    this.initScrollListener();
    this._scrollListenerReady = true;
  },

  handleStickStateChange(payload) {
    const escaped = !!payload?.escapedFromLock;
    const isAtBottom = !!payload?.isAtBottom;
    const isNearBottom = !!payload?.isNearBottom;
    const nearBottom = isAtBottom || isNearBottom;
    const userEscaped = !!this._escapedByUserScroll;
    uiBounceTrace(
      'stick-state-change',
      {
        isAtBottom,
        isNearBottom,
        escapedFromLock: escaped,
        userEscaped,
        autoScrollEnabled: this.autoScrollEnabled,
        userScrolling: this.userScrolling
      },
      'stick-state-change',
      180
    );
    this.stickIsAtBottom = isAtBottom;
    this.stickIsNearBottom = isNearBottom;

    // 用户主动脱离锁定：只要没回到底部/近底部，就保持“手动滚动中”状态
    if (userEscaped) {
      if (nearBottom) {
        this._escapedByUserScroll = false;
        this.chatSetScrollState({ userScrolling: false });
      } else {
        this.chatSetScrollState({ userScrolling: true });
      }
      return;
    }

    // 非用户触发的 escaped（例如高度突增导致的短暂锚点漂移）不应解除锁定
    this.chatSetScrollState({ userScrolling: false });
    if (!escaped) {
      return;
    }
    const active = typeof this.isOutputActive === 'function' ? this.isOutputActive() : true;
    if (!this.autoScrollEnabled || !active) {
      return;
    }
    const chatArea = this.getChatAreaController();
    if (chatArea && typeof chatArea.conditionalStickToBottom === 'function') {
      chatArea.conditionalStickToBottom({ force: false });
      return;
    }
    conditionalScrollToBottomHelper(this);
  },

  handleUserScrollIntent(payload) {
    const ts = Number(payload?.ts || Date.now());
    const chatArea = this.getChatAreaController();
    if (chatArea && typeof chatArea.stopStickScroll === 'function') {
      chatArea.stopStickScroll();
    }
    // 用户手动滚动后，短时间内禁止任何自动追底，规避 escapedFromLock 状态抖动竞态
    this._manualScrollSuppressUntil = Math.max(this._manualScrollSuppressUntil || 0, ts + 1600);
    this._escapedByUserScroll = true;
    this.chatSetScrollState({ userScrolling: true });
    uiBounceTrace(
      'ui.user-scroll-intent',
      {
        ts,
        delta: Number(payload?.delta || 0),
        top: Number(payload?.top || 0),
        suppressUntil: this._manualScrollSuppressUntil
      },
      'ui.user-scroll-intent',
      80
    );
  },

  setupMobileViewportWatcher() {
    if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') {
      this.updateMobileViewportState(false);
      return;
    }
    const query = window.matchMedia('(max-width: 768px)');
    this.mobileViewportQuery = query;
    this.updateMobileViewportState(query.matches);
    if (typeof query.addEventListener === 'function') {
      query.addEventListener('change', this.handleMobileViewportQueryChange);
    } else if (typeof query.addListener === 'function') {
      query.addListener(this.handleMobileViewportQueryChange);
    }
  },

  teardownMobileViewportWatcher() {
    const query = this.mobileViewportQuery;
    if (!query) {
      return;
    }
    if (typeof query.removeEventListener === 'function') {
      query.removeEventListener('change', this.handleMobileViewportQueryChange);
    } else if (typeof query.removeListener === 'function') {
      query.removeListener(this.handleMobileViewportQueryChange);
    }
    this.mobileViewportQuery = null;
  },

  handleMobileViewportQueryChange(event) {
    this.updateMobileViewportState(event.matches);
  },

  updateMobileViewportState(isMobile) {
    this.uiSetMobileViewport(!!isMobile);
    if (!isMobile) {
      this.uiSetMobileOverlayMenuOpen(false);
      this.closeMobileOverlay();
    }
  },

  toggleMobileOverlayMenu() {
    if (!this.isMobileViewport) {
      return;
    }
    this.uiToggleMobileOverlayMenu();
  },

  openMobileOverlay(target) {
    console.log(
      '[UI_DEBUG] openMobileOverlay called, target:',
      target,
      'isMobileViewport:',
      this.isMobileViewport,
      'activeMobileOverlay:',
      this.activeMobileOverlay
    );
    if (!this.isMobileViewport) {
      console.log('[UI_DEBUG] openMobileOverlay: blocked - not mobile viewport');
      return;
    }
    if (target === 'approval') {
      this.fetchPendingToolApprovals();
    }
    if (this.activeMobileOverlay === target) {
      console.log('[UI_DEBUG] openMobileOverlay: target already active, closing');
      this.closeMobileOverlay('same-target-click');
      return;
    }
    if (this.activeMobileOverlay === 'conversation') {
      this.uiSetSidebarCollapsed(true);
    }
    if (target === 'conversation') {
      this.uiSetSidebarCollapsed(false);
    }
    console.log('[UI_DEBUG] openMobileOverlay: opening target:', target);
    this.uiSetActiveMobileOverlay(target);
    this.uiSetMobileOverlayMenuOpen(false);
  },

  normalizeComposerDraftContent(rawValue) {
    let text = '';
    if (typeof rawValue === 'string') {
      text = rawValue;
    } else if (rawValue === null || rawValue === undefined) {
      text = '';
    } else {
      text = String(rawValue);
    }
    if (text.length > 40000) {
      text = text.slice(0, 40000);
    }
    return text;
  },

  scheduleComposerDraftPersist(reason = 'input') {
    const current = this.normalizeComposerDraftContent(this.inputMessage);
    const lastSynced = this.normalizeComposerDraftContent(this.composerDraftLastSyncedContent);
    const dirty = current !== lastSynced;
    this.composerDraftDirty = dirty;
    if (this.composerDraftSaveTimer) {
      clearTimeout(this.composerDraftSaveTimer);
      this.composerDraftSaveTimer = null;
    }
    if (!dirty) {
      return;
    }
    this.composerDraftSaveTimer = window.setTimeout(() => {
      this.persistComposerDraftNow({ reason: `debounce:${reason}` }).catch(() => {});
    }, 1000);
  },

  async persistComposerDraftNow(options = {}) {
    const reason = String(options?.reason || 'manual');
    const force = !!options?.force;
    const useBeacon = !!options?.useBeacon;
    const content = this.normalizeComposerDraftContent(
      Object.prototype.hasOwnProperty.call(options || {}, 'content')
        ? options.content
        : this.inputMessage
    );
    const lastSynced = this.normalizeComposerDraftContent(this.composerDraftLastSyncedContent);
    if (!force && content === lastSynced) {
      this.composerDraftDirty = false;
      return { success: true, skipped: true };
    }

    if (this.composerDraftSaveTimer) {
      clearTimeout(this.composerDraftSaveTimer);
      this.composerDraftSaveTimer = null;
    }

    const payloadText = JSON.stringify({ content });
    if (
      useBeacon &&
      typeof navigator !== 'undefined' &&
      typeof navigator.sendBeacon === 'function'
    ) {
      try {
        const blob = new Blob([payloadText], { type: 'application/json' });
        navigator.sendBeacon('/api/input-draft', blob);
        this.composerDraftLastSyncedContent = content;
        this.composerDraftDirty = false;
        return { success: true, queued: true, reason };
      } catch (error) {
        // sendBeacon 失败时回落到 fetch
      }
    }

    const response = await fetch('/api/input-draft', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json'
      },
      credentials: 'same-origin',
      body: payloadText,
      keepalive: !!options?.keepalive
    });
    const data = await response.json().catch(() => ({}));
    if (!response.ok || !data?.success) {
      throw new Error(data?.error || '保存输入草稿失败');
    }
    this.composerDraftLastSyncedContent = content;
    this.composerDraftDirty = false;
    return { success: true, saved: true, reason };
  },

  async restoreComposerDraftState(reason = 'manual') {
    const fetchSeq = Number(this.composerDraftFetchSeq || 0) + 1;
    this.composerDraftFetchSeq = fetchSeq;
    try {
      const response = await fetch('/api/input-draft', {
        method: 'GET',
        credentials: 'same-origin'
      });
      const payload = await response.json().catch(() => ({}));
      if (!response.ok || !payload?.success) {
        throw new Error(payload?.error || '获取输入草稿失败');
      }
      if (Number(this.composerDraftFetchSeq || 0) !== fetchSeq) {
        return;
      }
      const content = this.normalizeComposerDraftContent(payload?.data?.content || '');
      this.composerDraftLastSyncedContent = content;
      this.composerDraftDirty = false;
      this.inputSetMessage(content);
      this.$nextTick(() => {
        if (typeof this.autoResizeInput === 'function') {
          this.autoResizeInput();
        }
      });
      debugLog('[UI] 输入草稿已恢复', { reason, length: content.length });
    } catch (error) {
      console.warn('[UI] 恢复输入草稿失败:', error);
    }
  },

  handleBeforeUnloadDraftPersist() {
    this.persistComposerDraftNow({
      reason: 'beforeunload',
      force: true,
      useBeacon: true
    }).catch(() => {});
  },

  clearComposerDraftState(reason = 'manual') {
    debugLog('[UI] 清理输入草稿状态', { reason });
    this.inputClearMessage();
    this.composerDraftLastSyncedContent = '';
    this.composerDraftDirty = false;
    if (this.composerDraftSaveTimer) {
      clearTimeout(this.composerDraftSaveTimer);
      this.composerDraftSaveTimer = null;
    }
    this.inputSetLineCount(1);
    this.inputSetMultiline(false);
    this.inputClearSelectedImages();
    this.inputClearSelectedVideos();
    this.inputSetImagePickerOpen(false);
    this.inputSetVideoPickerOpen(false);
    this.imageEntries = [];
    this.videoEntries = [];
    this.imageLoading = false;
    this.videoLoading = false;
    this.mediaUploading = false;
    this.$nextTick(() => {
      if (typeof this.autoResizeInput === 'function') {
        this.autoResizeInput();
      }
    });
  },

  refreshCurrentPage() {
    if (typeof window === 'undefined') {
      return;
    }
    this.persistComposerDraftNow({
      reason: 'refresh-page',
      force: true,
      keepalive: true
    })
      .catch(() => {})
      .finally(() => {
        window.location.reload();
      });
  },

  handleMobileRefreshClick() {
    this.uiSetMobileOverlayMenuOpen(false);
    this.refreshCurrentPage();
  },

  closeMobileOverlay(source = 'unknown') {
    console.log(
      '[UI_DEBUG] closeMobileOverlay called from:',
      source,
      'activeMobileOverlay:',
      this.activeMobileOverlay
    );
    if (!this.activeMobileOverlay) {
      this.uiCloseMobileOverlay();
      return;
    }
    if (this.activeMobileOverlay === 'conversation') {
      this.uiSetSidebarCollapsed(true);
    }
    this.uiCloseMobileOverlay();
  },

  applyPolicyUiLocks() {
    const policyStore = usePolicyStore();
    const blocks = policyStore.uiBlocks;
    if (blocks.collapse_workspace) {
      this.uiSetWorkspaceCollapsed(true);
    }
    if (blocks.block_virtual_monitor && this.chatDisplayMode === 'monitor') {
      this.uiSetChatDisplayMode('chat');
    }
  },

  isPolicyBlocked(key: string, message?: string) {
    const policyStore = usePolicyStore();
    if (policyStore.uiBlocks[key]) {
      this.uiPushToast({
        title: '已被管理员禁用',
        message: message || '被管理员强制禁用',
        type: 'warning'
      });
      return true;
    }
    return false;
  },

  handleClickOutsideMobileMenu(event) {
    if (!this.isMobileViewport || !this.mobileOverlayMenuOpen) {
      return;
    }
    const trigger = this.$refs.mobilePanelTrigger;
    if (trigger && typeof trigger.contains === 'function' && trigger.contains(event.target)) {
      return;
    }
    this.uiSetMobileOverlayMenuOpen(false);
  },

  handleWorkspaceToggle() {
    if (this.isMobileViewport) {
      return;
    }
    if (this.isPolicyBlocked('collapse_workspace', '工作区已被管理员强制折叠')) {
      this.uiSetWorkspaceCollapsed(true);
      return;
    }
    const nextState = !this.workspaceCollapsed;
    this.uiSetWorkspaceCollapsed(nextState);
    if (nextState) {
      this.uiSetPanelMenuOpen(false);
    }
  },

  handleDisplayModeToggle() {
    if (this.displayModeSwitchDisabled) {
      // 显式提示管理员禁用
      this.isPolicyBlocked('block_virtual_monitor', '虚拟显示器已被管理员禁用');
      return;
    }
    if (
      this.chatDisplayMode === 'chat' &&
      this.isPolicyBlocked('block_virtual_monitor', '虚拟显示器已被管理员禁用')
    ) {
      return;
    }
    const next = this.chatDisplayMode === 'chat' ? 'monitor' : 'chat';
    this.uiSetChatDisplayMode(next);
  },

  handleMobileOverlayEscape(event) {
    if (event.key !== 'Escape' || !this.isMobileViewport) {
      return;
    }
    if (this.mobileOverlayMenuOpen) {
      this.uiSetMobileOverlayMenuOpen(false);
      return;
    }
    if (this.activeMobileOverlay) {
      this.closeMobileOverlay();
    }
  },

  handleMobileOverlaySelect(conversationId) {
    this.loadConversation(conversationId);
    this.closeMobileOverlay();
  },

  handleMobilePersonalClick() {
    this.closeMobileOverlay();
    this.uiSetMobileOverlayMenuOpen(false);
    this.openPersonalPage();
  },

  toggleSidebar() {
    if (this.isMobileViewport && this.activeMobileOverlay === 'conversation') {
      this.closeMobileOverlay();
      return;
    }
    this.uiToggleSidebar();
  },

  togglePanelMenu() {
    this.uiTogglePanelMenu();
  },

  selectPanelMode(mode) {
    this.uiSetPanelMode(mode);
    this.uiSetPanelMenuOpen(false);
    if (mode === 'subAgents') {
      // 切换到子智能体面板时立即刷新，避免等待轮询间隔
      this.subAgentFetch();
      this.subAgentStartPolling();
      return;
    }
    if (mode === 'backgroundCommands') {
      // 切换到后台指令面板时立即刷新，避免等待轮询间隔
      this.backgroundCommandFetch();
      this.backgroundCommandStartPolling();
    }
  },

  openPersonalPage() {
    if (this.isPolicyBlocked('block_personal_space', '个人空间已被管理员禁用')) {
      return;
    }
    this.personalizationOpenDrawer();
  },

  async checkTutorialPrompt() {
    this.tutorialPromptVisible = false;
    this.tutorialPromptUsername = '';
    try {
      const resp = await fetch('/api/tutorial-status', { credentials: 'same-origin' });
      const data = await resp.json().catch(() => ({}));
      console.info('[tutorial-prompt] status response:', {
        ok: resp.ok,
        status: resp.status,
        data
      });
      if (!resp.ok || !data?.success) {
        return;
      }
      const payload = data.data || {};
      if (!payload.applicable || !payload.should_prompt) {
        return;
      }
      this.tutorialPromptUsername = payload.username || '';
      this.tutorialPromptVisible = true;
    } catch (error) {
      console.warn('获取新手教程提示状态失败:', error);
    }
  },

  async updateTutorialPromptStatus(completed = true) {
    this.tutorialPromptLoading = true;
    try {
      const resp = await fetch('/api/tutorial-status', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        credentials: 'same-origin',
        body: JSON.stringify({ tutorial_completed: !!completed })
      });
      const data = await resp.json().catch(() => ({}));
      if (!resp.ok || !data?.success) {
        throw new Error(data?.error || '更新新手教程状态失败');
      }
      const tutorialStore = useTutorialStore();
      tutorialStore.markCompleted();
      this.tutorialPromptVisible = false;
      return true;
    } catch (error) {
      console.warn('更新新手教程提示状态失败:', error);
      this.uiPushToast({
        title: '提示',
        message: '保存新手教程状态失败，请稍后重试',
        type: 'warning'
      });
      return false;
    } finally {
      this.tutorialPromptLoading = false;
    }
  },

  async handleNewUserTutorialStart() {
    const ok = await this.updateTutorialPromptStatus(true);
    if (!ok) {
      return;
    }
    const personalStore = usePersonalizationStore();
    personalStore.closeDrawer();
    const tutorialStore = useTutorialStore();
    window.setTimeout(() => {
      tutorialStore.startTutorial();
    }, 220);
  },

  async handleNewUserTutorialSkip() {
    await this.updateTutorialPromptStatus(true);
  },

  async fetchHostWorkspaces() {
    if (!this.versioningHostMode) {
      this.hostWorkspaces = [];
      this.currentHostWorkspaceId = '';
      this.defaultHostWorkspaceId = '';
      return;
    }
    try {
      const resp = await fetch('/api/host/workspaces');
      const payload = await resp.json().catch(() => ({}));
      if (!resp.ok || !payload?.success) {
        throw new Error(payload?.error || '获取宿主机工作区列表失败');
      }
      const data = payload.data || {};
      const items = Array.isArray(data.workspaces) ? data.workspaces : [];
      this.hostWorkspaces = items.map((item: any) => ({
        workspace_id: String(item.workspace_id || ''),
        label: String(item.label || item.workspace_id || '未命名工作区'),
        path: String(item.path || ''),
        is_current: !!item.is_current
      }));
      this.currentHostWorkspaceId = String(data.current_workspace_id || '');
      this.defaultHostWorkspaceId = String(data.default_workspace_id || '');
    } catch (error) {
      console.warn('加载宿主机工作区失败:', error);
      this.hostWorkspaces = [];
      this.currentHostWorkspaceId = '';
      this.defaultHostWorkspaceId = '';
    }
  },

  async handleHostWorkspaceSwitch(workspaceId) {
    const targetId = String(workspaceId || '').trim();
    if (!targetId || this.hostWorkspaceSwitching) {
      return;
    }
    if (!this.versioningHostMode) {
      return;
    }
    if (targetId === this.currentHostWorkspaceId) {
      return;
    }

    this.hostWorkspaceSwitching = true;
    try {
      // 先在“当前工作区作用域”落盘草稿，再切换工作区。
      // 否则会把旧工作区草稿误写到目标工作区作用域里。
      await this.persistComposerDraftNow({
        reason: 'switch-host-workspace:before-select',
        force: true,
        keepalive: true
      }).catch(() => {});

      const query = encodeURIComponent(targetId);
      const resp = await fetch(`/api/host/workspaces/select?workspace_id=${query}`);
      const payload = await resp.json().catch(() => ({}));
      if (!resp.ok || !payload?.success) {
        throw new Error(payload?.error || '切换宿主机工作区失败');
      }
      const data = payload.data || {};
      this.currentHostWorkspaceId = String(data.current_workspace_id || targetId);
      if (data.project_path) {
        this.projectPath = String(data.project_path);
      }
      if (data.default_workspace_id) {
        this.defaultHostWorkspaceId = String(data.default_workspace_id);
      }
      this.uiPushToast({
        title: '工作区已切换',
        message: '正在刷新页面…',
        type: 'success'
      });
      window.setTimeout(() => {
        window.location.reload();
      }, 260);
    } catch (error) {
      const message = error instanceof Error ? error.message : String(error || '切换失败');
      this.uiPushToast({
        title: '切换工作区失败',
        message,
        type: 'error'
      });
    } finally {
      this.hostWorkspaceSwitching = false;
    }
  },

  async handleCreateHostWorkspace() {
    if (!this.versioningHostMode) {
      return;
    }
    if (this.hostWorkspaceSwitching || this.hostWorkspaceCreateSubmitting) {
      return;
    }
    this.hostWorkspaceCreatePath = '';
    this.hostWorkspaceCreateLabel = '';
    this.hostWorkspaceCreateError = '';
    this.hostWorkspaceCreateDialogOpen = true;
  },

  closeHostWorkspaceCreateDialog() {
    if (this.hostWorkspaceCreateSubmitting) {
      return;
    }
    this.hostWorkspaceCreateDialogOpen = false;
    this.hostWorkspaceCreateError = '';
  },

  async submitHostWorkspaceCreate() {
    if (!this.versioningHostMode) {
      return;
    }
    if (this.hostWorkspaceSwitching || this.hostWorkspaceCreateSubmitting) {
      return;
    }
    const workspacePath = String(this.hostWorkspaceCreatePath || '').trim();
    if (!workspacePath) {
      this.hostWorkspaceCreateError = '路径不能为空';
      return;
    }
    const label = String(this.hostWorkspaceCreateLabel || '').trim();

    this.hostWorkspaceCreateSubmitting = true;
    this.hostWorkspaceCreateError = '';
    try {
      const resp = await fetch('/api/host/workspaces/create', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        credentials: 'same-origin',
        body: JSON.stringify({
          path: workspacePath,
          label
        })
      });
      const payload = await resp.json().catch(() => ({}));
      if (!resp.ok || !payload?.success) {
        throw new Error(payload?.error || '新建宿主机工作区失败');
      }
      await this.fetchHostWorkspaces();
      const createdLabel = payload?.data?.workspace?.label || label || workspacePath;
      this.hostWorkspaceCreateDialogOpen = false;
      this.hostWorkspaceCreatePath = '';
      this.hostWorkspaceCreateLabel = '';
      this.uiPushToast({
        title: '工作区已创建',
        message: createdLabel,
        type: 'success'
      });
    } catch (error) {
      const message = error instanceof Error ? error.message : String(error || '新建失败');
      this.hostWorkspaceCreateError = message;
      this.uiPushToast({
        title: '新建工作区失败',
        message,
        type: 'error'
      });
    } finally {
      this.hostWorkspaceCreateSubmitting = false;
    }
  },

  fetchTodoList() {
    return this.fileFetchTodoList();
  },

  fetchSubAgents() {
    return this.subAgentFetch();
  },

  async toggleThinkingMode() {
    await this.handleCycleRunMode();
  },

  handleQuickModeToggle() {
    if (!this.isConnected || this.streamingMessage) {
      return;
    }
    this.handleCycleRunMode();
  },

  toggleQuickMenu() {
    if (!this.isConnected) {
      return;
    }
    const opened = this.inputToggleQuickMenu();
    if (!opened) {
      this.modeMenuOpen = false;
      this.modelMenuOpen = false;
    }
  },

  closeQuickMenu() {
    this.inputCloseMenus();
    this.modeMenuOpen = false;
    this.modelMenuOpen = false;
  },

  toggleModeMenu() {
    if (!this.isConnected || this.streamingMessage) {
      return;
    }
    const next = !this.modeMenuOpen;
    this.modeMenuOpen = next;
    if (next) {
      this.modelMenuOpen = false;
    }
    if (next) {
      this.inputSetToolMenuOpen(false);
      this.inputSetSettingsOpen(false);
      if (!this.quickMenuOpen) {
        this.inputOpenQuickMenu();
      }
    }
  },

  toggleModelMenu() {
    if (!this.isConnected || this.streamingMessage) {
      return;
    }
    const next = !this.modelMenuOpen;
    this.modelMenuOpen = next;
    if (next) {
      this.modeMenuOpen = false;
      this.inputSetToolMenuOpen(false);
      this.inputSetSettingsOpen(false);
      if (!this.quickMenuOpen) {
        this.inputOpenQuickMenu();
      }
    }
  },

  toggleHeaderMenu() {
    if (!this.isConnected) return;
    this.headerMenuOpen = !this.headerMenuOpen;
    if (this.headerMenuOpen) {
      this.closeQuickMenu();
      this.inputCloseMenus();
    }
  },

  async handleModeSelect(mode) {
    if (!this.isConnected || this.streamingMessage) {
      return;
    }
    await this.setRunMode(mode);
  },

  async handleHeaderRunModeSelect(mode) {
    await this.handleModeSelect(mode);
    this.closeHeaderMenu();
  },

  async handleModelSelect(key) {
    if (!this.isConnected || this.streamingMessage) {
      return;
    }
    const policyStore = usePolicyStore();
    if (policyStore.isModelDisabled(key)) {
      this.uiPushToast({
        title: '模型被禁用',
        message: '被管理员强制禁用',
        type: 'warning'
      });
      return;
    }
    const modelStore = useModelStore();
    const targetModel = modelStore.models.find((m) => m.key === key);
    if (this.conversationHasImages && !targetModel?.supportsImage) {
      this.uiPushToast({
        title: '切换失败',
        message: '当前对话包含图片，目标模型不支持图片输入',
        type: 'error'
      });
      return;
    }
    if (this.conversationHasVideos && !targetModel?.supportsVideo) {
      this.uiPushToast({
        title: '切换失败',
        message: '当前对话包含视频，目标模型不支持视频输入',
        type: 'error'
      });
      return;
    }
    const prev = this.currentModelKey;
    try {
      const resp = await fetch('/api/model', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ model_key: key })
      });
      const payload = await resp.json();
      if (!resp.ok || !payload.success) {
        throw new Error(payload.error || payload.message || '切换失败');
      }
      const data = payload.data || {};
      modelStore.setModel(data.model_key || key);
      if (data.run_mode) {
        this.runMode = data.run_mode;
        this.thinkingMode = data.thinking_mode ?? data.run_mode !== 'fast';
      } else {
        // 前端兼容策略：根据模型特性自动调整运行模式
        const currentModel = modelStore.currentModel;
        if (currentModel?.deepOnly) {
          this.runMode = 'deep';
          this.thinkingMode = true;
        } else if (currentModel?.fastOnly) {
          this.runMode = 'fast';
          this.thinkingMode = false;
        } else {
          this.thinkingMode = this.runMode !== 'fast';
        }
      }
      this.uiPushToast({
        title: '模型已切换',
        message: modelStore.currentModel?.label || key,
        type: 'success'
      });
    } catch (error) {
      modelStore.setModel(prev);
      const msg = error instanceof Error ? error.message : String(error || '切换失败');
      this.uiPushToast({
        title: '切换模型失败',
        message: msg,
        type: 'error'
      });
    } finally {
      this.modelMenuOpen = false;
      this.inputCloseMenus();
      this.inputSetQuickMenuOpen(false);
    }
  },

  async handleHeaderModelSelect(key, disabled) {
    if (disabled) return;
    await this.handleModelSelect(key);
    this.closeHeaderMenu();
  },

  async handleCycleRunMode() {
    const modes: Array<'fast' | 'thinking' | 'deep'> = ['fast', 'thinking', 'deep'];
    const currentMode = this.resolvedRunMode;
    const currentIndex = modes.indexOf(currentMode);
    const nextMode = modes[(currentIndex + 1) % modes.length];
    await this.setRunMode(nextMode);
  },

  async setRunMode(mode, options = {}) {
    if (!this.isConnected || this.streamingMessage) {
      this.modeMenuOpen = false;
      return;
    }
    const modelStore = useModelStore();
    const fastOnly = modelStore.currentModel?.fastOnly;
    const deepOnly = modelStore.currentModel?.deepOnly;
    if (fastOnly && mode !== 'fast') {
      if (!options.suppressToast) {
        this.uiPushToast({
          title: '模式不可用',
          message: '当前模型仅支持快速模式',
          type: 'warning'
        });
      }
      this.modeMenuOpen = false;
      this.inputCloseMenus();
      return;
    }
    if (deepOnly && mode !== 'deep') {
      if (!options.suppressToast) {
        this.uiPushToast({
          title: '模式不可用',
          message: '当前模型仅支持深度思考模式',
          type: 'warning'
        });
      }
      this.modeMenuOpen = false;
      this.inputCloseMenus();
      return;
    }
    if (mode === this.resolvedRunMode) {
      this.modeMenuOpen = false;
      this.closeQuickMenu();
      return;
    }
    try {
      const response = await fetch('/api/thinking-mode', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json'
        },
        body: JSON.stringify({ mode })
      });
      const payload = await response.json();
      if (!response.ok || !payload.success) {
        throw new Error(payload.message || payload.error || '切换失败');
      }
      const data = payload.data || {};
      this.thinkingMode =
        typeof data.thinking_mode === 'boolean' ? data.thinking_mode : mode !== 'fast';
      this.runMode = data.mode || mode;
    } catch (error) {
      console.error('切换运行模式失败:', error);
      const message = error instanceof Error ? error.message : String(error || '未知错误');
      this.uiPushToast({
        title: '切换思考模式失败',
        message: message || '请稍后重试',
        type: 'error'
      });
    } finally {
      this.modeMenuOpen = false;
      this.inputCloseMenus();
    }
  },

  getPermissionModeLabel(mode) {
    const options = Array.isArray(this.permissionModeOptions) ? this.permissionModeOptions : [];
    const hit = options.find((item) => item.value === mode);
    return hit ? hit.label : mode || '未知';
  },
  getExecutionModeLabel(mode) {
    const options = Array.isArray(this.executionModeOptions) ? this.executionModeOptions : [];
    const hit = options.find((item) => item.value === mode);
    return hit ? hit.label : mode || '未知';
  },

  togglePermissionMenu() {
    if (!this.isConnected) {
      return;
    }
    this.permissionMenuOpen = !this.permissionMenuOpen;
  },

  closePermissionMenu() {
    this.permissionMenuOpen = false;
  },

  async changePermissionMode(mode) {
    const target = String(mode || '')
      .trim()
      .toLowerCase();
    if (!target) {
      this.closePermissionMenu();
      return;
    }
    try {
      const response = await fetch('/api/permission-mode', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json'
        },
        body: JSON.stringify({ mode: target })
      });
      const payload = await response.json().catch(() => ({}));
      if (!response.ok || !payload?.success) {
        throw new Error(payload?.message || payload?.error || '切换权限失败');
      }
      if (typeof payload?.mode === 'string') {
        this.currentPermissionMode = payload.mode;
      }
      this.pendingPermissionMode = '';
      this.uiPushToast({
        title: '权限已更新',
        message: payload?.message || '已立即生效',
        type: 'info',
        duration: 1800
      });
    } catch (error) {
      const msg = error instanceof Error ? error.message : String(error || '切换权限失败');
      this.uiPushToast({
        title: '切换权限失败',
        message: msg,
        type: 'error'
      });
    } finally {
      this.closePermissionMenu();
    }
  },

  async changeExecutionMode(mode) {
    const target = String(mode || '').trim().toLowerCase();
    if (!this.executionModeEnabled || !target) {
      return;
    }
    try {
      const response = await fetch('/api/execution-mode', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ mode: target })
      });
      const payload = await response.json().catch(() => ({}));
      if (!response.ok || !payload?.success) {
        throw new Error(payload?.message || payload?.error || '切换执行环境失败');
      }
      const state = payload?.state || {};
      if (typeof state.mode === 'string') {
        this.currentExecutionMode = state.mode;
      }
      this.pendingExecutionMode = '';
      this.executionModeDirectUntil = state.direct_until ?? null;
      this.executionModeTtlSeconds =
        typeof state.ttl_seconds === 'number' ? state.ttl_seconds : this.executionModeTtlSeconds;
      this.uiPushToast({
        title: '执行环境已更新',
        message: payload?.message || '已立即生效',
        type: this.currentExecutionMode === 'direct' ? 'warning' : 'info',
        duration: 1800
      });
    } catch (error) {
      const msg = error instanceof Error ? error.message : String(error || '切换执行环境失败');
      this.uiPushToast({
        title: '切换执行环境失败',
        message: msg,
        type: 'error'
      });
    }
  },

  async handleSwitchPermissionToUnrestricted(approvalId) {
    await this.changePermissionMode('unrestricted');
    if (approvalId) {
      await this.approveToolApproval(approvalId);
    }
  },

  async fetchPermissionMode() {
    try {
      const response = await fetch('/api/permission-mode');
      const payload = await response.json().catch(() => ({}));
      if (!response.ok || !payload?.success) {
        return;
      }
      if (typeof payload.mode === 'string') {
        this.currentPermissionMode = payload.mode;
      }
      this.pendingPermissionMode = typeof payload.pending_mode === 'string' ? payload.pending_mode : '';
    } catch (_error) {
      // ignore
    }
  },

  async fetchExecutionMode() {
    try {
      const response = await fetch('/api/execution-mode');
      const payload = await response.json().catch(() => ({}));
      if (!response.ok || !payload?.success) {
        return;
      }
      this.executionModeEnabled = !!payload.enabled;
      const state = payload.state || {};
      if (typeof state.mode === 'string') {
        this.currentExecutionMode = state.mode;
      }
      this.pendingExecutionMode = typeof payload.pending_mode === 'string' ? payload.pending_mode : '';
      this.executionModeDirectUntil = state.direct_until ?? null;
      if (typeof state.ttl_seconds === 'number') {
        this.executionModeTtlSeconds = state.ttl_seconds;
      }
    } catch (_error) {
      // ignore
    }
  },

  async openPathAuthorizationDialog() {
    if (!this.executionModeEnabled) return;
    this.pathAuthorizationDialogOpen = true;
    try {
      const response = await fetch('/api/path-authorization');
      const payload = await response.json().catch(() => ({}));
      if (response.ok && payload?.success) {
        const writablePaths = Array.isArray(payload.writable_paths) ? payload.writable_paths : [];
        const readableExtraPaths = Array.isArray(payload.readable_extra_paths)
          ? payload.readable_extra_paths
          : [];
        this.pathAuthorizationWritableDraft = writablePaths.join('\n');
        this.pathAuthorizationReadableDraft = readableExtraPaths.join('\n');
        this.pathAuthorizationMode = 'writable';
        this.pathAuthorizationDraft = this.pathAuthorizationWritableDraft;
      }
    } catch (_error) {
      // ignore
    }
  },

  setPathAuthorizationMode(mode) {
    if (this.pathAuthorizationMode === 'readable') {
      this.pathAuthorizationReadableDraft = String(this.pathAuthorizationDraft || '');
    } else {
      this.pathAuthorizationWritableDraft = String(this.pathAuthorizationDraft || '');
    }
    const next = mode === 'readable' ? 'readable' : 'writable';
    this.pathAuthorizationMode = next;
    this.pathAuthorizationDraft =
      next === 'readable' ? this.pathAuthorizationReadableDraft : this.pathAuthorizationWritableDraft;
  },

  closePathAuthorizationDialog() {
    this.pathAuthorizationDialogOpen = false;
  },

  async savePathAuthorization() {
    const currentLines = String(this.pathAuthorizationDraft || '')
      .split('\n')
      .map((x) => x.trim())
      .filter(Boolean);
    if (this.pathAuthorizationMode === 'readable') {
      this.pathAuthorizationReadableDraft = currentLines.join('\n');
    } else {
      this.pathAuthorizationWritableDraft = currentLines.join('\n');
    }
    const writableLines = String(this.pathAuthorizationWritableDraft || '')
      .split('\n')
      .map((x) => x.trim())
      .filter(Boolean);
    const readableLines = String(this.pathAuthorizationReadableDraft || '')
      .split('\n')
      .map((x) => x.trim())
      .filter(Boolean);
    this.pathAuthorizationSaving = true;
    try {
      const response = await fetch('/api/path-authorization', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          writable_paths: writableLines,
          readable_extra_paths: readableLines
        })
      });
      const payload = await response.json().catch(() => ({}));
      if (!response.ok || !payload?.success) {
        throw new Error(payload?.error || '保存失败');
      }
      const savedWritable = Array.isArray(payload.writable_paths) ? payload.writable_paths : writableLines;
      const savedReadable = Array.isArray(payload.readable_extra_paths)
        ? payload.readable_extra_paths
        : readableLines;
      this.pathAuthorizationWritableDraft = savedWritable.join('\n');
      this.pathAuthorizationReadableDraft = savedReadable.join('\n');
      this.pathAuthorizationDraft =
        this.pathAuthorizationMode === 'readable'
          ? this.pathAuthorizationReadableDraft
          : this.pathAuthorizationWritableDraft;
      this.uiPushToast({
        title: '路径授权已保存',
        message: '命令工具立即生效；终端会话请重开后生效',
        type: 'success'
      });
      this.pathAuthorizationDialogOpen = false;
    } catch (error) {
      const msg = error instanceof Error ? error.message : String(error || '保存失败');
      this.uiPushToast({ title: '保存路径授权失败', message: msg, type: 'error' });
    } finally {
      this.pathAuthorizationSaving = false;
    }
  },

  async fetchPendingToolApprovals() {
    if (!this.currentConversationId) {
      this.pendingToolApprovals = [];
      return;
    }
    try {
      const response = await fetch(
        `/api/tool-approvals/pending?conversation_id=${encodeURIComponent(this.currentConversationId)}`
      );
      const payload = await response.json().catch(() => ({}));
      if (!response.ok || !payload?.success) {
        return;
      }
      const items = Array.isArray(payload.items) ? payload.items : [];
      this.pendingToolApprovals = items;
      // 电脑端：有审批时自动展开面板
      if (items.length > 0 && !this.isMobileViewport) {
        this.rightCollapsed = false;
        if (this.rightWidth < this.minPanelWidth) {
          this.rightWidth = this.minPanelWidth;
        }
      }
    } catch (_error) {
      // ignore
    }
  },

  async decideToolApproval(approvalId, decision) {
    const id = String(approvalId || '').trim();
    if (!id) {
      return;
    }
    if (!Array.isArray(this.decidingApprovalIds)) {
      this.decidingApprovalIds = [];
    }
    if (!this.decidingApprovalIds.includes(id)) {
      this.decidingApprovalIds.push(id);
    }
    try {
      const response = await fetch(`/api/tool-approvals/${encodeURIComponent(id)}/decision`, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json'
        },
        body: JSON.stringify({ decision })
      });
      const payload = await response.json().catch(() => ({}));
      if (!response.ok || !payload?.success) {
        throw new Error(payload?.message || payload?.error || '提交审批失败');
      }
      this.pendingToolApprovals = (this.pendingToolApprovals || []).filter(
        (item) => item && item.approval_id !== id
      );
      if (!this.pendingToolApprovals.length) {
        this.rightCollapsed = true;
        if (this.isMobileViewport && this.activeMobileOverlay === 'approval') {
          this.closeMobileOverlay();
        }
      }
    } catch (error) {
      const msg = error instanceof Error ? error.message : String(error || '审批失败');
      this.uiPushToast({
        title: '审批失败',
        message: msg,
        type: 'error'
      });
    } finally {
      this.decidingApprovalIds = (this.decidingApprovalIds || []).filter((item) => item !== id);
    }
  },

  approveToolApproval(approvalId) {
    return this.decideToolApproval(approvalId, 'approved');
  },

  rejectToolApproval(approvalId) {
    return this.decideToolApproval(approvalId, 'rejected');
  },

  handleInputChange() {
    this.autoResizeInput();
    this.scheduleComposerDraftPersist('input-change');
  },

  handleInputFocus() {
    this.inputSetFocused(true);
    this.closeQuickMenu();
    this.closePermissionMenu();
  },

  handleInputBlur() {
    this.inputSetFocused(false);
  },

  handleRealtimeTerminalClick() {
    if (!this.isConnected) {
      return;
    }
    if (this.isPolicyBlocked('block_realtime_terminal', '实时终端已被管理员禁用')) {
      return;
    }
    this.openRealtimeTerminal();
  },

  handleFocusPanelToggleClick() {
    if (!this.isConnected) {
      return;
    }
    if (this.isPolicyBlocked('block_focus_panel', '聚焦面板已被管理员禁用')) {
      return;
    }
    this.toggleFocusPanel();
  },

  handleApprovalPanelToggleClick() {
    console.log(
      '[UI_DEBUG] handleApprovalPanelToggleClick called, isMobileViewport:',
      this.isMobileViewport,
      'currentConversationId:',
      this.currentConversationId,
      'activeMobileOverlay:',
      this.activeMobileOverlay
    );
    if (!this.currentConversationId) {
      console.log('[UI_DEBUG] handleApprovalPanelToggleClick: blocked - no conversation');
      return;
    }
    if (this.isMobileViewport) {
      console.log('[UI_DEBUG] handleApprovalPanelToggleClick: mobile path, toggling overlay');
      // 手机端：获取审批列表（不自动关闭），然后切换遮罩
      this.fetchPendingToolApprovals();
      this.openMobileOverlay('approval');
      return;
    }
    console.log('[UI_DEBUG] handleApprovalPanelToggleClick: desktop path, toggling panel');
    this.toggleApprovalPanel();
  },

  handleTokenPanelToggleClick(fromSettingsMenu = false) {
    console.log(
      '[UI_DEBUG] handleTokenPanelToggleClick called, fromSettingsMenu:',
      fromSettingsMenu,
      'isMobileViewport:',
      this.isMobileViewport,
      'tokenPanelCollapsed:',
      this.tokenPanelCollapsed,
      'currentConversationId:',
      this.currentConversationId
    );
    if (!this.currentConversationId) {
      console.log('[UI_DEBUG] handleTokenPanelToggleClick: blocked - no conversation');
      return;
    }
    if (this.isPolicyBlocked('block_token_panel', '用量统计已被管理员禁用')) {
      console.log('[UI_DEBUG] handleTokenPanelToggleClick: blocked - policy');
      return;
    }
    // 移动端禁用“点击展开顶部用量面板”，仅允许在已展开时点击收起
    if (this.isMobileViewport && this.tokenPanelCollapsed && !fromSettingsMenu) {
      console.log('[UI_DEBUG] handleTokenPanelToggleClick: blocked - mobile viewport restriction');
      return;
    }
    console.log('[UI_DEBUG] handleTokenPanelToggleClick: calling toggleTokenPanel');
    this.toggleTokenPanel();
  },

  handleCompressConversationClick() {
    if (this.compressing || this.streamingMessage || !this.isConnected) {
      return;
    }
    if (this.compressionInProgress) {
      this.uiPushToast({
        title: '对话自动压缩中',
        message: '当前对话正在压缩，请稍后再试',
        type: 'warning'
      });
      return;
    }
    if (this.isPolicyBlocked('block_compress_conversation', '压缩对话已被管理员禁用')) {
      return;
    }
    this.compressConversation();
  },

  openReviewDialog() {
    if (this.isPolicyBlocked('block_conversation_review', '对话引用已被管理员禁用')) {
      return;
    }
    if (!this.isConnected) {
      this.uiPushToast({
        title: '无法使用',
        message: '当前未连接，无法生成回顾文件',
        type: 'warning'
      });
      return;
    }
    if (!this.conversations.length && !this.conversationsLoading) {
      this.loadConversationsList();
    }
    const fallback = this.conversations.find((c) => c.id !== this.currentConversationId);
    if (!fallback) {
      this.uiPushToast({
        title: '暂无可用对话',
        message: '没有可供回顾的其他对话记录',
        type: 'info'
      });
      return;
    }
    this.reviewSelectedConversationId = fallback.id;
    this.reviewDialogOpen = true;
    this.reviewPreviewLines = [];
    this.reviewPreviewError = null;
    this.reviewGeneratedPath = null;
    this.loadReviewPreview(fallback.id);
    this.closeQuickMenu();
  },

  closeHeaderMenu() {
    this.headerMenuOpen = false;
  },

  handleReviewSelect(id) {
    if (id === this.currentConversationId) {
      this.uiPushToast({
        title: '无法引用当前对话',
        message: '请选择其他对话生成回顾',
        type: 'warning'
      });
      return;
    }
    this.reviewSelectedConversationId = id;
    this.loadReviewPreview(id);
  },

  async handleConfirmReview() {
    if (this.reviewSubmitting) return;
    if (!this.reviewSelectedConversationId) {
      this.uiPushToast({
        title: '请选择对话',
        message: '请选择要生成回顾的对话记录',
        type: 'info'
      });
      return;
    }
    if (this.reviewSelectedConversationId === this.currentConversationId) {
      this.uiPushToast({
        title: '无法引用当前对话',
        message: '请选择其他对话生成回顾',
        type: 'warning'
      });
      return;
    }
    if (!this.currentConversationId) {
      this.uiPushToast({
        title: '无法发送',
        message: '当前没有活跃对话，无法自动发送提示消息',
        type: 'warning'
      });
      return;
    }

    this.reviewSubmitting = true;
    try {
      const { path, char_count } = await this.generateConversationReview(
        this.reviewSelectedConversationId
      );
      if (!path) {
        throw new Error('未获取到生成的文件路径');
      }
      const count = typeof char_count === 'number' ? char_count : 0;
      this.reviewGeneratedPath = path;
      const suggestion =
        count && count <= 10000 ? '建议直接完整阅读。' : '建议使用 read 工具进行搜索或分段阅读。';
      if (this.reviewSendToModel) {
        const message = `帮我继续这个任务，对话文件在 ${path}，文件长 ${count || '未知'} 字符，${suggestion} 请阅读文件了解后，不要直接继续工作，而是向我汇报你的理解，然后等我做出指示。`;
        const sent = await this.sendAutoUserMessage(message);
        if (sent) {
          this.reviewDialogOpen = false;
        }
      } else {
        this.uiPushToast({
          title: '回顾文件已生成',
          message: path,
          type: 'success'
        });
      }
    } catch (error) {
      const msg = error instanceof Error ? error.message : String(error || '生成失败');
      this.uiPushToast({
        title: '生成回顾失败',
        message: msg,
        type: 'error'
      });
    } finally {
      this.reviewSubmitting = false;
    }
  },

  async loadReviewPreview(conversationId) {
    this.reviewPreviewLoading = true;
    this.reviewPreviewError = null;
    this.reviewPreviewLines = [];
    try {
      const resp = await fetch(
        `/api/conversations/${conversationId}/review_preview?limit=${this.reviewPreviewLimit}`
      );
      const payload = await resp.json().catch(() => ({}));
      if (!resp.ok || !payload?.success) {
        const msg = payload?.message || payload?.error || '获取预览失败';
        throw new Error(msg);
      }
      this.reviewPreviewLines = payload?.data?.preview || [];
    } catch (error) {
      const msg = error instanceof Error ? error.message : String(error || '获取预览失败');
      this.reviewPreviewError = msg;
    } finally {
      this.reviewPreviewLoading = false;
    }
  },

  async generateConversationReview(conversationId) {
    const response = await fetch(`/api/conversations/${conversationId}/review`, {
      method: 'POST'
    });
    const payload = await response.json().catch(() => ({}));
    if (!response.ok || !payload?.success) {
      const msg = payload?.message || payload?.error || '生成失败';
      throw new Error(msg);
    }
    const data = payload.data || payload;
    return {
      path: data.path || data.file_path || data.relative_path,
      char_count: data.char_count ?? data.length ?? data.size ?? 0
    };
  },

  handleClickOutsideQuickMenu(event) {
    if (!this.quickMenuOpen && !this.permissionMenuOpen) {
      return;
    }
    const shell =
      this.getComposerElement('stadiumShellOuter') || this.getComposerElement('compactInputShell');
    if (shell && shell.contains(event.target)) {
      return;
    }
    this.closeQuickMenu();
    this.closePermissionMenu();
  },

  handleClickOutsideHeaderMenu(event) {
    if (!this.headerMenuOpen) return;
    const ribbon = this.$refs.titleRibbon as HTMLElement | undefined;
    const menu = this.$refs.headerMenu as HTMLElement | undefined;
    if ((ribbon && ribbon.contains(event.target)) || (menu && menu.contains(event.target))) {
      return;
    }
    this.closeHeaderMenu();
  },

  handleClickOutsidePanelMenu(event) {
    if (!this.panelMenuOpen) {
      return;
    }
    const leftPanel = this.$refs.leftPanel;
    const wrapper = leftPanel && leftPanel.panelMenuWrapper ? leftPanel.panelMenuWrapper : null;
    if (wrapper && wrapper.contains(event.target)) {
      return;
    }
    this.uiSetPanelMenuOpen(false);
  },

  isConversationBlank() {
    if (!Array.isArray(this.messages) || !this.messages.length) return true;
    return !this.messages.some((msg) => msg && (msg.role === 'user' || msg.role === 'assistant'));
  },

  pickWelcomeText() {
    const pool = this.blankWelcomePool;
    if (!Array.isArray(pool) || !pool.length) {
      this.blankWelcomeText = '有什么可以帮忙的？';
      return;
    }
    const idx = Math.floor(Math.random() * pool.length);
    this.blankWelcomeText = pool[idx];
  },

  refreshBlankHeroState() {
    const isBlank = this.isConversationBlank();
    const currentConv = this.currentConversationId || 'temp';
    const needNewWelcome = !this.blankHeroActive || this.lastBlankConversationId !== currentConv;

    if (isBlank) {
      if (needNewWelcome && !this.blankHeroExiting) {
        this.pickWelcomeText();
      }
      this.blankHeroActive = true;
      this.lastBlankConversationId = currentConv;
    } else {
      this.blankHeroActive = false;
      this.blankHeroExiting = false;
      this.lastBlankConversationId = null;
    }
  },

  toggleSettings() {
    if (!this.isConnected) {
      return;
    }
    this.modeMenuOpen = false;
    this.modelMenuOpen = false;
    const nextState = this.inputToggleSettingsMenu();
    if (nextState) {
      this.inputSetToolMenuOpen(false);
      if (!this.quickMenuOpen) {
        this.inputOpenQuickMenu();
      }
    }
  },

  toggleFocusPanel() {
    this.rightCollapsed = !this.rightCollapsed;
    if (!this.rightCollapsed && this.rightWidth < this.minPanelWidth) {
      this.rightWidth = this.minPanelWidth;
    }
  },

  toggleApprovalPanel() {
    console.log(
      '[UI_DEBUG] toggleApprovalPanel called, current rightCollapsed:',
      this.rightCollapsed,
      'new state:',
      !this.rightCollapsed
    );
    this.rightCollapsed = !this.rightCollapsed;
    if (!this.rightCollapsed && this.rightWidth < this.minPanelWidth) {
      this.rightWidth = this.minPanelWidth;
    }
    if (!this.rightCollapsed) {
      this.fetchPendingToolApprovals();
    }
  },

  addSystemMessage(content) {
    userMDebug('ui.addSystemMessage:incoming', { content });
    const systemNoticeLabel = parseSystemNoticeLabel(content);
    if (!systemNoticeLabel) {
      // 其他 system 消息全部隐藏，且不进入渲染链路，避免出现空白块
      userMDebug('ui.addSystemMessage:blocked', { content });
      return;
    }
    userMDebug('ui.addSystemMessage:accepted', {
      original: content,
      normalizedLabel: systemNoticeLabel
    });
    this.chatAddSystemMessage(systemNoticeLabel, { variant: 'sub_agent_done' });
    this.$forceUpdate();
    this.conditionalScrollToBottom();
  },

  appendSystemAction(content) {
    this.addSystemMessage(content);
  },

  startTitleTyping(title: string, options: { animate?: boolean } = {}) {
    if (this.titleTypingTimer) {
      clearInterval(this.titleTypingTimer);
      this.titleTypingTimer = null;
    }
    const target = (title || '').trim();
    const animate = options.animate ?? true;
    if (!animate) {
      this.titleTypingTarget = target;
      this.titleTypingText = target;
      return;
    }
    const previous = (this.titleTypingText || '').trim();
    if (previous === target) {
      this.titleTypingTarget = target;
      this.titleTypingText = target;
      return;
    }
    this.titleTypingTarget = target;
    this.titleTypingText = previous;

    const frames: string[] = [];
    for (let i = previous.length; i >= 0; i--) {
      frames.push(previous.slice(0, i));
    }
    for (let j = 1; j <= target.length; j++) {
      frames.push(target.slice(0, j));
    }

    let index = 0;
    this.titleTypingTimer = window.setInterval(() => {
      if (index >= frames.length) {
        clearInterval(this.titleTypingTimer!);
        this.titleTypingTimer = null;
        this.titleTypingText = target;
        return;
      }
      this.titleTypingText = frames[index];
      index += 1;
    }, 32);
  },

  toggleBlock(blockId) {
    if (!blockId) {
      return;
    }
    if (this.expandedBlocks && this.expandedBlocks.has(blockId)) {
      this.chatCollapseBlock(blockId);
    } else {
      this.chatExpandBlock(blockId);
    }
  },

  handleThinkingScroll(blockId, event) {
    if (!blockId || !event || !event.target) {
      return;
    }
    const el = event.target;
    const threshold = 12;
    const atBottom = el.scrollHeight - el.scrollTop - el.clientHeight < threshold;
    this.chatSetThinkingLock(blockId, atBottom);
  },

  scrollToBottom() {
    uiBounceTrace(
      'ui.scrollToBottom:called',
      {
        autoScrollEnabled: this.autoScrollEnabled,
        userScrolling: this.userScrolling
      },
      'ui.scrollToBottom:called',
      80
    );
    this._escapedByUserScroll = false;
    const chatArea = this.getChatAreaController();
    if (chatArea && typeof chatArea.scrollToBottom === 'function') {
      chatArea.scrollToBottom({
        behavior: 'auto',
        force: this.autoScrollEnabled
      });
      this.chatSetScrollState({ userScrolling: false });
      return;
    }
    scrollToBottomHelper(this);
  },

  conditionalScrollToBottom() {
    const active = typeof this.isOutputActive === 'function' ? this.isOutputActive() : true;
    const chatArea = this.getChatAreaController();
    const stickState =
      chatArea && typeof chatArea.getStickState === 'function' ? chatArea.getStickState() : null;
    uiBounceTrace(
      'ui.conditionalScrollToBottom:called',
      {
        active,
        autoScrollEnabled: this.autoScrollEnabled,
        userScrolling: this.userScrolling,
        stickIsNearBottom: this.stickIsNearBottom,
        escapedFromLock: !!stickState?.escapedFromLock
      },
      'ui.conditionalScrollToBottom:called',
      120
    );
    if (!active) {
      return;
    }
    const now = Date.now();
    if (now <= (this._manualScrollSuppressUntil || 0)) {
      uiBounceTrace(
        'ui.conditionalScrollToBottom:skip-manual-cooldown',
        {
          now,
          suppressUntil: this._manualScrollSuppressUntil
        },
        'ui.conditionalScrollToBottom:skip-manual-cooldown',
        120
      );
      return;
    }
    // 仅在“确认是用户主动脱离锁定”时才阻断自动追底；
    // 对于内容高度突变导致的 escapedFromLock，不应长期卡住锁定。
    if (stickState?.escapedFromLock && this._escapedByUserScroll) {
      uiBounceTrace(
        'ui.conditionalScrollToBottom:skip-escaped-lock',
        {
          escapedFromLock: !!stickState?.escapedFromLock,
          escapedByUser: !!this._escapedByUserScroll,
          isNearBottom: !!stickState?.isNearBottom,
          isAtBottom: !!stickState?.isAtBottom
        },
        'ui.conditionalScrollToBottom:skip-escaped-lock',
        120
      );
      return;
    }
    // 只要用户处于手动滚动状态，就停止自动追底，避免“被弹回”
    // （stickIsNearBottom 在 show_html 动态重排时可能短暂失真，不可作为阻断前置条件）
    if (this.userScrolling) {
      uiBounceTrace(
        'ui.conditionalScrollToBottom:skip-user-scrolling',
        {
          userScrolling: this.userScrolling,
          stickIsNearBottom: this.stickIsNearBottom
        },
        'ui.conditionalScrollToBottom:skip-user-scrolling',
        120
      );
      return;
    }
    if (chatArea && typeof chatArea.conditionalStickToBottom === 'function') {
      chatArea.conditionalStickToBottom({ force: false });
      return;
    }
    conditionalScrollToBottomHelper(this);
  },

  toggleScrollLock() {
    uiBounceTrace(
      'ui.scrollToBottomButton:clicked',
      {
        autoScrollEnabled: this.autoScrollEnabled,
        userScrolling: this.userScrolling,
        stickNearBottom: this.stickIsNearBottom
      },
      'ui.scrollToBottomButton:clicked',
      0
    );
    const chatArea = this.getChatAreaController();
    if (chatArea && typeof chatArea.scrollToBottom === 'function') {
      chatArea.scrollToBottom({ behavior: 'smooth', force: true });
    } else {
      scrollToBottomHelper(this, {
        ignoreUserScrolling: true,
        resetUserScrolling: true,
        behavior: 'smooth',
        force: true
      });
    }
    this._escapedByUserScroll = false;
    this.chatSetScrollState({ autoScrollEnabled: true, userScrolling: false });
    return true;
  },

  scrollThinkingToBottom(blockId) {
    scrollThinkingToBottomHelper(this, blockId);
  },

  // 面板调整方法
  startResize(panel, event) {
    startPanelResize(this, panel, event);
  },

  handleResize(event) {
    handlePanelResize(this, event);
  },

  stopResize() {
    stopPanelResize(this);
  },

  getInputComposerRef() {
    return this.$refs.inputComposer || null;
  },

  handleComposerHeightChange(payload = {}) {
    const raw =
      Number(payload?.reservedHeight || 0) ||
      Number(payload?.height || 0) ||
      Number(payload?.composerHeight || 0);
    if (!Number.isFinite(raw) || raw <= 0) {
      return;
    }
    const next = Math.max(80, Math.min(560, Math.round(raw)));
    if (Math.abs(next - Number(this.composerReservedHeight || 0)) < 1) {
      return;
    }
    this.composerReservedHeight = next;
  },

  getComposerElement(field) {
    const composer = this.getInputComposerRef();
    const unwrap = (value: any) => {
      if (!value) {
        return null;
      }
      if (value instanceof HTMLElement) {
        return value;
      }
      if (typeof value === 'object' && 'value' in value && !(value instanceof Window)) {
        return value.value;
      }
      return value;
    };
    if (composer && composer[field]) {
      return unwrap(composer[field]);
    }
    if (this.$refs && this.$refs[field]) {
      return unwrap(this.$refs[field]);
    }
    return null;
  },

  getMessagesAreaElement() {
    const ref = this.$refs.messagesArea;
    if (!ref) {
      return null;
    }
    if (ref instanceof HTMLElement) {
      return ref;
    }
    if (ref.rootEl) {
      return ref.rootEl.value || ref.rootEl;
    }
    if (ref.$el && ref.$el.querySelector) {
      const el = ref.$el.querySelector('.messages-area');
      if (el) {
        return el;
      }
    }
    return null;
  },

  getChatAreaController() {
    const ref = this.$refs.messagesArea;
    if (!ref) return null;
    if (typeof ref === 'object') return ref;
    return null;
  },

  getThinkingContentElement(blockId) {
    const chatArea = this.$refs.messagesArea;
    if (chatArea && typeof chatArea.getThinkingRef === 'function') {
      const el = chatArea.getThinkingRef(blockId);
      if (el) {
        return el;
      }
    }
    const refName = `thinkingContent-${blockId}`;
    const elRef = this.$refs[refName];
    if (Array.isArray(elRef)) {
      return elRef[0] || null;
    }
    return elRef || null;
  },

  isOutputActive() {
    return !!(this.streamingMessage || this.taskInProgress || this.hasPendingToolActions());
  },

  logMessageState(action, extra = {}) {
    const count = Array.isArray(this.messages) ? this.messages.length : 'N/A';
    debugLog('[Messages]', {
      action,
      count,
      conversationId: this.currentConversationId,
      streaming: this.streamingMessage,
      ...extra
    });
  },

  iconStyle(iconKey, size) {
    const iconPath = this.icons ? this.icons[iconKey] : null;
    if (!iconPath) {
      return {};
    }
    const style = { '--icon-src': `url(${iconPath})` };
    if (size) {
      style['--icon-size'] = size;
    }
    return style;
  },

  openGuiFileManager() {
    if (this.isPolicyBlocked('block_file_manager', '文件管理器已被管理员禁用')) {
      return;
    }
    window.open('/file-manager', '_blank');
  },

  renderMarkdown(content, isStreaming = false) {
    return renderMarkdownHelper(content, isStreaming);
  },

  isExplicitNewConversationRoute() {
    const normalizedPath = window.location.pathname.replace(/^\/+|\/+$/g, '');
    return normalizedPath === 'new';
  },

  async getRunningTaskConversationId() {
    try {
      const resp = await fetch('/api/tasks');
      if (!resp.ok) {
        return null;
      }
      const payload = await resp.json();
      const tasks = Array.isArray(payload?.data) ? payload.data : [];
      const runningTask = tasks.find(
        (task: any) => task?.status === 'running' && task?.conversation_id
      );
      return runningTask?.conversation_id || null;
    } catch (error) {
      console.warn('获取运行中任务失败:', error);
      return null;
    }
  },

  async bootstrapRoute() {
    // 在路由解析期间抑制标题动画，避免预置"新对话"闪烁
    this.suppressTitleTyping = true;
    this.titleReady = false;
    this.currentConversationTitle = '';
    this.titleTypingText = '';
    const path = window.location.pathname.replace(/^\/+/, '');
    if (!path || this.isExplicitNewConversationRoute()) {
      this.currentConversationId = null;
      this.currentConversationTitle = '新对话';
      this.logMessageState('bootstrapRoute:clear-messages-for-new');
      this.messages = [];
      this.titleReady = true;
      this.suppressTitleTyping = false;
      this.startTitleTyping('新对话', { animate: false });
      this.initialRouteResolved = true;
      this.refreshBlankHeroState();
      await this.restoreComposerDraftState('bootstrap-route:new');
      return;
    }

    const convId = path.startsWith('conv_') ? path : `conv_${path}`;
    try {
      const resp = await fetch(`/api/conversations/${convId}/load`, { method: 'PUT' });
      const result = await resp.json();
      if (result.success) {
        if (typeof result.run_mode === 'string') {
          this.runMode = result.run_mode;
          this.thinkingMode =
            typeof result.thinking_mode === 'boolean'
              ? result.thinking_mode
              : result.run_mode !== 'fast';
        } else if (typeof result.thinking_mode === 'boolean') {
          this.thinkingMode = result.thinking_mode;
          this.runMode = result.thinking_mode ? 'thinking' : 'fast';
        }
        if (typeof result.model_key === 'string' && result.model_key) {
          this.modelSet(result.model_key);
        }
        this.currentConversationId = convId;
        this.currentConversationTitle = result.title || '';
        this.titleReady = true;
        this.suppressTitleTyping = false;
        this.startTitleTyping(this.currentConversationTitle, { animate: false });
        history.replaceState(
          { conversationId: convId },
          '',
          `/${this.stripConversationPrefix(convId)}`
        );
      } else {
        history.replaceState({}, '', '/new');
        this.currentConversationId = null;
        this.currentConversationTitle = '新对话';
        this.titleReady = true;
        this.suppressTitleTyping = false;
        this.startTitleTyping('新对话', { animate: false });
      }
    } catch (error) {
      console.warn('初始化路由失败:', error);
      history.replaceState({}, '', '/new');
      this.currentConversationId = null;
      this.currentConversationTitle = '新对话';
      this.titleReady = true;
      this.suppressTitleTyping = false;
      this.startTitleTyping('新对话', { animate: false });
    } finally {
      this.initialRouteResolved = true;
    }
    await this.restoreComposerDraftState('bootstrap-route:conversation');
  },

  handlePopState(event) {
    const state = event.state || {};
    const convId = state.conversationId;
    if (!convId) {
      this.currentConversationId = null;
      this.currentConversationTitle = '新对话';
      this.logMessageState('handlePopState:clear-messages-no-conversation');
      this.messages = [];
      this.logMessageState('handlePopState:after-clear-no-conversation');
      this.resetAllStates('handlePopState:no-conversation');
      this.resetTokenStatistics();
      this.restoreComposerDraftState('popstate:new').catch(() => {});
      return;
    }
    this.loadConversation(convId);
  },

  stripConversationPrefix(conversationId) {
    if (!conversationId) return '';
    return conversationId.startsWith('conv_') ? conversationId.slice(5) : conversationId;
  },

  initScrollListener() {
    const chatArea = this.getChatAreaController();
    if (
      chatArea &&
      typeof chatArea.isUsingStickToBottom === 'function' &&
      chatArea.isUsingStickToBottom()
    ) {
      this._scrollListenerReady = true;
      return;
    }
    const messagesArea = this.getMessagesAreaElement();
    if (!messagesArea) {
      console.warn('消息区域未找到');
      return;
    }
    this._scrollListenerReady = true;
    let scrollDebugCount = 0;
    const SCROLL_DEBUG_MAX = 1200;
    let scrollDebugLastTs = 0;
    const isScrollDebugEnabled = () => {
      if (typeof window === 'undefined') return false;
      const until = Number((window as any).__SHOW_TAG_DRAWING_UNTIL__ || 0);
      return Date.now() <= until;
    };
    const scrollDebug = (event, payload = {}, throttleMs = 80) => {
      if (!isScrollDebugEnabled()) {
        return;
      }
      if (scrollDebugCount >= SCROLL_DEBUG_MAX) {
        return;
      }
      const now = Date.now();
      if (throttleMs > 0 && now - scrollDebugLastTs < throttleMs) {
        return;
      }
      scrollDebugLastTs = now;
      scrollDebugCount += 1;
      if (scrollDebugCount === SCROLL_DEBUG_MAX) {
        console.warn('[SCROLL_DEBUG]', 'listener:log-limit-reached', { max: SCROLL_DEBUG_MAX });
        return;
      }
      console.log('[SCROLL_DEBUG]', event, payload);
    };
    scrollDebug(
      'listener:setup',
      {
        autoScrollEnabled: this.autoScrollEnabled,
        userScrolling: this.userScrolling
      },
      0
    );

    let isProgrammaticScroll = false;
    const bottomThreshold = 12;

    this._setScrollingFlag = (flag) => {
      isProgrammaticScroll = !!flag;
      scrollDebug('listener:programmatic-flag', { flag: !!flag }, 0);
    };

    messagesArea.addEventListener('scroll', () => {
      if (isProgrammaticScroll) {
        scrollDebug(
          'listener:scroll:ignore-programmatic',
          {
            top: messagesArea.scrollTop,
            height: messagesArea.scrollHeight,
            client: messagesArea.clientHeight
          },
          120
        );
        return;
      }

      const scrollTop = messagesArea.scrollTop;
      const scrollHeight = messagesArea.scrollHeight;
      const clientHeight = messagesArea.clientHeight;
      const isAtBottom = scrollHeight - scrollTop - clientHeight < bottomThreshold;
      const nearBottomThreshold = 48;
      const isNearBottom = scrollHeight - scrollTop - clientHeight < nearBottomThreshold;
      const active = typeof this.isOutputActive === 'function' ? this.isOutputActive() : true;
      if (this.autoScrollEnabled && active) {
        // 锁定模式下不让 userScrolling 状态来回抖动
        this.chatSetScrollState({ userScrolling: false });
        scrollDebug('listener:scroll:locked-keep-bottom-state', {
          top: scrollTop,
          height: scrollHeight,
          client: clientHeight,
          remain: scrollHeight - scrollTop - clientHeight,
          isAtBottom,
          isNearBottom
        });
        return;
      }
      // 仅维护状态，不在 scroll 事件中强制写 scrollTop（避免上下抖动）
      this.chatSetScrollState({ userScrolling: !(isAtBottom || isNearBottom) });
      scrollDebug('listener:scroll:update-state', {
        top: scrollTop,
        height: scrollHeight,
        client: clientHeight,
        remain: scrollHeight - scrollTop - clientHeight,
        isAtBottom,
        isNearBottom,
        autoScrollEnabled: this.autoScrollEnabled,
        userScrollingBefore: this.userScrolling,
        userScrollingNext: !(isAtBottom || isNearBottom)
      });
    });
  },

  async initSocket() {
    // 主网页端已切换为 REST + 轮询模式，这里保留空实现用于兼容旧调用
    this.socket = null;
  },

  async checkConnectionHealth() {
    if (this.connectionHeartbeatInFlight) {
      connectionDiag('log', 'health-skip-inflight', {
        seq: this.connectionHeartbeatSeq,
        isConnected: !!this.isConnected,
        failCount: this.connectionHeartbeatFailCount
      });
      return;
    }
    this.connectionHeartbeatInFlight = true;
    const seq = Number(this.connectionHeartbeatSeq || 0) + 1;
    this.connectionHeartbeatSeq = seq;
    const requestId = `${Date.now()}-${seq}`;
    const startedAt = Date.now();
    const diagEnabled = isConnectionDiagEnabled();
    const healthUrl = diagEnabled ? '/api/health?diag=1' : '/api/health';
    const wasConnected = !!this.isConnected;
    let responseStatus: number | null = null;
    const controller = typeof AbortController !== 'undefined' ? new AbortController() : null;
    const timeoutMs =
      typeof this.connectionHeartbeatRequestTimeoutMs === 'number' &&
      this.connectionHeartbeatRequestTimeoutMs > 0
        ? this.connectionHeartbeatRequestTimeoutMs
        : 5000;
    const timeoutId = controller ? window.setTimeout(() => controller.abort(), timeoutMs) : null;
    try {
      const response = await fetch(healthUrl, {
        method: 'GET',
        cache: 'no-store',
        headers: {
          'X-Connection-Heartbeat': requestId
        },
        signal: controller?.signal
      });
      responseStatus = response.status;
      if (!response.ok) {
        throw new Error(`HTTP ${response.status}`);
      }
      const failCountBeforeRecover = this.connectionHeartbeatFailCount || 0;
      this.isConnected = true;
      this.connectionHeartbeatFailCount = 0;
      this.connectionHeartbeatLastLatencyMs = Date.now() - startedAt;
      this.connectionHeartbeatLastStatusCode = responseStatus;
      this.connectionHeartbeatLastError = '';
      if (!wasConnected) {
        this.connectionHeartbeatLastChangeAt = Date.now();
        connectionDiag(
          'warn',
          'health-recovered',
          {
            requestId,
            seq,
            elapsedMs: this.connectionHeartbeatLastLatencyMs,
            failCountBeforeRecover,
            status: responseStatus,
            diagEnabled,
            endpoint: healthUrl,
            conversationId: this.currentConversationId || null,
            taskInProgress: !!this.taskInProgress,
            streamingMessage: !!this.streamingMessage,
            visibility: document?.visibilityState || 'unknown',
            online: typeof navigator !== 'undefined' ? navigator.onLine : null
          },
          { force: true }
        );
      } else if (
        isConnectionDiagEnabled() &&
        (this.connectionHeartbeatLastLatencyMs >= 700 || seq <= 3 || seq % 60 === 0)
      ) {
        connectionDiag('log', 'health-ok', {
          requestId,
          seq,
          elapsedMs: this.connectionHeartbeatLastLatencyMs,
          status: responseStatus,
          endpoint: healthUrl,
          visibility: document?.visibilityState || 'unknown'
        });
      }
    } catch (error) {
      const nextFailCount = (this.connectionHeartbeatFailCount || 0) + 1;
      this.connectionHeartbeatFailCount = nextFailCount;
      this.connectionHeartbeatLastLatencyMs = Date.now() - startedAt;
      this.connectionHeartbeatLastStatusCode = responseStatus;
      const errName = error?.name || 'Error';
      const errMessage = error?.message || String(error);
      this.connectionHeartbeatLastError = `${errName}: ${errMessage}`;
      const failThreshold =
        typeof this.connectionHeartbeatFailThreshold === 'number' &&
        this.connectionHeartbeatFailThreshold > 0
          ? this.connectionHeartbeatFailThreshold
          : 3;
      const shouldDisconnect = nextFailCount >= failThreshold;
      // 改为连续失败阈值后再置灰，避免偶发请求超时导致“假断连”
      if (shouldDisconnect) {
        this.isConnected = false;
      }
      if (wasConnected && shouldDisconnect) {
        this.connectionHeartbeatLastChangeAt = Date.now();
      }
      const shouldLogFail =
        wasConnected || nextFailCount <= failThreshold || nextFailCount % 10 === 0;
      connectionDiag(
        shouldLogFail ? 'warn' : 'log',
        'health-failed',
        {
          requestId,
          seq,
          elapsedMs: this.connectionHeartbeatLastLatencyMs,
          failCount: nextFailCount,
          status: responseStatus,
          endpoint: healthUrl,
          diagEnabled,
          wasConnected,
          nowConnected: !!this.isConnected,
          failThreshold,
          shouldDisconnect,
          errorName: errName,
          errorMessage: errMessage,
          timeout: errName === 'AbortError',
          visibility: document?.visibilityState || 'unknown',
          online: typeof navigator !== 'undefined' ? navigator.onLine : null,
          conversationId: this.currentConversationId || null,
          taskInProgress: !!this.taskInProgress,
          streamingMessage: !!this.streamingMessage,
          hasPendingTools:
            typeof this.hasPendingToolActions === 'function' ? this.hasPendingToolActions() : null
        },
        { force: shouldLogFail }
      );
    } finally {
      this.connectionHeartbeatInFlight = false;
      if (timeoutId) {
        clearTimeout(timeoutId);
      }
    }
  },

  startConnectionHeartbeat() {
    if (this.connectionHeartbeatActive) {
      return;
    }
    this.connectionHeartbeatActive = true;
    this.connectionHeartbeatFailCount = 0;
    connectionDiag(
      'log',
      'heartbeat-start',
      {
        connectedIntervalMs: this.connectionHeartbeatIntervalMs,
        disconnectedIntervalMs: this.connectionHeartbeatDisconnectedIntervalMs
      },
      { force: true }
    );
    const runHeartbeat = async () => {
      if (!this.connectionHeartbeatActive) {
        return;
      }
      await this.checkConnectionHealth();
      if (!this.connectionHeartbeatActive) {
        return;
      }
      const connectedInterval =
        typeof this.connectionHeartbeatIntervalMs === 'number' &&
        this.connectionHeartbeatIntervalMs > 0
          ? this.connectionHeartbeatIntervalMs
          : 8000;
      const disconnectedInterval =
        typeof this.connectionHeartbeatDisconnectedIntervalMs === 'number' &&
        this.connectionHeartbeatDisconnectedIntervalMs > 0
          ? this.connectionHeartbeatDisconnectedIntervalMs
          : 1000;
      const nextInterval = this.isConnected ? connectedInterval : disconnectedInterval;
      if (
        isConnectionDiagEnabled() &&
        (!this.isConnected || (this.connectionHeartbeatSeq || 0) <= 3)
      ) {
        connectionDiag('log', 'heartbeat-next', {
          seq: this.connectionHeartbeatSeq,
          isConnected: !!this.isConnected,
          failCount: this.connectionHeartbeatFailCount,
          nextInterval
        });
      }
      this.connectionHeartbeatTimer = window.setTimeout(() => {
        runHeartbeat();
      }, nextInterval);
    };

    // 先做一次即时探活，再根据连接状态动态设置下次轮询间隔
    runHeartbeat();
  },

  stopConnectionHeartbeat() {
    this.connectionHeartbeatActive = false;
    if (this.connectionHeartbeatTimer) {
      clearTimeout(this.connectionHeartbeatTimer);
      this.connectionHeartbeatTimer = null;
    }
    connectionDiag('log', 'heartbeat-stop', {}, { force: true });
  },

  openRealtimeTerminal() {
    const { protocol, hostname, port } = window.location;
    const target = `${protocol}//${hostname}${port ? ':' + port : ''}/terminal`;
    window.open(target, '_blank');
  },

  async loadInitialData() {
    try {
      debugLog('加载初始数据...');

      const statusResponse = await fetch('/api/status');
      if (!statusResponse.ok) {
        throw new Error(`状态接口请求失败: ${statusResponse.status}`);
      }
      const statusData = await statusResponse.json();
      this.isConnected = true;
      this.socket = null;
      this.projectPath = statusData.project_path || '';
      this.agentVersion = statusData.version || this.agentVersion;
      this.thinkingMode = !!statusData.thinking_mode;
      this.applyStatusSnapshot(statusData);
      this.fetchPermissionMode();
      this.fetchExecutionMode();
      this.fetchPendingToolApprovals();
      // 立即更新配额和运行模式，避免等待其他慢接口
      this.fetchUsageQuota();
      const modelStore = useModelStore();
      await modelStore.fetchModels().catch((err) => {
        console.warn('加载模型列表失败，使用前端默认列表:', err);
      });
      if (statusData && typeof statusData.model_key === 'string') {
        modelStore.setModel(statusData.model_key);
        this.currentModelKey = modelStore.currentModelKey;
      }
      // 拉取管理员策略
      const policyStore = usePolicyStore();
      await policyStore.fetchPolicy();
      this.applyPolicyUiLocks();

      // 加载个性化设置
      const personalizationStore = usePersonalizationStore();
      if (!personalizationStore.loaded && !personalizationStore.loading) {
        await personalizationStore.fetchPersonalization().catch((err) => {
          console.warn('加载个性化设置失败:', err);
        });
      }

      // 仅在“无当前对话”的新会话入口应用默认模型/模式；
      // 对于已存在对话（含刷新恢复/URL指定对话），必须保留对话自身设置。
      const statusConversationIdForDefaults = statusData?.conversation?.current_id;
      const hasActiveConversation =
        typeof statusConversationIdForDefaults === 'string' &&
        statusConversationIdForDefaults.length > 0 &&
        !statusConversationIdForDefaults.startsWith('temp_');
      if (personalizationStore.loaded && !hasActiveConversation && !this.currentConversationId) {
        const defaultRunMode = personalizationStore.form.default_run_mode;
        const defaultModel = personalizationStore.form.default_model;

        if (defaultRunMode) {
          this.runMode = defaultRunMode;
          debugLog('应用默认运行模式:', defaultRunMode);
        }

        if (defaultModel) {
          modelStore.setModel(defaultModel);
          this.currentModelKey = modelStore.currentModelKey;
          debugLog('应用默认模型:', defaultModel);
        }

        // 根据默认运行模式设置思考模式
        if (defaultRunMode === 'thinking') {
          this.thinkingMode = true;
        } else if (defaultRunMode === 'fast') {
          this.thinkingMode = false;
        }
      }

      const focusPromise = this.focusFetchFiles();
      let treePromise: Promise<any> | null = null;
      const isHostMode = statusData?.container?.mode === 'host';
      this.versioningHostMode = !!isHostMode;
      if (isHostMode) {
        this.fileMarkTreeUnavailable('宿主机模式下文件树不可用');
        await this.fetchHostWorkspaces();
      } else {
        this.hostWorkspaces = [];
        this.currentHostWorkspaceId = '';
        this.defaultHostWorkspaceId = '';
        this.hostWorkspaceCreateDialogOpen = false;
        this.hostWorkspaceCreatePath = '';
        this.hostWorkspaceCreateLabel = '';
        this.hostWorkspaceCreateError = '';
        this.hostWorkspaceCreateSubmitting = false;
        treePromise = this.fileFetchTree();
      }

      // 获取当前对话信息
      const isExplicitNewRoute = this.isExplicitNewConversationRoute();
      const runningConversationId = await this.getRunningTaskConversationId();
      const statusConversationId = statusData.conversation && statusData.conversation.current_id;
      const resumeConversationId = statusConversationId || runningConversationId;
      const shouldResumeOnNewRoute = isExplicitNewRoute && !!runningConversationId;

      if (
        resumeConversationId &&
        !this.currentConversationId &&
        (!isExplicitNewRoute || shouldResumeOnNewRoute)
      ) {
        this.skipConversationHistoryReload = true;
        // 首次从状态恢复对话时，避免 socket 的 conversation_loaded 再次触发历史加载
        this.skipConversationLoadedEvent = true;
        this.suppressTitleTyping = true;
        this.titleReady = false;
        this.currentConversationTitle = '';
        this.titleTypingText = '';
        this.currentConversationId = resumeConversationId;
        const pathFragment = this.stripConversationPrefix(resumeConversationId);
        const currentPath = window.location.pathname.replace(/^\/+/, '');
        if (currentPath !== pathFragment) {
          history.replaceState({ conversationId: resumeConversationId }, '', `/${pathFragment}`);
        }

        // 如果有当前对话，尝试获取标题和历史
        try {
          const convResponse = await fetch(`/api/conversations/current`);
          const convData = await convResponse.json();
          if (convData.success && convData.data) {
            this.currentConversationTitle = convData.data.title;
            this.titleReady = true;
            this.suppressTitleTyping = false;
            this.startTitleTyping(this.currentConversationTitle, { animate: false });
          } else {
            this.titleReady = true;
            this.suppressTitleTyping = false;
            const fallbackTitle = this.currentConversationTitle || '新对话';
            this.currentConversationTitle = fallbackTitle;
            this.startTitleTyping(fallbackTitle, { animate: false });
          }
          // 初始化时调用一次，因为 skipConversationHistoryReload 会阻止 watch 触发
          if (
            this.lastHistoryLoadedConversationId !== this.currentConversationId ||
            !Array.isArray(this.messages) ||
            this.messages.length === 0
          ) {
            await this.fetchAndDisplayHistory();
          }
          // 获取当前对话的Token统计
          this.fetchConversationTokenStatistics();
          this.updateCurrentContextTokens();
          await this.fetchVersioningStatus(this.currentConversationId, { silent: true });
        } catch (e) {
          console.warn('获取当前对话标题失败:', e);
          this.titleReady = true;
          this.suppressTitleTyping = false;
          this.startTitleTyping(this.currentConversationTitle || '新对话', '');
        }
      }

      // 待办数据依赖当前会话上下文，放在会话恢复/切换之后拉取，避免初始化早期拿到空快照
      const todoPromise = this.fileFetchTodoList();

      // 等待其他加载项完成（允许部分失败不阻塞模式切换）
      const pendingPromises = [focusPromise, todoPromise];
      if (treePromise) {
        pendingPromises.push(treePromise);
      }
      await Promise.allSettled(pendingPromises);
      await this.loadToolSettings(true);

      // 加载对话列表（修复：初始化时未加载对话列表的bug）
      this.conversationsOffset = 0;
      await this.loadConversationsList();

      debugLog('初始数据加载完成');
    } catch (error) {
      console.error('加载初始数据失败:', error);
      this.isConnected = false;
    }
  },

  confirmAction(options = {}) {
    return this.uiRequestConfirm(options);
  },

  async handleCopyCodeClick(event) {
    const target = event.target;
    if (!target || !target.classList || !target.classList.contains('copy-code-btn')) {
      return;
    }

    const blockId = target.getAttribute('data-code');
    if (!blockId) {
      return;
    }

    // 防止重复点击
    if (target.classList.contains('copied')) {
      return;
    }

    const selector = `[data-code-id="${blockId.replace(/"/g, '\\"')}"]`;
    const codeEl = document.querySelector(selector);
    if (!codeEl) {
      return;
    }

    const encoded = codeEl.getAttribute('data-original-code');
    const content = encoded ? this.decodeHtmlEntities(encoded) : codeEl.textContent || '';

    if (!content.trim()) {
      return;
    }

    try {
      await navigator.clipboard.writeText(content);

      // 添加 copied 类，切换为对勾图标
      target.classList.add('copied');

      // 5秒后恢复
      setTimeout(() => {
        target.classList.remove('copied');
      }, 5000);
    } catch (error) {
      console.warn('复制失败:', error);
    }
  },

  decodeHtmlEntities(text) {
    const textarea = document.createElement('textarea');
    textarea.innerHTML = text;
    return textarea.value;
  }
};
