<template>
  <!-- ═══ 第三批抽离: 顶部工具栏(provider/model 选择器 + 标题 + 审批角标 + 顶部操作),
       从 AiChat.vue 行 12-145 迁移,零行为变更。store 单例共享(useAiStore 同实例),
       动作按钮经 emit 回父(父持有 clear/compress/confirm 逻辑 + 单一 toast 源)。 ═══ -->
  <div class="ai-header">
    <div class="ai-header-left">
      <button class="ai-btn-icon" @click="emit('toggle-sidebar')" :title="$t('aiChat.conversationList')">
        <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><line x1="3" y1="6" x2="21" y2="6"/><line x1="3" y1="12" x2="21" y2="12"/><line x1="3" y1="18" x2="21" y2="18"/></svg>
      </button>
      <span class="ai-header-icon">
        <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2a7 7 0 014 12.74V17a1 1 0 01-1 1H9a1 1 0 01-1-1v-2.26A7 7 0 0112 2z"/><line x1="9" y1="21" x2="15" y2="21"/></svg>
      </span>
      <span class="ai-header-title">{{ $t('ai.assistant') }}</span>
      <div
        v-if="pendingApprovalCount > 0"
        class="ai-approval-badge"
        :title="$t('aiChat.pendingApprovalHint')"
        @click="emit('scroll-to-pending')"
      >
        ⏳ {{ $t('aiChat.pendingApprovalCount', { n: pendingApprovalCount }) }}
      </div>
    </div>
    <!-- 提供商状态 + 模型选择器 -->
    <div class="ai-header-center">
      <div
        class="ai-provider-bar"
        :class="{ 'ai-provider-bar--switchable': store.state.providers.length > 1 }"
        v-if="store.state.providers.length > 0"
        :title="store.state.providers.length > 1 ? $t('aiChat.clickToSwitchProvider') : ''"
        @click="cycleProvider"
      >
        <span class="provider-dot" :class="'provider-dot--' + providerStatus"></span>
        <span class="provider-name">{{ activeProviderName }}</span>
      </div>
      <div class="ai-provider-bar ai-provider-empty" v-else>
        <span class="provider-hint">{{ $t('aiChat.providerNotConfigured') }}</span>
      </div>
      <div class="ai-model-picker" :title="$t('aiChat.specifyModeHint') || ''">
        <select
          v-if="enabledModels.length"
          class="ai-model-select ai-model-select--compact"
          :value="selectedModelId"
          @change="onModelSelect($event)"
        >
          <option v-for="m in enabledModels" :key="m.model_id" :value="m.model_id">
            {{ modelOptionLabel(m) }}
          </option>
        </select>
        <span v-else class="ai-model-empty">{{ $t('aiChat.noModels') }}</span>
      </div>
    </div>
    <div class="ai-header-actions">
      <!-- ══ 更多菜单(...):收纳低频的上下文管理操作(清空上下文/压缩上下文)══ -->
      <!-- 使用频率分级:新建/清空对话/窗口控制是高频常用,上下文管理是低频项;
           收进 popout 降低顶部视觉密度,需时从 ... 入口展开。点击外部收起。 -->
      <div class="ai-more-menu" ref="moreMenuRef">
        <button
          class="ai-btn-icon"
          :class="{ 'ai-btn-icon--active': moreMenuOpen }"
          :title="$t('aiChat.moreActions')"
          @click="moreMenuOpen = !moreMenuOpen"
        >
          <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="5" r="1.5"/><circle cx="12" cy="12" r="1.5"/><circle cx="12" cy="19" r="1.5"/></svg>
        </button>
        <Transition name="ai-more-popout">
          <div v-if="moreMenuOpen" class="ai-more-popout" role="menu">
            <!-- F-15 阶段2: 清空上下文(历史消息归档保留,不删 DB;新对话不受影响) -->
            <button
              class="ai-more-item"
              :disabled="!hasActiveMessages || store.state.streaming"
              @click="emit('clear-context'); moreMenuOpen = false"
            >
              <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><polyline points="21 8 21 21 3 21 3 8"/><rect x="1" y="3" width="22" height="5"/><line x1="10" y1="12" x2="14" y2="12"/></svg>
              <span>{{ $t('aiChat.clearContext') }}</span>
            </button>
            <!-- F-15 阶段2: 压缩上下文(LLM 摘要落 system + 历史消息标 compressed 归档) -->
            <button
              class="ai-more-item"
              :disabled="!hasActiveMessages || store.state.streaming || store.isCompressing.value"
              @click="emit('compress-context'); moreMenuOpen = false"
            >
              <svg v-if="!store.isCompressing.value" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><polyline points="4 14 10 14 10 20"/><polyline points="20 10 14 10 14 4"/><line x1="14" y1="10" x2="21" y2="3"/><line x1="3" y1="21" x2="10" y2="14"/></svg>
              <svg v-else class="ai-btn-spinner" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M21 12a9 9 0 11-6.219-8.56"/></svg>
              <span>{{ store.isCompressing.value ? $t('aiChat.compressing') : $t('aiChat.compressContext') }}</span>
            </button>
          </div>
        </Transition>
      </div>

      <!-- 嵌入模式：最大化/还原 + 分离 + 关闭 -->
      <template v-if="!detached">
        <button class="ai-btn-icon" @click="emit('toggle-maximize')" :title="store.state.maximized ? $t('aiChat.restoreSidebar') : $t('aiChat.maximize')">
          <svg v-if="store.state.maximized" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/><line x1="9" y1="3" x2="9" y2="21"/></svg>
          <svg v-else width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M8 3H5a2 2 0 00-2 2v3m18 0V5a2 2 0 00-2-2h-3m0 18h3a2 2 0 002-2v-3M3 16v3a2 2 0 002 2h3"/></svg>
        </button>
        <button class="ai-btn-icon" @click="emit('detach')" :title="$t('aiChat.detachWindow')">
          <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M15 3h6v6"/><path d="M9 21H3v-6"/><path d="M21 3l-7 7"/><path d="M3 21l7-7"/></svg>
        </button>
        <button class="ai-btn-icon" @click="emit('close-panel')" :title="$t('aiChat.closePanel')">
          <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
        </button>
      </template>
      <!-- 分离模式：吸附 + 置顶 + 关闭 -->
      <template v-else>
        <button class="ai-btn-icon" :class="{ 'ai-btn-icon--active': store.state.docked }" @click="emit('dock')" :title="store.state.docked ? $t('aiChat.undock') : $t('aiChat.dockRight')">
          <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="3" width="8" height="18" rx="1"/><rect x="14" y="3" width="8" height="18" rx="1"/></svg>
        </button>
        <button class="ai-btn-icon" :class="{ 'ai-btn-icon--active': alwaysOnTop }" @click="emit('toggle-always-on-top')" :title="alwaysOnTop ? $t('aiChat.unpin') : $t('aiChat.pinOnTop')">
          <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2C8 2 6 5 6 8v3l-2 2v1h16v-1l-2-2V8c0-3-2-6-6-6z"/><line x1="12" y1="14" x2="12" y2="22"/></svg>
        </button>
        <button class="ai-btn-icon" @click="emit('close-detached')" :title="$t('aiChat.closeWindow')">
          <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
        </button>
      </template>
    </div>
  </div>

  <!-- 底部工具区:标题 + 目标 + 历史消息 + 上下文摘要 -->
  <div class="ai-bottom-tools" ref="goalsRef">
    <span v-if="currentConvTitle" class="ai-bottom-title">{{ currentConvTitle }}</span>
    <div class="ai-bottom-tools-right">
    <!-- 目标计数 -->
    <div v-if="goals.length" class="ai-goals-inline">
      <span class="ai-goals-inline-badge ai-tool-badge" @click="togglePanel('goals')" :title="$t('aiChat.viewGoals')">
        <svg width="9" height="9" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><circle cx="12" cy="12" r="6"/><circle cx="12" cy="12" r="2"/></svg>
        {{ goals.filter(g => g.status === 'active').length }}/{{ goals.length }}
      </span>
      <div v-if="goalsExpanded" class="ai-goals-inline-list">
        <div v-for="(g, i) in goals" :key="i" class="ai-goal-inline-item" :class="{ 'ai-goal-completed': g.status === 'completed' }">
          <span v-if="g.status === 'active'" class="ai-goal-dot"></span>
          <span v-else class="ai-goal-check">✓</span>
          <span class="ai-goal-text">{{ g.text }}</span>
          <button class="ai-goal-remove" @click.stop="removeGoal(i)">×</button>
        </div>
      </div>
    </div>
    <!-- 历史消息 -->
    <div v-if="historyMsgs.length" class="ai-history-inline">
      <span class="ai-history-inline-badge ai-tool-badge" @click="togglePanel('history')" :title="$t('aiChat.viewHistory')">
        <svg width="9" height="9" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
        {{ historyMsgs.length }}
      </span>
      <div v-if="historyExpanded" class="ai-history-inline-list">
        <div v-for="h in historyMsgs" :key="h.id" class="ai-history-inline-item" @click="scrollToMessage(h.id)">
          <span class="ai-history-index">{{ h.num }}</span>
          <span class="ai-history-text">{{ h.text }}</span>
          <span class="ai-history-time">{{ h.time }}</span>
        </div>
      </div>
    </div>
    <!-- 上下文摘要 -->
    <div v-if="summaryMsgs.length" class="ai-summary-inline">
      <span class="ai-summary-inline-badge ai-tool-badge" @click="togglePanel('summary')" :title="$t('aiChat.viewSummary')">
        <svg width="9" height="9" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 6h16M4 12h16M4 18h12"/></svg>
        {{ summaryMsgs.length }}
      </span>
      <div v-if="summaryExpanded" class="ai-summary-inline-list">
        <div v-for="(s, i) in summaryMsgs" :key="i" class="ai-summary-inline-item" @click="scrollToMessage(s.id)">
          <span class="ai-summary-label">{{ $t('aiChat.compressedSummaryLabel') }} #{{ summaryMsgs.length - i }}</span>
          <p class="ai-summary-preview">{{ s.text }}</p>
        </div>
      </div>
    </div>
    <!-- L3 完整上下文 -->
    <div v-if="contextInfo.goals.length || contextInfo.summaryCount > 0" class="ai-context-inline">
      <span class="ai-context-inline-badge ai-tool-badge" @click="togglePanel('context')" :title="$t('aiChat.viewContext')">
        <svg width="9" height="9" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M12 16v-4"/><circle cx="12" cy="8" r="1" fill="currentColor"/></svg>
      </span>
      <div v-if="contextExpanded" class="ai-context-inline-list">
        <!-- 系统提示 -->
        <div class="ai-context-section">
          <div class="ai-context-section-title">{{ $t('aiChat.contextSystemPrompt') }}</div>
          <div class="ai-context-section-body">
            <div class="ai-context-row">
              <span class="ai-context-label">目标({{ contextInfo.goals.length }})</span>
              <span class="ai-context-value">{{ contextInfo.goals.join('; ') || '无' }}</span>
            </div>
            <div class="ai-context-row">
              <span class="ai-context-label">{{ $t('aiChat.contextEnvironment') }}</span>
              <span class="ai-context-value">{{ contextInfo.osInfo }}</span>
            </div>
            <div class="ai-context-row">
              <span class="ai-context-label">{{ $t('aiChat.contextCompressedSummary') }}</span>
              <span class="ai-context-value">{{ contextInfo.summaryCount }} {{ $t('aiChat.contextCountUnit') }}</span>
            </div>
          </div>
        </div>
        <!-- 增强上下文 -->
        <div class="ai-context-section">
          <div class="ai-context-section-title">{{ $t('aiChat.contextEnrichment') }}</div>
          <div class="ai-context-section-body">
            <div class="ai-context-row">
              <span class="ai-context-label">@项目</span>
              <span class="ai-context-value">{{ contextInfo.projectNames || '无' }}</span>
            </div>
          </div>
        </div>
      </div>
    </div>
    </div>
  </div>

  <!-- UX-2025-14: Provider 切换临时 bar(切换后展示当前 model 名,2s 后淡出) -->
  <Transition name="ai-provider-switch">
    <div v-if="providerBarVisible" class="ai-provider-switch-bar">
      <span class="ai-provider-switch-dot"></span>
      <span class="ai-provider-switch-text">{{ $t('aiChat.providerSwitchHint', { model: activeProviderName }) }}</span>
    </div>
  </Transition>
</template>

<script setup lang="ts">
// 第三批抽离: 顶部工具栏 TopBar(从 AiChat.vue 行 12-145 template + 行 741-902 script 迁移)
// 零行为变更。provider/model 选择逻辑直接读 store(单例共享),动作按钮 emit 回父,
// 父持有 clear/compress/confirm 逻辑 + 单一 toast 源(provider-switched 回父弹 toast)。
import { ref, computed, watch, onMounted, onBeforeUnmount } from 'vue'
import { useI18n } from 'vue-i18n'
import { useAiStore } from '../../stores/ai'
import { aiApi } from '../../api/ai'
import type { AiMessage, ModelConfig, GoalEntry } from '../../api/types'

defineProps<{
  /** 分离窗口模式(嵌入 vs 分离决定顶部按钮组) */
  detached?: boolean
  /** 有活跃消息(清空/压缩按钮启用条件;父 computed) */
  hasActiveMessages: boolean
  /** 待审批数(审批角标显隐 + 计数;父 computed) */
  pendingApprovalCount: number
  /** 分离窗口置顶态(父持有,经 prop 透传按钮 active 态) */
  alwaysOnTop: boolean
}>()

const emit = defineEmits<{
  /** 折叠/展开会话侧栏(store.toggleSidebar,父转发保持 store 单一调用源) */
  (e: 'toggle-sidebar'): void
  /** 新建会话(父 confirmNewConversation,F-09 决策e 直接新建无中断确认) */
  (e: 'new-conversation'): void
  /** 清空上下文(归档保留;父 handleClearContext) */
  (e: 'clear-context'): void
  /** 压缩上下文(LLM 摘要;父 handleCompressContext) */
  (e: 'compress-context'): void
  /** 清空对话(真删 DB;父 confirmClearChat 带二次确认) */
  (e: 'clear-chat'): void
  /** 点击审批角标滚动到首个待审批(父 scrollToFirstPending) */
  (e: 'scroll-to-pending'): void
  /** 最大化/还原(store.toggleMaximize) */
  (e: 'toggle-maximize'): void
  /** 分离窗口(store.detachPanel) */
  (e: 'detach'): void
  /** 关闭面板(store.togglePanel) */
  (e: 'close-panel'): void
  /** 吸附/取消吸附(store.dockDetached) */
  (e: 'dock'): void
  /** 置顶切换(父 toggleAlwaysOnTop 调 Tauri setAlwaysOnTop) */
  (e: 'toggle-always-on-top'): void
  /** 关闭分离窗口(store.closeDetachedWindow) */
  (e: 'close-detached'): void
  /** Provider 循环切换后回传 model 名(父 showToast 弹切换提示,保持单一 toast 源) */
  (e: 'provider-switched', modelName: string): void
}>()

const store = useAiStore()
const { t } = useI18n()

// ══ 更多菜单 popout 状态(上下文管理操作收纳)══
// 顶部按钮过多时,低频的清空/压缩上下文收入 ... popout;点击按钮切换,点击外部收起。
const moreMenuOpen = ref(false)
const moreMenuRef = ref<HTMLElement | null>(null)
function onDocClickCloseMoreMenu(e: MouseEvent) {
  if (!moreMenuOpen.value) return
  const root = moreMenuRef.value
  if (root && !root.contains(e.target as Node)) {
    moreMenuOpen.value = false
  }
}
onMounted(() => document.addEventListener('click', onDocClickCloseMoreMenu))
onBeforeUnmount(() => document.removeEventListener('click', onDocClickCloseMoreMenu))
// ESC 键收起更多菜单(键盘友好)
function onEscCloseMoreMenu(e: KeyboardEvent) {
  if (e.key === 'Escape' && moreMenuOpen.value) moreMenuOpen.value = false
}
onMounted(() => document.addEventListener('keydown', onEscCloseMoreMenu))
onBeforeUnmount(() => document.removeEventListener('keydown', onEscCloseMoreMenu))

/** provider 连通状态(零侵入,基于最后一条 assistant 消息 isError 推断):
 *  unknown=未发过(灰) / ok=最近一次 AI 请求成功(绿) / error=最近失败(红,如 401/超时/服务不通)。
 *  不主动 ping(省配额),反映最近一次实际请求结果。切换 provider 后状态保留直到新请求。 */
const providerStatus = computed<'unknown' | 'ok' | 'error'>(() => {
  const msgs = store.state.messages
  for (let i = msgs.length - 1; i >= 0; i--) {
    const m = msgs[i]
    if (m.role === 'assistant') return m.isError ? 'error' : 'ok'
  }
  return 'unknown'
})

const activeProviderName = computed(() => {
  const active = store.state.providers.find(p => p.id === store.state.activeProvider)
  return active?.name || store.state.providers[0]?.name || t('aiChat.notConfigured')
})

// ── 模型选择器(对话透明化 L1:折叠为紧凑下拉,直接选择模型) ──
// active provider 的 model_configs(用户在 Settings 拉取过的,仅 enabled 项)。
// 空池(provider 未拉取/无 enabled 模型)时下拉为空。
const activeProviderRecord = computed(() =>
  store.state.providers.find(p => p.id === store.state.activeProvider)
  || store.state.providers.find(p => p.is_default)
  || store.state.providers[0]
  || null,
)
const enabledModels = computed(() => {
  const p = activeProviderRecord.value
  if (!p?.model_configs) return []
  return p.model_configs
    .filter(m => m.enabled)
    .slice()
    .sort((a, b) => (b.weight ?? 0) - (a.weight ?? 0))
})
const selectedModelId = computed(() => store.modelOverride.value || '')
function modelOptionLabel(m: ModelConfig): string {
  return m.label || m.model_id
}
function onModelSelect(e: Event) {
  const v = (e.target as HTMLSelectElement).value
  store.modelOverride.value = v || null
}

// 切换会话默认选中权重最高 model
watch(() => store.state.activeConversationId, () => {
  store.modelOverride.value = enabledModels.value[0]?.model_id || null
})

// 切 provider 后模型 override 同步(BUG-2026-07-07:provider 切换后 header 模型名不刷新):
// - modelOverride 是模块级 ref(useAiSend)不持久化,切 provider 后仍持旧 provider 的 model_id,
//   该 id 不在新 provider 池里 → 下拉显示错位(UI 显示 X,实际后端用路由默认 Y)。
// - 守卫:当前选择对新 provider 模型池无效(空/不在池)时,默认选权重最高模型。
//   enabledModels 已按 weight 降序排,model[0] 即权重最高。
// - immediate:组件挂载时也校验(modelOverride 为 null → 默认权重最高,与下拉首项对齐)。
watch(enabledModels, (models) => {
  const current = store.modelOverride.value
  const valid = current && models.some(m => m.model_id === current)
  if ((!valid || !current) && models.length) {
    store.modelOverride.value = models[0].model_id
  }
}, { immediate: true })

// ── 当前对话标题 ──
const currentConvTitle = computed(() => {
  const id = store.state.activeConversationId
  if (!id) return ''
  const conv = store.state.conversations.find(c => c.id === id)
  return conv?.title || ''
})

// 🎯 对话目标面板(对话透明化 L1:Goal visibility)
const goals = ref<GoalEntry[]>([])
const goalsExpanded = ref(false)

// ── 📋 历史消息(当前会话 user 消息列表) ──
interface HistoryItem { id: string; text: string; time: string; num: number }
/** 时间显示:今日 HH:mm / 昨天 HH:mm / MM-DD HH:mm */
function fmtTime(ts: number): string {
  const d = new Date(ts)
  const now = new Date()
  const pad = (n: number) => String(n).padStart(2, '0')
  const hm = `${pad(d.getHours())}:${pad(d.getMinutes())}`
  // 同一天 → 只显示时间
  if (d.getFullYear() === now.getFullYear() && d.getMonth() === now.getMonth() && d.getDate() === now.getDate()) {
    return hm
  }
  // 昨天
  const yest = new Date(now)
  yest.setDate(yest.getDate() - 1)
  if (d.getFullYear() === yest.getFullYear() && d.getMonth() === yest.getMonth() && d.getDate() === yest.getDate()) {
    return `昨天 ${hm}`
  }
  // 更早
  return `${pad(d.getMonth()+1)}-${pad(d.getDate())} ${hm}`
}
const MAX_HISTORY = 20
const MAX_CHARS = 80
const historyMsgs = computed<HistoryItem[]>(() => {
  const allUserMsgs = store.state.messages
    .filter((m): m is AiMessage & { timestamp: number } => m.role === 'user' && !!m.content && !!m.timestamp)
    .map((m, idx) => ({
      id: m.id,
      text: m.content.length > MAX_CHARS ? m.content.slice(0, MAX_CHARS) + '…' : m.content,
      time: fmtTime(m.timestamp),
      num: idx + 1,
    }))
  return allUserMsgs.slice(-MAX_HISTORY).reverse()
})
const historyExpanded = ref(false)

/** 点击历史消息项 → 滚动定位到对应的消息气泡 */
function scrollToMessage(msgId: string) {
  historyExpanded.value = false
  // 消息列表已有 data-msg-id 属性,直接 DOM 查询定位。
  // BUG-260802:msgId 由后端生成,可能含 CSS 选择器元字符(: . [ ] / 空格 等),
  // 模板拼 selector 会被选择器解析器当伪类/类名异常 → querySelector 抛错或返回 null。
  // 用 CSS.escape 转义 msgId,确保任意字符串安全嵌入属性选择器。
  // 旧浏览器无 CSS.escape 的兜底:encodeURIComponent + attr*= 模糊匹配(非精确,降级路径)。
  let el: Element | null = null
  const escaped = typeof CSS !== 'undefined' && typeof CSS.escape === 'function'
    ? CSS.escape(msgId)
    : encodeURIComponent(msgId)
  try {
    el = document.querySelector(`[data-msg-id="${escaped}"]`)
  } catch {
    // 转义后仍异常(理论不会)→ 退化为遍历查找
    const all = document.querySelectorAll('[data-msg-id]')
    el = Array.from(all).find(n => n.getAttribute('data-msg-id') === msgId) ?? null
  }
  if (el) {
    el.scrollIntoView({ behavior: 'smooth', block: 'center' })
  }
}

/** 目标/历史下拉容器 DOM 引用（用于点击外部检测） */
const goalsRef = ref<HTMLElement | null>(null)

function onBottomToolsClickOutside(e: MouseEvent) {
  if (!goalsRef.value) return
  if (goalsExpanded.value && !goalsRef.value.contains(e.target as Node)) {
    goalsExpanded.value = false
  }
  if (historyExpanded.value && !goalsRef.value.contains(e.target as Node)) {
    historyExpanded.value = false
  }
  if (summaryExpanded.value && !goalsRef.value.contains(e.target as Node)) {
    summaryExpanded.value = false
  }
  if (contextExpanded.value && !goalsRef.value.contains(e.target as Node)) {
    contextExpanded.value = false
  }
}
// ── 📄 上下文摘要(压缩/归档的系统消息) ──
interface SummaryItem { id: string; text: string }
const summaryMsgs = computed<SummaryItem[]>(() => {
  return store.state.messages
    .filter(m => m.role === 'system' && !!m.content)
    .map(m => ({
      id: m.id,
      text: m.content.length > 120 ? m.content.slice(0, 120) + '…' : m.content,
    }))
})
const summaryExpanded = ref(false)

// ── L3 完整上下文 ──
interface ContextInfo {
  goals: GoalEntry[]
  summaryCount: number
  osInfo: string
  projectNames: string
}
const contextExpanded = ref(false)

/** 弹出面板互斥:goals/history/summary/context 四个 badge 面板,点一个收起其他,再点当前切换。 */
function togglePanel(panel: 'goals' | 'history' | 'summary' | 'context') {
  const refs = { goals: goalsExpanded, history: historyExpanded, summary: summaryExpanded, context: contextExpanded }
  const willOpen = !refs[panel].value
  goalsExpanded.value = historyExpanded.value = summaryExpanded.value = contextExpanded.value = false
  refs[panel].value = willOpen
}

const contextInfo = computed<ContextInfo>(() => {
  const convId = store.state.activeConversationId
  const conv = convId ? store.state.conversations.find(c => c.id === convId) : undefined
  // 从所有 user 消息中收集 mentionSpans(kind === 'project'),去重
  const projectLabels = new Set<string>()
  for (const m of store.state.messages) {
    if (m.role === 'user' && m.mentionSpans?.length) {
      for (const sp of m.mentionSpans) {
        if (sp.kind === 'project' && sp.label) {
          projectLabels.add(sp.label)
        }
      }
    }
  }
  return {
    goals: conv?.pinned_goals ?? [],
    summaryCount: summaryMsgs.value.length,
    osInfo: 'Windows 11 / PowerShell',
    projectNames: Array.from(projectLabels).join(', ') || '无',
  }
})

/** 按 Escape 关闭所有下拉 */
function onBottomToolsKeydown(e: KeyboardEvent) {
  if (e.key === 'Escape') {
    goalsExpanded.value = false
    historyExpanded.value = false
    summaryExpanded.value = false
    contextExpanded.value = false
  }
}
onMounted(() => {
  document.addEventListener('click', onBottomToolsClickOutside)
  document.addEventListener('keydown', onBottomToolsKeydown)
})
onBeforeUnmount(() => {
  document.removeEventListener('click', onBottomToolsClickOutside)
  document.removeEventListener('keydown', onBottomToolsKeydown)
})

/** 监听 activeConversationId 变化,加载目标 */
watch(() => store.state.activeConversationId, (convId) => {
  loadGoals(convId)
}, { immediate: true })
/** 消息变化后也刷新目标 */
watch(() => store.state.messages.length, () => {
  setTimeout(() => loadGoals(store.state.activeConversationId), 100)
})

function loadGoals(convId: string | null) {
  if (!convId) { goals.value = []; return }
  const conv = store.state.conversations.find(c => c.id === convId)
  // 兼容旧格式(string[]):后端迁移过渡期可能返回纯文本数组
  const raw = conv?.pinned_goals ?? []
  goals.value = raw.map((g: any) =>
    typeof g === 'string' ? { text: g, status: 'active' as const } : g
  )
}

/** 移除第 i 个目标 */
async function removeGoal(i: number) {
  const convId = store.state.activeConversationId
  if (!convId) return
  const next = [...goals.value]
  next.splice(i, 1)
  goals.value = next
  if (next.length === 0) goalsExpanded.value = false
  // API 接受 string[] 或 GoalEntry[]
  await aiApi.updateConversationGoals(convId, next.map(g => g.text))
}

// 循环切换 Provider(provider bar 点击)
function cycleProvider() {
  const ps = store.state.providers
  if (ps.length < 2) return
  // F-260616-10: activeProvider 为 null 时从 DB 默认(is_default)起算,避免 findIndex=-1 跳到 providers[0]
  const startId = store.state.activeProvider || ps.find(p => p.is_default)?.id || null
  const idx = startId ? ps.findIndex(p => p.id === startId) : -1
  // idx=-1 且无默认 provider 时 fallback 到 0(极端边界,正常路径不会走到)
  const next = ps[(Math.max(idx, 0) + 1) % ps.length]
  store.setProvider(next.id)
  // UX-2025-14: 切换反馈 — toast 提示当前 model + 临时 bar 展示 model 名(2s 后淡出)。
  // toast 机制:复用父自管 reactive showToast(分离窗口无 App.vue 根 toast),经 emit 回父弹。
  showProviderSwitched(next.name || next.id)
}

// ── UX-2025-14: Provider 切换临时 bar ──
// 切换后展示当前 model 名小字,0.15s 淡入,2s 后淡出。本子组件自管 bar 显隐(ref + Transition),
// toast 经 emit 回父(父持有 toast reactive 容器)。
const providerBarVisible = ref(false)
let _providerBarTimer: ReturnType<typeof setTimeout> | null = null
function showProviderSwitched(modelName: string) {
  // toast:经 emit 回父(单一 toast 源)
  emit('provider-switched', modelName)
  // bar:ref 控制显隐,Transition 驱动 0.15s 淡入/淡出
  providerBarVisible.value = true
  if (_providerBarTimer) clearTimeout(_providerBarTimer)
  _providerBarTimer = setTimeout(() => { providerBarVisible.value = false }, 2000)
}

// UX-2025-14:释放 provider 切换 bar 计时器(从 AiChat onBeforeUnmount 迁移,与 _providerBarTimer 同处)
onBeforeUnmount(() => {
  if (_providerBarTimer) { clearTimeout(_providerBarTimer); _providerBarTimer = null }
})
</script>

<!-- ═══ TopBar scoped 样式(第三批抽离: 从 AiChat.vue <style scoped> 迁移相关规则,
     零视觉变更。TopBar 多根节点(header + provider bar + Transition),父级 AiChat scoped
     不穿透多根子组件,故本组件自持样式。CSS 变量(--df-*)由根级提供,共享同一份。) ═══ -->
<style scoped>
/* ═══ Header ═══ */
.ai-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 10px 14px;
  border-bottom: 0.5px solid var(--df-border);
}
.ai-header-left {
  display: flex;
  align-items: center;
  gap: 6px;
}
.ai-header-icon {
  display: flex;
  align-items: center;
  color: var(--df-accent);
}
.ai-header-title {
  font-size: 13px;
  font-weight: 500;
  color: var(--df-text);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  max-width: 200px;
}
/* AE-2025-02:审批徽标(待审批>0 时显于头部,点击跳首个审批卡) */
.ai-approval-badge {
  display: inline-flex;
  align-items: center;
  cursor: pointer;
  background: var(--df-warning-bg);
  color: var(--df-warning);
  padding: 2px 8px;
  border-radius: var(--df-radius-sm);
  font-size: 11px;
  font-weight: 500;
  white-space: nowrap;
  transition: opacity 0.15s var(--df-ease);
}
.ai-approval-badge:hover {
  opacity: 0.85;
}

/* ═══ 模型选择器(对话透明化 L1:紧凑下拉) ═══ */
.ai-model-picker {
  display: flex;
  align-items: center;
  min-width: 0;
}
.ai-model-select {
  min-width: 0;
  max-width: 180px;
  border: 0.5px solid var(--df-border);
  border-radius: var(--df-radius-sm);
  background: var(--df-bg);
  color: var(--df-text);
  font-size: 10px;
  font-family: var(--df-font-mono);
  padding: 3px 6px;
  outline: none;
  cursor: pointer;
}
.ai-model-select--compact {
  max-width: 130px;
}
.ai-model-select:disabled {
  opacity: 0.6;
  cursor: not-allowed;
}
.ai-model-empty {
  font-size: 10px;
  color: var(--df-text-dim);
  white-space: nowrap;
}
.ai-header-actions {
  display: flex;
  gap: 4px;
}
/* ── 图标按钮(共享工具类,header-actions 内清空/压缩/最大化/分离/关闭等)── */
.ai-btn-icon {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 26px;
  height: 26px;
  border: none;
  border-radius: var(--df-radius-sm);
  background: transparent;
  color: var(--df-text-dim);
  cursor: pointer;
  transition: all 0.15s;
}
.ai-btn-icon:hover {
  background: var(--df-bg-card);
  color: var(--df-text-secondary);
}
.ai-btn-icon--active {
  background: var(--df-accent-bg);
  color: var(--df-accent);
}
.ai-btn-icon:disabled {
  opacity: 0.35;
  cursor: not-allowed;
}
.ai-btn-icon:disabled:hover {
  background: transparent;
  color: var(--df-text-dim);
}
/* ── 更多菜单 popout(清空/压缩上下文等低频操作收纳;CSS 原缺失致打开样式错乱)── */
.ai-more-menu { position: relative; }  /* popout absolute 定位基准 */
.ai-more-popout {
  position: absolute;
  top: calc(100% + 4px);
  right: 0;
  min-width: 168px;
  padding: 4px;
  background: var(--df-bg-card);
  border: 0.5px solid var(--df-border);
  border-radius: var(--df-radius);
  box-shadow: 0 4px 16px rgba(0, 0, 0, 0.25);
  z-index: 100;
  display: flex;
  flex-direction: column;
  gap: 2px;
}
.ai-more-item {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 6px 8px;
  border: none;
  border-radius: var(--df-radius-sm);
  background: transparent;
  color: var(--df-text-secondary);
  font-size: 12px;
  cursor: pointer;
  white-space: nowrap;
}
.ai-more-item:hover:not(:disabled) { background: var(--df-bg-hover, rgba(0,0,0,0.06)); color: var(--df-text); }
.ai-more-item:disabled { opacity: 0.35; cursor: not-allowed; }
/* Transition:淡入 + 下滑 */
.ai-more-popout-enter-active, .ai-more-popout-leave-active { transition: opacity .15s, transform .15s; }
.ai-more-popout-enter-from, .ai-more-popout-leave-to { opacity: 0; transform: translateY(-4px); }
/* 压缩中 spinner 图标旋转动画(@keyframes ai-btn-spin 在父级 AiChat.vue 全局定义,
   scoped 不重命名 @keyframes,此处按名引用) */
.ai-btn-spinner {
  animation: ai-btn-spin 0.9s linear infinite;
}
/* 底部工具区(标题左 + 工具右) */
.ai-bottom-tools {
  display: flex;
  align-items: center;

  padding: 0 14px 4px;
}
.ai-bottom-title {
  font-size: 11px;
  color: var(--df-text-secondary);
  min-width: 0;
  margin-right: auto;

}
.ai-bottom-tools-right {
  display: flex;
  align-items: center;
  gap: 4px;
  flex-shrink: 0;
  margin-left: auto;
}
/* ═══ Provider Bar (header 中间区) ═══ */
.ai-provider-bar {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  font-size: 11px;
  color: var(--df-text-secondary);
  white-space: nowrap;
}
.ai-provider-bar--switchable {
  cursor: pointer;
}
.ai-provider-bar--switchable:hover {
  color: var(--df-text-primary);
}
/* provider 连通状态点:灰=未测/绿=最近成功/红=最近失败(基于最后 assistant 消息 isError) */
.provider-dot { width: 5px; height: 5px; border-radius: 50%; background: var(--df-text-dim); flex-shrink: 0; transition: background .2s; }
.provider-dot--ok { background: var(--df-success); }
.provider-dot--error { background: var(--df-danger); }
.provider-name {
  font-size: 11px;
  font-family: var(--df-font-mono);
  color: var(--df-text-dim);
}
.ai-provider-empty {
  background: var(--df-danger-bg);
}

/* 历史消息(底部) */
.ai-history-inline {
  position: relative;
  display: inline-flex;
  align-items: center;
}
/* 底部工具通用徽标 */
.ai-tool-badge {
  cursor: pointer;
  font-size: 11px;
  line-height: 16px;
  padding: 2px 8px;
  border-radius: 4px;
  display: inline-block;
  vertical-align: middle;
  gap: 4px;
  color: var(--df-text);
  background: var(--df-bg-card);
  border: 0.5px solid var(--df-border);
  transition: color 0.15s, background 0.15s;
  white-space: nowrap;
}
.ai-tool-badge svg { vertical-align: middle; }  /* 图标在行高内垂直居中(与数字同基线)*/
.ai-tool-badge:hover {
  background: var(--df-bg-card-hover);
}

/* 目标计数(底部) */
.ai-goals-inline {
  position: relative;
  display: inline-flex;
  align-items: center;
}
.ai-history-inline-list {
  position: absolute;
  left: auto; right: 0;
  top: 100%;
  z-index: 100;
  min-width: 240px;
  max-width: 400px;
  max-height: 300px;
  overflow-y: auto;
  background: var(--df-bg);
  border: 0.5px solid var(--df-border);
  border-radius: var(--df-radius-sm);
  box-shadow: 0 2px 8px rgba(0,0,0,0.1);
  padding: 4px 0;
  margin-top: 2px;
}
.ai-history-inline-item {
  display: flex;
  align-items: flex-start;
  gap: 6px;
  padding: 4px 8px;
  font-size: 11px;
  cursor: pointer;
  color: var(--df-text-secondary);
  transition: background 0.1s;
}
.ai-history-inline-item:hover {
  background: var(--df-bg-card);
  color: var(--df-text);
}
.ai-history-index {
  flex-shrink: 0;
  font-size: 10px;
  color: var(--df-text-dim);
  min-width: 16px;
  text-align: left;
  font-family: var(--df-font-mono);
}
.ai-history-text {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  line-height: 1.4;
}
.ai-history-time {
  flex-shrink: 0;
  font-size: 10px;
  color: var(--df-text-dim);
  font-family: var(--df-font-mono);
}

/* ═══ 上下文摘要(底部) ═══ */
.ai-summary-inline {
  position: relative;
  display: inline-flex;
  align-items: center;
}
.ai-summary-inline-list {
  position: absolute;
  left: auto; right: 0;
  top: 100%;
  z-index: 100;
  min-width: 280px;
  max-width: 420px;
  max-height: 360px;
  overflow-y: auto;
  background: var(--df-bg);
  border: 0.5px solid var(--df-border);
  border-radius: var(--df-radius-sm);
  box-shadow: 0 2px 8px rgba(0,0,0,0.1);
  padding: 6px 0;
  margin-top: 2px;
}
.ai-summary-inline-item {
  padding: 6px 10px;
  cursor: pointer;
  border-bottom: 0.5px solid var(--df-border);
  transition: background 0.1s;
}
.ai-summary-inline-item:last-child { border-bottom: none; }
.ai-summary-inline-item:hover {
  background: var(--df-bg-card);
}
.ai-summary-label {
  display: block;
  font-size: 10px;
  font-weight: 600;
  color: var(--df-accent);
  margin-bottom: 2px;
}
.ai-summary-preview {
  margin: 0;
  font-size: 11px;
  color: var(--df-text-secondary);
  line-height: 1.5;
  display: -webkit-box;
  -webkit-line-clamp: 3;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

/* ═══ L3 完整上下文(底部) ═══ */
.ai-context-inline {
  position: relative;
  display: inline-flex;
  align-items: center;
}
.ai-context-inline-list {
  position: absolute;
  left: auto; right: 0;
  top: 100%;
  z-index: 100;
  min-width: 280px;
  max-width: 420px;
  background: var(--df-bg);
  border: 0.5px solid var(--df-border);
  border-radius: var(--df-radius-sm);
  box-shadow: 0 2px 8px rgba(0,0,0,0.1);
  margin-top: 2px;
}
.ai-context-section {
  padding: 6px 10px;
  border-bottom: 0.5px solid var(--df-border);
}
.ai-context-section:last-child { border-bottom: none; }
.ai-context-section-title {
  font-size: 10px;
  font-weight: 600;
  color: var(--df-accent);
  margin-bottom: 4px;
}
.ai-context-section-body {
  display: flex;
  flex-direction: column;
  gap: 3px;
}
.ai-context-row {
  display: flex;
  gap: 6px;
  font-size: 11px;
}
.ai-context-label {
  flex-shrink: 0;
  color: var(--df-text-dim);
  min-width: 56px;
}
.ai-context-value {
  color: var(--df-text-secondary);
  word-break: break-all;
}

/* header 中间区:provider + 模型选择器并排 */
.ai-header-center {
  display: flex;
  align-items: center;
  gap: 6px;
  flex: 1;
  justify-content: center;
  min-width: 0;
}
.ai-goals-inline-list {
  position: absolute;
  top: 100%;
  left: auto; right: 0;
  z-index: 100;
  min-width: 200px;
  max-width: 360px;
  background: var(--df-bg, #fff);
  border: 0.5px solid var(--df-border);
  border-radius: var(--df-radius-sm, 4px);
  box-shadow: 0 2px 8px rgba(0,0,0,0.1);
  padding: 4px 0;
  margin-top: 2px;
}
.ai-goal-inline-item {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 4px 8px;
  font-size: 11px;
  cursor: pointer;
  border-radius: 2px;
}
.ai-goal-inline-item:hover {
  background: var(--df-bg-card);
}
.ai-goal-text {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.ai-goal-dot { width: 6px; height: 6px; border-radius: 50%; background: var(--df-success); flex-shrink: 0; }
.ai-goal-check { color: var(--df-success); font-weight: bold; flex-shrink: 0; }
.ai-goal-remove { border: none; background: transparent; color: var(--df-text-dim); cursor: pointer; padding: 0 2px; font-size: 13px; line-height: 1; flex-shrink: 0; }
.ai-goal-remove:hover {
  color: var(--df-danger, #e5484d);
}
.provider-hint {
  font-size: 11px;
  color: var(--df-danger);
}
/* UX-2025-14: Provider 切换临时 bar(淡入淡出 0.15s) */
.ai-provider-switch-bar {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 4px 14px;
  background: color-mix(in srgb, var(--df-accent) 12%, transparent);
  border-bottom: 0.5px solid color-mix(in srgb, var(--df-accent) 25%, transparent);
  font-size: 11px;
  color: var(--df-accent);
  user-select: none;
}
.ai-provider-switch-dot {
  width: 5px;
  height: 5px;
  border-radius: 50%;
  background: var(--df-accent);
  flex-shrink: 0;
  animation: ai-agentic-pulse 1s ease-in-out infinite;
}
.ai-provider-switch-text {
  font-family: var(--df-font-mono);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.ai-provider-switch-enter-active,
.ai-provider-switch-leave-active { transition: opacity 0.15s var(--df-ease), max-height 0.15s var(--df-ease); }
.ai-provider-switch-enter-from,
.ai-provider-switch-leave-to { opacity: 0; max-height: 0; padding-top: 0; padding-bottom: 0; }
.ai-provider-switch-enter-to,
.ai-provider-switch-leave-from { opacity: 1; max-height: 32px; }
</style>
