<template>
  <div class="ai-panel">
    <!-- ═══ 对话侧边栏(第一批抽离至 ai/ConversationSidebar.vue,零行为变更) ═══ -->
    <ConversationSidebar
      ref="convSidebarRef"
      @confirm-delete="confirmDeleteConversation"
      @confirm-new-conversation="confirmNewConversation"
    />
    <!-- ═══ 聊天区域 ═══ -->
    <div class="ai-chat-area">
      <!-- ═══ 第三批抽离至 ai/TopBar.vue(顶部工具栏:provider/model 选择器 + 标题 + 审批角标 + 顶部操作 + provider bar,零行为变更) ═══ -->
      <TopBar
        :detached="props.detached"
        :has-active-messages="hasActiveMessages"
        :pending-approval-count="pendingApprovalCount"
        :always-on-top="alwaysOnTop"
        @toggle-sidebar="store.toggleSidebar()"
        @new-conversation="confirmNewConversation"
        @clear-context="handleClearContext"
        @compress-context="handleCompressContext"
        @clear-chat="confirmClearChat"
        @scroll-to-pending="scrollToFirstPending"
        @toggle-maximize="store.toggleMaximize()"
        @detach="store.detachPanel()"
        @close-panel="store.togglePanel()"
        @dock="store.dockDetached()"
        @toggle-always-on-top="toggleAlwaysOnTop"
        @close-detached="store.closeDetachedWindow()"
        @provider-switched="(name: string) => showToast(t('aiChat.providerSwitched', { model: name }), 'info')"
      />

      <!-- 第四批抽离至 ai/MessageList.vue(消息列表 + 流式 Markdown 分块渲染 + 工具卡转发 +
           折叠归档段 + 滚动跟随/回底部,零行为变更,store/useMarkdown 单例共享) -->
      <MessageList
        ref="messageListRef"
        :editing-msg-id="editingMsgId"
        @start-edit="startEdit"
        @send-example-prompt="sendExamplePrompt"
        @go-to-settings="goToSettings"
        @toast="(p) => showToast(p.msg, p.type)"
      />

      <!-- Input -->
      <div class="ai-input-area">
        <!-- AE-2025-07: Agentic 循环进度条
             仅 agentic 多轮(streaming 且收到过 AiAgentRound round>0 事件)显示;
             单轮问答 agentRound 恒 0,不显示(避免误显)。
             可用字段:round(后端 AiAgentRound.round)、pendingApprovals(前端 state)。
             降级省略:
               - max(总轮次):后端 AiAgentRound 事件未透传 max_rounds,本轮不显;
                 TODO 后端 AiAgentRound 补 max_rounds 字段后新增 *WithMax i18n key + 条件渲染。
               - completed(已完成工具数):前端无累计计数器,本轮不显;
                 TODO 加 completed counter(AiToolCallCompleted 时按当前对话累计)。 -->
        <div v-if="showAgenticProgress" class="ai-agentic-progress">
          <span class="ai-agentic-progress-dot"></span>
          <span class="ai-agentic-progress-text">{{ agenticProgressText }}</span>
        </div>
        <!-- 第五批抽离至 ai/MaxRoundsCard.vue(F-260616-03 达 max_iterations 暂停态操作卡,零行为变更,store 单例 + pendingMaxRounds 模块级 ref 共享,toast 经 emit 转父) -->
        <MaxRoundsCard @toast="(p) => showToast(p.msg, p.type)" />
        <!-- L1 求助协议(aichat 体验与 agent 能力系统化重构 §2.3,2026-06-21):
             断路器熔断 emit AiHelpRequired → pendingHelp(模块级 ref)→ 本卡显 reason + options 按钮供用户选。
             机制优先 prompt 说教:代码强制熔断 + 结构化求助卡(非教 AI 主动问用户)。 -->
        <HelpRequiredCard @toast="(p) => showToast(p.msg, p.type)" />
        <!-- F-260619-03 Phase B: 路径授权弹窗(LLM 访问白名单外目录挂起,三选项 once/always/deny)。
             path_auth 审批链阶段3b:统一审批开关 df-ai-unified-approval 开(true,默认)时下线——
             path 挂起归一进 ToolCard 内联审批(单真相源:状态层合);关(false,兜底回退)时挂载
             DirAuthDialog 独立弹窗老链路。两路共存,appSettings 随时回退无需改代码。 -->
        <DirAuthDialog v-if="!unifiedApproval" @toast="(p) => showToast(p.msg, p.type)" />
        <!-- 待发送队列（生成中排队的消息，完成后自动续发） -->
        <div v-if="store.state.queue.length > 0" class="ai-queue" :class="{ 'ai-queue--timeout': queueTimedOut }">
          <div class="ai-queue-head">
            <span class="ai-queue-title">{{ $t('aiChat.queueTitle', { n: store.state.queue.length }) }}<span v-if="queueWaitSeconds >= 0" class="ai-queue-timer">({{ queueWaitSeconds }}s)</span></span>
            <div class="ai-queue-actions">
              <button v-if="queueTimedOut" class="ai-queue-force" @click="handleForceSend">{{ $t('ai.forceSendBtn') }}</button>
              <button class="ai-queue-clear" @click="store.clearQueue">{{ $t('aiChat.clearQueue') }}</button>
            </div>
          </div>
          <div class="ai-queue-list">
            <div v-for="(item, idx) in store.state.queue" :key="idx" class="ai-queue-item">
              <!-- UX-260616-06 决策只编 text:编辑态切 inline input,回车/失焦写回,ESC 取消 -->
              <template v-if="editingQueueIdx === idx">
                <span v-if="item.skill" class="ai-queue-skill">/{{ item.skill }}</span>
                <input
                  v-model="editingQueueText"
                  class="ai-queue-edit-input"
                  type="text"
                  @keydown.enter.prevent="saveEditQueued"
                  @keydown.esc.prevent="cancelEditQueued"
                  @blur="saveEditQueued"
                />
                <button class="ai-queue-btn" @mousedown.prevent="saveEditQueued" :title="$t('aiChat.queueSave')">✓</button>
                <button class="ai-queue-btn" @mousedown.prevent="cancelEditQueued" :title="$t('aiChat.queueCancel')">✕</button>
              </template>
              <template v-else>
                <span v-if="item.skill" class="ai-queue-skill">/{{ item.skill }}</span>
                <span class="ai-queue-text">{{ item.text }}</span>
                <!-- UX-260616-06:编辑按钮 -->
                <button class="ai-queue-btn" @click="startEditQueued(idx)" :title="$t('aiChat.queueEdit')">{{ $t('aiChat.queueEdit') }}</button>
                <!-- UX-260616-07 决策 a:立即发送(打断当前+发本条) -->
                <button class="ai-queue-btn" @click="handleSendQueuedNow(idx)" :title="$t('aiChat.queueSendNow')">{{ $t('aiChat.queueSendNow') }}</button>
                <button class="ai-queue-x" @click="store.cancelQueued(idx)" :title="$t('common.cancel')">×</button>
              </template>
            </div>
          </div>
        </div>
        <!-- 第二批抽离: ChatInput 子组件(输入框+附件+技能/实体联想+发送/停止) -->
        <ChatInput
          ref="chatInputRef"
          :editing-msg-id="editingMsgId"
          @sent="scrollToBottom"
          @error="onChatInputError"
          @cancel-edit="cancelEdit"
        />
      </div>
    </div>
    <!-- 确认弹层（删对话等不可逆操作） -->
    <ConfirmDialog
      :visible="confirmState.visible"
      :msg="confirmState.msg"
      :danger-label="confirmState.dangerLabel"
      @result="answerConfirm"
    />
    <!-- 错误 toast(零依赖,复用 Settings.vue 同款 reactive toast;分离窗口无 App.vue 根 toast,故本组件自管) -->
    <Transition name="ai-toast">
      <div v-if="toast.visible" class="ai-toast" :class="'ai-toast-' + toast.type">{{ toast.msg }}</div>
    </Transition>
  </div>
</template>

<script setup lang="ts">
import { ref, computed, nextTick, onMounted, onBeforeUnmount, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import { useRouter } from 'vue-router'
import { listen } from '@tauri-apps/api/event'
import { useAiStore } from '../stores/ai'
import { useAppSettingsStore } from '../stores/appSettings'
import ConfirmDialog from './ConfirmDialog.vue'
import ConversationSidebar from './ai/ConversationSidebar.vue'
import ChatInput from './ai/ChatInput.vue'
import TopBar from './ai/TopBar.vue'
import MessageList from './ai/MessageList.vue'
import MaxRoundsCard from './ai/MaxRoundsCard.vue'
import HelpRequiredCard from './ai/HelpRequiredCard.vue'
import DirAuthDialog from './ai/DirAuthDialog.vue'
import { useConfirm } from '../composables/useConfirm'
import { useToast } from '../composables/useToast'
import { useProjectStore } from '../stores/project'
import type { AiMessage } from '../api/types'

const props = defineProps<{
  detached?: boolean
}>()


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

// path_auth 审批链阶段3b(统一审批模型):df-ai-unified-approval 开关响应式 ref。
// 开(true,默认):path 挂起归一进 ToolCard 内联审批(once/always/deny),不挂 DirAuthDialog。
// 关(false,兜底回退):挂 DirAuthDialog 独立弹窗老链路(useAiEvents AiDirAuthRequired case 走 pendingDirAuths)。
const appSettings = useAppSettingsStore()
const unifiedApproval = appSettings.useSetting<boolean>('df-ai-unified-approval', true)

// ── UX-2025-20: 空状态示例问题随 MessageList 子组件迁移(examplePrompts 数组在子组件内);
// 父仅保留 sendExamplePrompt 转发(子 emit 'send-example-prompt' → 父委托 ChatInput.sendExamplePrompt)。──

/** 跳 Settings 配置 provider:嵌入模式 router.push;分离窗口无 Settings 路由,降级 toast 提示 */
function goToSettings() {
  if (props.detached) {
    showToast(t('aiChat.goToSettingsHint'), 'warning')
    return
  }
  router.push('/settings')
}

/** 点击示例问题卡片:委托 ChatInput 子组件发送(填入输入框 + handleSend 同款路径) */
function sendExamplePrompt(i18nKey: string) {
  chatInputRef.value?.sendExamplePrompt(i18nKey)
}

// ── 错误 toast(useToast composable,消除 AiChat/Settings/Projects/TaskDetail 4 处重复) ──
// AiChat 可能以分离窗口运行(App.vue 根 toast 不在 DOM 中),故本组件自管一个 toast;
// i18n 不在本任务白名单,文案硬编码,后续补 aiChat.toastSendFail key
const { toast, showToast } = useToast()

// B-260616-12: 工具慢执行提示事件 unlistener(onMounted 注册,onBeforeUnmount 释放)
let _unlistenToolSlow: (() => void) | null = null
// AE-2025-04: 会话信任自动放行 toast unlistener(useAiEvents 经事件总线中转,同 _unlistenToolSlow 模式)
let _unlistenAutoApproved: (() => void) | null = null

// 第二批抽离: inputText/inputEl/图片输入/技能/实体联想/handleSend 已迁移至 ChatInput.vue
// chatInputRef: 父调 expose(focus/startEditFocus/clearInput/hasUnsentInput/sendExamplePrompt)
const chatInputRef = ref<InstanceType<typeof ChatInput> | null>(null)

// editingMsgId 仍由父持有(startEdit/cancelEdit 在父,经 prop 透传 ChatInput)
const editingMsgId = ref<string | null>(null)

/** ChatInput emit('error') 转发到本组件 toast(保持单一 toast 源) */
function onChatInputError(payload: { msg: string; type: 'error' | 'warning' | 'info' }): void {
  showToast(payload.msg, payload.type)
}

// 第四批抽离: MessageList 子组件引用(expose scrollToBottom/scrollToFirstPending)
const messageListRef = ref<InstanceType<typeof MessageList> | null>(null)

// AE-2025-02:头部审批徽标 — 当前对话积压的待审批工具数(>0 时显示)
const pendingApprovalCount = computed(() => store.state.pendingApprovals.length)

// AE-2025-07: Agentic 循环进度条
// 仅 agentic 多轮(streaming 且 agentRound>0)显示;单轮问答不收 AiAgentRound 故恒隐藏。
const showAgenticProgress = computed(() =>
  store.state.streaming && store.state.agentRound > 0,
)
// 进度文案:round 必显;pending>0 时附加待审批数(max/completed 本轮降级省略,见模板 TODO)
const agenticProgressText = computed(() => {
  const pending = store.state.pendingApprovals.length
  const completed = store.state.completedTools
  return pending > 0
    ? t('ai.agenticProgress', { round: store.state.agentRound, pending })
    : t('ai.agenticProgressNoPending', { round: store.state.agentRound, completed })
})

/** 点击徽标:滚动到第一个 pending_approval 卡片(委托 MessageList.scrollToFirstPending) */
function scrollToFirstPending(): void {
  messageListRef.value?.scrollToFirstPending()
}

// ── 确认弹层（删对话等不可逆操作，复用 ConfirmDialog 组件） ──
// 状态机抽至 composables/useConfirm(原 Projects/ProjectDetail/Ideas/Settings/AiChat 各重复一份)
// 模板 confirmState.visible/msg 在 <script setup> 中自动解包 ref,绑定 ConfirmDialog 不变
const { confirmState, confirmDialog, answerConfirm } = useConfirm()
async function confirmDeleteConversation(id: string) {
  const conv = store.state.conversations.find(c => c.id === id)
  if (!conv) console.warn('confirmDeleteConversation: conv not found, id=', id)
  const title = conv?.title || t('aiChat.newConversation')
  // UX-260617-24:删除当前活跃对话加醒目提示(通用文案不区分,活跃对话误删影响更大)。
  const isActive = id === store.state.activeConversationId
  const msgKey = isActive ? 'aiChat.confirmDeleteActiveConversation' : 'aiChat.confirmDeleteConversation'
  if (!await confirmDialog(t(msgKey, { title }))) return
  store.deleteConversation(id)
}

/** 清空当前对话(真删 DB messages,带二次确认防误删) */
async function confirmClearChat() {
  if (!await confirmDialog(t('aiChat.confirmClearChat'))) return
  await store.clearChat()
}

/**
 * 新建对话(UX-08 §3.4 / F-09 决策e):新建 = createConversation + 切 active,
 * 旧 conv 后台 loop 继续(后端 ai_conversation_create 决策e 不杀旧 loop),与 switchConversation
 * 同属真并发语义——不弹「中断并新建」确认(与并行矛盾)。对齐 switchConversation:89-91
 * 「允许生成中切换:后台继续生成,事件按 conversation_id 路由」。
 *
 * 历史:B-260615-10 单 loop 软复位弹中断确认,F-09 决策e 上线后已废弃(A 路线遗留)。
 */
async function confirmNewConversation() {
  try {
    await store.newConversation()
  } catch (e) {
    console.error('[AI] 新建对话失败:', e)
    // 用错误气泡反馈用户，不静默吞掉
  }
}

// 第二批抽离: 技能 `/` 联想 + @ 实体引用联想 + watch(inputText) 已迁移至 ChatInput.vue
// projectsStore 仍由父持有(onMounted 预加载 projects/tasks 供子组件 @ 联想读取)
const projectsStore = useProjectStore()

// UX-09:切换对话/新对话时取消编辑态(编辑态绑定末条 user,切走后无效)
// 2026-06-18 改:打开/切换会话默认进 specify 模式并选中权重最高 model(用户诉求:默认确定选中、
// 不随机/不摇摆)。enabledModels 已按 weight 降序,[0]=最高。空池(未拉取)落 null=auto 兜底。
// 后端 run_agentic_loop 对 override 兜底校验池内才用,前端预选无副作用。
// 第三批抽离: 默认 model 选中逻辑(modelOverride=enabledModels[0])随 enabledModels 迁移至
// TopBar.vue 的 activeConversationId watch;本组件仅保留编辑态取消。
// 滚动边沿/跟随意图重置(SW-260618-18/B-260618-24)随 MessageList 迁移,由子组件自管。
watch(() => store.state.activeConversationId, () => {
  if (editingMsgId.value) cancelEdit()
})



// 对话侧栏子组件引用(Ctrl+K 快捷键调 focusSearch;子组件自管重命名/导出/拖拽/搜索/分组)
const convSidebarRef = ref<InstanceType<typeof ConversationSidebar> | null>(null)

// 第三批抽离: activeProviderName/activeProviderRecord/enabledModels/isSpecifyMode/
// selectedModelId/modelPickerHint/modelOptionLabel/setAutoMode/setSpecifyMode/onModelSelect/
// cycleProvider/providerBarVisible/showProviderSwitched 已迁移至 ai/TopBar.vue
// (顶部工具栏 + provider/model 选择逻辑,零行为变更,store 单例共享 modelOverride)。

/** UX-09:进入编辑态 — 置 editingMsgId(透传 ChatInput prop)+ 委托 ChatInput.startEditFocus 回填/聚焦。 */
function startEdit(msg: AiMessage): void {
  editingMsgId.value = msg.id
  chatInputRef.value?.startEditFocus(msg)
}

/** UX-09:取消编辑态(切换对话/清空/ESC)。清 editingMsgId + 委托 ChatInput.clearInput 清输入。 */
function cancelEdit(): void {
  editingMsgId.value = null
  chatInputRef.value?.clearInput()
}

// ── 全局快捷键(window 级,非输入框内 handleKeydown) ──
// 只绑不与浏览器/WebView2 默认冲突的键:
// - Ctrl+L(浏览器聚焦地址栏——WebView2 内测,preventDefault 通常有效)
// - Ctrl+B(无浏览器默认冲突)
// - Esc(无默认冲突)
// Ctrl+N/Ctrl+R:Tauri WebView2 拦截了浏览器默认行为,preventDefault 可靠
// 注:Ctrl+K(UX-06 §3.1)已绑——Tauri WebView2 无浏览器地址栏,聚焦搜索框无冲突
function onGlobalKeydown(e: KeyboardEvent) {
  const ctrl = e.ctrlKey || e.metaKey
  // Ctrl+N: 新建对话(Tauri 下 preventDefault 可靠,无浏览器新窗口冲突)
  if (ctrl && (e.key === 'n' || e.key === 'N')) {
    e.preventDefault()
    if (!store.state.streaming) store.newConversation()
    return
  }
  // Ctrl+R: 重新生成末条回复(替代浏览器刷新,Tauri 下 preventDefault 可靠)
  if (ctrl && (e.key === 'r' || e.key === 'R')) {
    e.preventDefault()
    if (!store.state.streaming) store.regenerate()
    return
  }
  // Ctrl+L: 清空当前对话(带 confirm;生成中跳过避免删流式中消息)
  if (ctrl && (e.key === 'l' || e.key === 'L')) {
    e.preventDefault()
    if (!store.state.streaming) {
      confirmClearChat()
    }
    return
  }
  // UX-06 §3.1: Ctrl+K 聚焦搜索框(Tauri WebView2 无浏览器地址栏,preventDefault 可靠;
  // 侧栏未开时先 toggleSidebar 展开,再聚焦)
  if (ctrl && (e.key === 'k' || e.key === 'K')) {
    e.preventDefault()
    if (!store.state.sidebarOpen) store.toggleSidebar()
    // 经 ConversationSidebar expose 的 focusSearch 聚焦搜索框(等价原 searchInputRef.value?.focus())
    nextTick(() => convSidebarRef.value?.focusSearch())
    return
  }
  // Ctrl+B: 切侧栏宽度/显隐(复用 store.toggleSidebar)
  if (ctrl && (e.key === 'b' || e.key === 'B')) {
    e.preventDefault()
    store.toggleSidebar()
    return
  }
  // Esc: 关闭当前弹窗/面板(优先级:confirmDialog > 分离窗口 > 嵌入面板 > noop)
  if (e.key === 'Escape') {
    if (confirmState.value.visible) {
      e.preventDefault()
      answerConfirm(false) // 视为取消,resolve Promise 打断 confirmClearChat/confirmDelete
      return
    }
    if (props.detached) {
      e.preventDefault()
      store.closeDetachedWindow()
      return
    }
    // 嵌入面板:store.togglePanel 关面板(noop 兜底——Esc 在输入框由 handleKeydown 处理技能浮层)
    // 不主动关嵌入面板:Esc 在输入框频繁,误关面板体验差;留 noop
    return
  }
}

/**
 * UX-260617-16:未发送输入保护。检测 inputText/pendingImages/pendingSkill 任一非空,
 * 用于 beforeunload 拦截(分离窗口关闭/刷新)与未来 onBeforeUnmount 拦截(面板切换)。
 */
function hasUnsentInput(): boolean {
  return chatInputRef.value?.hasUnsentInput() ?? false
}

/**
 * beforeunload 处理器:有未发送输入时阻止关闭/刷新,触发浏览器原生「确定离开?」提示。
 * 现代标准:e.preventDefault()(Chrome 同时要求 returnValue 非空才弹原生提示,故两者都设)。
 * 注:无法自定义文案(浏览器已废弃 custom message),只能给通用离开提示。
 */
function onBeforeUnloadGuard(e: BeforeUnloadEvent) {
  if (!hasUnsentInput()) return
  e.preventDefault()
  e.returnValue = ''
}

// 第二批抽离: handleKeydown/autoResize/handleSend 已迁移至 ChatInput.vue
// (ChatInput 经 emit('sent')/emit('cancel-edit')/emit('error') 与父交互)

// ── B-260616-02 L2 发送韧性:排队超时检测 + 强制发送 ──

/** 队首排队是否已超 30s */
const queueTimedOut = computed(() => store.isQueueTimedOut())

/** 队首已排队秒数(用于 UI 展示倒计时;无队列时 -1) */
const queueWaitSeconds = computed(() => {
  const first = store.state.queue[0]
  if (!first) return -1
  return Math.floor((Date.now() - first.enqueuedAt) / 1000)
})

/** 强制发送:委托 tryForceSend 唯一负责弹 confirm + shift 队列 + force_send */
async function handleForceSend() {
  await store.tryForceSend(confirmDialog)
}

// ── UX-260616-06 队列消息编辑(决策只编 text)+ UX-260616-07 立即发送(决策 a 插队)──

/** 当前正在编辑的队列项 index;null=非编辑态 */
const editingQueueIdx = ref<number | null>(null)
/** 编辑态输入框文本(回车写回 / ESC 取消还原) */
const editingQueueText = ref('')

/** 进入编辑态:记录 index + 快照当前 text */
function startEditQueued(idx: number) {
  const item = store.state.queue[idx]
  if (!item) return
  editingQueueIdx.value = idx
  editingQueueText.value = item.text
}

/** 取消编辑:还原非编辑态,不写回 */
function cancelEditQueued() {
  editingQueueIdx.value = null
  editingQueueText.value = ''
}

/** 保存编辑:回车/失焦触发,委托 store.editQueued 写回 text */
function saveEditQueued() {
  if (editingQueueIdx.value === null) return
  store.editQueued(editingQueueIdx.value, editingQueueText.value)
  editingQueueIdx.value = null
  editingQueueText.value = ''
}

/** UX-260616-07 立即发送:委托 store.sendQueuedNow(splice+stop+sendMessage) */
async function handleSendQueuedNow(idx: number) {
  await store.sendQueuedNow(idx)
}

// 第四批抽离: 滚动跟随/回底部/流式 Markdown 分块渲染/工具卡自动收起/选区保持 全部随
// MessageList 子组件迁移(零行为变更,store/useMarkdown 单例共享)。
// 父仅保留 scrollToBottom 转发桩(ChatInput @sent / TopBar @scroll-to-pending 经 expose 调)。
function scrollToBottom(smooth = false) {
  messageListRef.value?.scrollToBottom(smooth)
}
onBeforeUnmount(() => {
  // 全局快捷键(UX-2025-07):卸载时移除 window listener,防分离窗口/切组件后泄漏
  window.removeEventListener('keydown', onGlobalKeydown)
  // UX-260617-16:摘除未发送输入保护(分离窗口关闭/页面刷新触发 beforeunload)
  window.removeEventListener('beforeunload', onBeforeUnloadGuard)
  // 注:导出菜单外部点击监听/侧栏拖拽 listener/标题淡入计时器现由 ConversationSidebar
  // 子组件 onBeforeUnmount 自管(第一批抽离),父不再持有这些引用。
  // 流式 rAF 清理(streaming watch 内 cancelAnimationFrame)随 MessageList 迁移,子组件
  // onBeforeUnmount 自管(子 watch streaming 已注册,卸载自动停)。
  // CR-260615-24: 释放后端 listener(ai-chat-event/ai-conversation-changed)+清流式看门狗。
  // 之前 stopListener 定义但全仓零调用 → listener+watchdog 永久泄漏;分离窗口主窗口 AiChat
  // 卸载(走 App.vue v-if detach)时 listener 仍挂后端,watchdog 130s 后写已卸载 state。
  store.stopListener()
  // F-15 阶段2: 释放上下文管理事件监听器
  store.stopContextListener()
  if (_unlistenToolSlow) { _unlistenToolSlow(); _unlistenToolSlow = null } // B-260616-12
  if (_unlistenAutoApproved) { _unlistenAutoApproved(); _unlistenAutoApproved = null } // AE-2025-04
  // toast timer 清理由 useToast composable 的 onUnmounted 自动管理
})

onMounted(async () => {
  // 全局快捷键(UX-2025-07):window 级 listener,卸载时移除(对齐 _unlistenToolSlow 生命周期)
  window.addEventListener('keydown', onGlobalKeydown)
  // UX-260617-16:未发送输入保护。AiChat 非路由组件(App.vue 内嵌 + AiDetached 路由),
  // beforeRouteLeave 不适用。分离窗口关闭/页面刷新时若有未发送 inputText/pendingImages/
  // pendingSkill,经 beforeunload 拦截提示用户。注意:e.preventDefault() 才是现代浏览器
  // 触发原生「确定离开?」的标准做法 returnValue 仅 Chrome 兼容旧写法。
  window.addEventListener('beforeunload', onBeforeUnloadGuard)
  // 注:导出菜单外部点击监听现由 ConversationSidebar 子组件 onMounted 自管(第一批抽离)
  const t0 = (window as any).__APP_T0 ?? 0
  // 启动耗时埋点:debug 级(浏览器默认折叠不干扰,空白屏复现时可开 verbose 看)
  console.debug(`[启动] AiChat mounted: ${(performance.now() - t0).toFixed(0)}ms`)
  // Markdown 渲染器预热随 MessageList 子组件迁移(子 onMounted 内 loadMarkdown);
  // useMarkdown 模块级单例,父不再直接调用。
  await store.startListener()
  await store.loadProviders()
  await store.loadConversations()
  store.loadSkills()
  // @ 提及预加载: AiChat 的 @ 联想读取 projectsStore.projects/tasks,
  // 但 loadProjects/loadTasks 分别绑在 Projects.vue/Tasks.vue 的 onMounted。
  // 若用户启动后直接用 AI Chat(未访问过这两个页面), store 数据为空 → @ 无匹配。
  // 此处按需预加载(已加载则跳过), 不阻塞首屏(无 await, 后台执行)。
  if (!projectsStore.projects.length) void projectsStore.loadProjects()
  if (!projectsStore.tasks.length) void projectsStore.loadTasks()
  // B-260616-12: 监听工具慢执行提示(useAiEvents 经 Tauri 事件总线广播;composable 无组件上下文,
  // 由本组件弹本地 toast。主窗口与分离窗口各挂一份 AiChat,各自消费,无重复——emit 单发,
  // 两窗口的 listener 各收一次但 msg 相同,toast 各自渲染互不干扰)
  _unlistenToolSlow = await listen<{ name: string }>('ai-tool-slow-toast', (e) => {
    showToast(t('aiTool.executionSlow', { name: e.payload.name }), 'warning')
  })
  // AE-2025-04: 会话信任自动放行 toast(轻量 info,非审批气泡,避免干扰主流程)
  _unlistenAutoApproved = await listen<{ tool: string; dir: string }>('ai-tool-auto-approved-toast', (e) => {
    showToast(t('aiChat.autoApprovedToast', { tool: e.payload.tool, dir: e.payload.dir }), 'info')
  })
  // F-15 阶段2: 注册上下文管理事件监听器(清空/压缩生命周期事件 → toast/刷新)
  await initContextEventListeners()
  // 恢复生成中态(刷新后/分离窗口接管):
  // - 分离模式(detached=true):resumeInDetached 接管主窗口当前对话
  // - 主面板模式(detached=false):restoreGeneratingState 恢复主面板自身的生成态
  //   (UX-260617-13: 主面板刷新后无等价恢复链 → streaming=false + 无占位气泡 + 后端
  //   事件不进流式分支,退化为非流式。现对齐 resumeInDetached 同款恢复链)。
  // 两者互斥(detached 三元决定),不会双重恢复;restoreGeneratingState 内幂等守卫(state.streaming 已 true 则跳过)。
  // 在 startListener(L0 ai-client-ready 握手)之后调用,握手仍先于恢复逻辑生效。
  if (props.detached) {
    const params = new URLSearchParams(location.hash.split('?')[1] || '')
    await store.resumeInDetached(params.get('conv'))
  } else {
    // fromMainPanel=true: 主面板刷新无 df-ai-gen 快照(detachPanel 未触发),
    // 用 activeConversationId + ai_is_generating 真值守卫恢复(绕过 localStorage 路径)
    await store.restoreGeneratingState({ fromMainPanel: true })
  }
  console.debug(`[启动] AiChat IPC 完成: ${(performance.now() - t0).toFixed(0)}ms`)
})

// ── 置顶功能(仅分离窗口有意义;持久化:刷新后保持置顶状态图标一致)──
// 关键:置顶只对「分离窗口」生效。AiChat 在主窗口(App.vue)与分离窗口(AiDetached.vue)都挂载,
// 若不区分,主窗口启动时会因 KV=true 把【整个主窗口】置顶到所有应用之上(BUG)。
// 主窗口内 AI 是 docked 面板,主窗口本身不该置顶。故 restore/apply 一律 gate 在 props.detached。
const alwaysOnTop = ref(false)
const appSettingsStore = useAppSettingsStore()
// 从 SQLite KV 恢复上次置顶状态(仅分离窗口读用;主窗口读了也无副作用,ref 仅驱动按钮 active 态)
const savedAlwaysOnTop = appSettingsStore.get<boolean>('df-ai-always-on-top', false)
alwaysOnTop.value = props.detached ? savedAlwaysOnTop : false
// 同步到 Tauri 窗口:分离窗口按 KV 还原;主窗口显式清置顶(防御历史误设/窗口状态残留)。
// 注意:此处不可用顶层 await —— <script setup> 顶层 await 会使本组件变成 async setup,
// 而 App.vue / AiDetached.vue 直接挂载 <AiChat /> 未包 <Suspense>,会导致渲染空白。
// 用 .then() fire-and-forget,既不阻塞初始化流程,又保持组件同步 setup。
import('@tauri-apps/api/webviewWindow').then(({ getCurrentWebviewWindow }) =>
  getCurrentWebviewWindow()
    .setAlwaysOnTop(props.detached ? savedAlwaysOnTop : false)
    .catch(() => {/* 静默:webview 不可用时忽略 */}),
)
async function toggleAlwaysOnTop() {
  const { getCurrentWebviewWindow } = await import('@tauri-apps/api/webviewWindow')
  const win = getCurrentWebviewWindow()
  await win.setAlwaysOnTop(!alwaysOnTop.value)
  alwaysOnTop.value = !alwaysOnTop.value
  // 持久化到 SQLite KV(刷新后恢复)
  appSettingsStore.set('df-ai-always-on-top', alwaysOnTop.value)
}

// 第五批抽离: F-260616-03 max_iterations 暂停态操作卡(maxRoundsActing/showMaxRoundsCard/
// handleContinueLoop/handleStopLoop/pendingMaxRounds watch)已迁移至 ai/MaxRoundsCard.vue
// (零行为变更,store 单例 + pendingMaxRounds 模块级 ref 共享,toast 经 emit 转父)。

// ── F-15 阶段2: 手动上下文管理(清空 / 压缩) ──
// ChatMessage.status 渲染语义(types.ts AiMessage.status 字面量联合定义):
//   null | undefined | 'active'        → 正常消息(现状)
//   'archived_segment'                  → 清空上下文归档标记 → 折叠成"已归档 N 条"分隔条
//   'compressed'                        → 压缩标记 → 折叠成"已压缩(展开看摘要)"块
//   'truncated'                         → switchConversation 已过滤,不到这里
// 连续同 status 合并一条分隔条(避免每条都折叠条 → N 条归档只显 1 条分隔条)。

// 第四批抽离: 消息列表分段(messageSegments/renderItems)+ 折叠段展开(expandedSegmentIds/
// toggleSegment)+ MessageSegment/RenderItem 类型 全部随 MessageList 子组件迁移(零行为变更)。

/**
 * 是否存在"活跃消息"(可清空/可压缩)— 控制清空/压缩按钮启用。
 * archived(已清空归档) + compressed(已压缩,后端不再发给 LLM)都算已脱离上下文,不计活跃。
 * 空会话 / 全归档 / 全压缩 → 无活跃 → 按钮禁用(不发起无意义 IPC)。
 */
const hasActiveMessages = computed(() =>
  store.state.messages.some(m => {
    const s = m.status
    return s !== 'archived_segment' && s !== 'compressed'
  }),
)


/**
 * 清空当前对话上下文(二次确认 → aiApi.clearContext)。
 * 历史消息后端标 archived_segment 归档保留,DB 不删;新对话不受影响。
 * 二次确认:不可逆操作,危险按钮显式「清空」(dangerLabel)。
 */
async function handleClearContext(): Promise<void> {
  const convId = store.state.activeConversationId
  if (!convId) return
  if (store.state.streaming) return // 生成中禁操作,避免与流式事件竞争
  if (!hasActiveMessages.value) return // 无活跃消息(空/全归档)禁操作
  // 二次确认:不可逆操作,危险按钮显式「清空」(不回退通用「确认」)
  if (!await confirmDialog(t('aiChat.clearContextConfirm'), t('aiChat.clearContextDangerLabel'))) return
  try {
    await store.clearContext()
  } catch (e) {
    const msg = e instanceof Error ? e.message : String(e)
    showToast(t('aiChat.clearError', { msg }), 'error')
  }
}

/**
 * 压缩当前对话上下文(LLM 摘要落 system + 历史消息标 compressed)。
 * loading 由 store.isCompressing 控制(防重复点击);失败走 ai_error 事件或 catch。
 */
async function handleCompressContext(): Promise<void> {
  if (store.isCompressing.value) return // loading 中禁重复点击
  if (store.state.streaming) return // 生成中禁操作
  if (!hasActiveMessages.value) return // 无活跃消息禁操作
  try {
    await store.compressContext()
  } catch (e) {
    const msg = e instanceof Error ? e.message : String(e)
    showToast(t('aiChat.compressError', { msg }), 'error')
  }
}

/**
 * 注册上下文管理事件监听器(onMounted 调),注入 toast 副作用回调。
 * - onCleared:toast 成功 + 刷新消息列表(后端会重发 conversation_changed,但本地立刷更及时)
 * - onCompressed:toast 成功(摘要 system 消息由后端 emit 后经 conversation_changed 刷新回填)
 * - onError:toast 错误(isCompressing 已在监听器内复位)
 */
async function initContextEventListeners(): Promise<void> {
  await store.initContextListener({
    onCleared: () => {
      showToast(t('aiChat.clearSuccess'), 'info')
      // 后端 emit ai_context_cleared 后会重置 session.messages,前端刷对话拉最新状态。
      // 守卫:事件到达前用户可能切走,activeConversationId 变 null,跳过刷新避免空指针。
      if (store.state.activeConversationId) {
        void store.switchConversation(store.state.activeConversationId)
      }
    },
    // 治 Task#1:仅手动压缩触发(useAiContext 的 AiManualCompressed case 调此回调);
    // 自动压缩(AiAutoCompressed)静默不进此回调,避免每次发送误弹 toast + 误刷新打断流。
    onCompressed: (_convId, _summary) => {
      showToast(t('aiChat.compressSuccess'), 'info')
      // 刷新对话(历史消息落库标 compressed + system 摘要消息回填到视图)
      if (store.state.activeConversationId) {
        void store.switchConversation(store.state.activeConversationId)
      }
    },
    onError: (_convId, message) => {
      showToast(t('aiChat.compressError', { msg: message }), 'error')
    },
  })
}
</script>

<style scoped>
/* ═══ AiChat 容器 scoped(God 拆分遗漏修复:侧栏/输入/消息样式已下沉到对应子组件 scoped,
     复用按钮抽 components.css 全局。本块仅保留 AiChat 自身元素:面板布局/聊天区/input-area
     容器/agentic 进度/待发队列/toast。子组件元素由各子组件 scoped 负责,父级 scoped 不渗透。) ═══ */
.ai-panel {
  position: relative; /* 容纳 .ai-toast(absolute,相对面板定位,嵌入/分离模式均生效) */
  display: flex;
  flex-direction: row;
  height: 100%;
  background: var(--df-bg);
  border-left: 0.5px solid var(--df-border);
}

/* ═══ 聊天区域 ═══ */
.ai-chat-area {
  position: relative;
  flex: 1;
  display: flex;
  flex-direction: column;
  height: 100%;
  min-width: 0;
}

/* (Header/Provider Bar/模型选择器/.ai-btn-icon 样式已下沉:
     TopBar.vue scoped 自持 + .ai-btn-icon* 抽 components.css 全局共享,
     原 AiChat scoped 不渗透子组件根,删此重复块零视觉变更) */

/* ═══ Input ═══ */
.ai-input-area {
  padding: 10px 14px;
  border-top: 0.5px solid var(--df-border);
  background: var(--df-bg);
}
/* 第五批抽离: F-260616-03 max_iterations 操作卡样式已迁移至 ai/MaxRoundsCard.vue */

/* ── AE-2025-07: Agentic 循环进度条 ── */
.ai-agentic-progress {
  display: flex;
  align-items: center;
  gap: 6px;
  margin-bottom: 6px;
  padding: 3px 8px;
  background: color-mix(in srgb, var(--df-accent) 8%, transparent);
  border-radius: var(--df-radius);
  font-size: 11px;
  color: var(--df-text-dim);
  user-select: none;
}
.ai-agentic-progress-dot {
  width: 5px;
  height: 5px;
  border-radius: 50%;
  background: var(--df-accent);
  flex-shrink: 0;
  animation: ai-agentic-pulse 1.2s ease-in-out infinite;
}
.ai-agentic-progress-text {
  font-family: var(--df-font-mono);
  white-space: nowrap;
}
@keyframes ai-agentic-pulse {
  0%, 100% { opacity: 1; }
  50% { opacity: 0.3; }
}

/* ── 待发送队列 ── */
.ai-queue {
  margin-bottom: 6px;
  padding: 6px 8px;
  background: color-mix(in srgb, var(--df-accent) 8%, transparent);
  border: 0.5px solid color-mix(in srgb, var(--df-accent) 30%, transparent);
  border-radius: var(--df-radius);
}
.ai-queue--timeout {
  background: color-mix(in srgb, var(--df-warning) 10%, transparent);
  border-color: color-mix(in srgb, var(--df-warning) 40%, transparent);
}
.ai-queue-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 4px;
}
.ai-queue-title { font-size: 11px; color: var(--df-text-dim); font-weight: 600; }
.ai-queue-timer { font-weight: 400; opacity: 0.7; }
.ai-queue--timeout .ai-queue-title { color: var(--df-warning); }
.ai-queue-actions { display: flex; align-items: center; gap: 8px; }
.ai-queue-force {
  background: var(--df-warning); border: none; color: #000; cursor: pointer;
  font-size: 11px; font-weight: 600; padding: 2px 8px; border-radius: calc(var(--df-radius) - 2px);
}
.ai-queue-force:hover { filter: brightness(1.1); }
.ai-queue-clear { background: none; border: none; color: var(--df-text-dim); cursor: pointer; font-size: 11px; padding: 0; }
.ai-queue-clear:hover { color: var(--df-text); }
.ai-queue-list { display: flex; flex-direction: column; gap: 3px; }
.ai-queue-item {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 3px 6px;
  background: var(--df-bg-card);
  border-radius: calc(var(--df-radius) - 2px);
  font-size: 12px;
}
.ai-queue-skill { color: var(--df-accent); font-weight: 600; flex-shrink: 0; }
.ai-queue-text {
  color: var(--df-text);
  flex: 1;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.ai-queue-x { background: none; border: none; color: var(--df-text-dim); cursor: pointer; font-size: 14px; line-height: 1; padding: 0 2px; flex-shrink: 0; }
.ai-queue-x:hover { color: var(--df-danger); }
/* UX-260616-06 队列项编辑/立即发送按钮 + 编辑态 input */
.ai-queue-btn {
  background: none; border: none; color: var(--df-text-dim); cursor: pointer;
  font-size: 11px; line-height: 1; padding: 0 4px; flex-shrink: 0;
}
.ai-queue-btn:hover { color: var(--df-accent); }
.ai-queue-edit-input {
  flex: 1; min-width: 0; font-size: 12px; padding: 1px 4px;
  background: var(--df-bg-card); color: var(--df-text);
  border: 0.5px solid var(--df-accent);
  border-radius: calc(var(--df-radius) - 2px);
  outline: none;
}

/* (.ai-btn-icon:disabled / .ai-btn-spinner / @keyframes ai-btn-spin 已抽 components.css 全局共享) */

/* ═══ 错误 toast(复用 Settings.vue .toast 样式,前缀 ai- 防 scoped 冲突) ═══ */
.ai-toast {
  position: absolute;
  top: 16px;
  left: 50%;
  transform: translateX(-50%);
  padding: 8px 14px;
  border-radius: var(--df-radius);
  font-size: 12px;
  z-index: 100;
  box-shadow: 0 4px 16px rgba(0,0,0,0.3);
  max-width: 80%;
  overflow-wrap: anywhere;
}
.ai-toast-error { background: var(--df-danger-bg); color: var(--df-danger); border: 0.5px solid var(--df-danger); }
.ai-toast-warning { background: var(--df-warning-bg); color: var(--df-warning); border: 0.5px solid var(--df-warning); }
.ai-toast-info { background: var(--df-accent-bg); color: var(--df-accent); border: 0.5px solid var(--df-accent); }
/* P0-2: ToastType 新增 'success' 兜底样式 */
.ai-toast-success { background: var(--df-success-bg); color: var(--df-success); border: 0.5px solid var(--df-success); }
.ai-toast-enter-active, .ai-toast-leave-active { transition: opacity 0.2s, transform 0.2s; }
.ai-toast-enter-from, .ai-toast-leave-to { opacity: 0; transform: translate(-50%, -8px); }

</style>
