<template>
  <div
    class="ai-tool-card"
    :class="[
      'ai-tool-card--' + tc.status,
      { 'ai-tool-card--collapsed': !isExpanded && !shouldKeepOpen(tc), 'ai-tool-card--failed': isFailed },
    ]"
  >
    <!-- 卡片头部：图标 + 名称 + 状态 -->
    <div class="ai-tool-header" @click="emit('toggle', tc.id)">
      <span class="ai-tool-icon-wrap" :class="'ai-tool-icon-wrap--' + toolCategory(tc.name)">
        <span v-html="toolIcon(tc.name)"></span>
      </span>
      <div class="ai-tool-header-text">
        <span class="ai-tool-name">{{ toolDisplayName(tc) }}</span>
        <span v-if="tc.status === 'running'" class="ai-tool-sub">{{ $t('aiTool.running') }}</span>
        <span v-else-if="isFailed" class="ai-tool-sub ai-tool-sub--failed">{{ $t('aiTool.executionFailed') }}</span>
        <span v-else-if="toolResultSummary(tc)" class="ai-tool-sub">{{ toolResultSummary(tc) }}</span>
      </div>
      <span class="ai-tool-status-dot" :class="'ai-tool-status-dot--' + (isFailed ? 'failed' : tc.status)"></span>
      <span v-if="!shouldKeepOpen(tc)" class="ai-tool-collapse-hint" :class="{ 'is-open': isExpanded }">▸</span>
    </div>

    <!-- Body: 收起态隐藏(running/pending 强制展开) -->
    <div v-show="isExpanded || shouldKeepOpen(tc)" class="ai-tool-body">

      <!-- Running 骨架屏 -->
      <div v-if="tc.status === 'running'" class="ai-tool-skeleton">
        <div class="ai-tool-skeleton-line" style="width:60%"></div>
        <div class="ai-tool-skeleton-line" style="width:85%"></div>
        <div class="ai-tool-skeleton-line" style="width:40%"></div>
      </div>

      <!-- 审批：参数内容 + 风险提示 + 按钮 -->
      <div v-if="tc.status === 'pending_approval'" class="ai-tool-approval">
        <div v-if="toolArgsEntries(tc.args).length" class="ai-tool-approval-args">
          <div v-for="arg in toolArgsEntries(tc.args)" :key="arg.key" class="ai-tool-arg">
            <span class="ai-tool-arg-key">{{ arg.label }}</span>
            <span class="ai-tool-arg-val">{{ displayArgValue(arg) }}</span>
          </div>
        </div>
        <div v-if="tc.reason" class="ai-tool-approval-reason">⚠ {{ tc.reason }}</div>
        <!-- AE-2025-03: write_file 审批 diff 预览（红删绿增，行级）。旧文件不存在→tc.diff 为空，回退显上方 args content -->
        <div v-if="tc.diff" class="ai-tool-approval-diff">
          <pre class="ai-tool-diff-pre"><code><span v-for="(ln, idx) in diffLines" :key="idx" class="ai-tool-diff-line" :class="'ai-tool-diff-line--' + ln.kind">{{ ln.text }}{{ '\n' }}</span></code></pre>
        </div>
        <div class="ai-tool-actions">
          <button class="ai-tool-btn ai-tool-btn--approve"
                 :disabled="approving"
                 @click="onApprove(true)">
            <span v-if="approving" class="ai-tool-btn-spinner" />
            <svg v-else width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>
            {{ $t('aiTool.approve') }}
          </button>
          <button class="ai-tool-btn ai-tool-btn--reject"
                  :disabled="approving"
                  @click="onApprove(false)">
            <span v-if="approving" class="ai-tool-btn-spinner" />
            <svg v-else width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" 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>
            {{ $t('aiTool.reject') }}
          </button>
        </div>
      </div>

      <!-- 已拒绝提示 -->
      <div v-if="tc.status === 'rejected'" class="ai-tool-rejected">
        <span class="ai-tool-rejected-icon">✕</span>
        {{ tc.result || $t('aiTool.rejectedHint') }}
      </div>

      <!-- read_file 结果：代码预览 -->
      <div v-if="tc.name === 'read_file' && tc.result && tc.status === 'completed' && parsed" class="ai-tool-file-content">
        <div class="ai-tool-file-bar">
          <span class="ai-tool-file-icon" v-html="fileIcon"></span>
          <span class="ai-tool-file-path">{{ parsed?.path }}</span>
          <span class="ai-tool-file-meta">{{ parsed?.has_more
            ? $t('aiTool.linesTruncated', { shown: parsed?.returned_lines ?? 0, total: parsed?.lines ?? 0 })
            : `${parsed?.lines || 0} ${$t('aiTool.lines')}` }} · {{ formatBytes(parsed?.size) }}</span>
          <button class="ai-tool-expand-btn" @click="emit('expand-content', tc.id)">
            {{ isContentExpanded ? $t('aiTool.collapse') : $t('aiTool.expand') }}
          </button>
        </div>
        <pre class="ai-tool-file-pre" :class="{ 'ai-tool-file-pre--collapsed': !isContentExpanded }"><code>{{ parsed?.content }}</code></pre>
      </div>

      <!-- list_directory 结果：文件树 -->
      <div v-else-if="tc.name === 'list_directory' && tc.result && tc.status === 'completed' && parsed" class="ai-tool-dir-list">
        <div class="ai-tool-file-bar">
          <span class="ai-tool-file-icon ai-tool-file-icon--dir" v-html="dirIcon"></span>
          <span class="ai-tool-file-path">{{ parsed?.path }}</span>
          <span class="ai-tool-file-meta">{{ parsed?.entries?.length || 0 }} {{ $t('aiTool.items') }}</span>
          <span v-if="parsed?.truncated" class="ai-tool-file-meta ai-tool-file-meta--warn">{{ $t('aiTool.dirTruncated', { n: 1000 }) }}</span>
        </div>
        <div class="ai-tool-dir-entries">
          <div
            v-for="(entry, i) in parsed?.entries"
            :key="i"
            class="ai-tool-dir-entry"
            :class="'ai-tool-dir-entry--' + entry.type"
            :style="{ paddingLeft: `${12 + (entry.depth || 0) * 16}px` }"
          >
            <span class="ai-tool-dir-icon" :class="entry.type === 'directory' ? 'ai-tool-dir-icon--folder' : 'ai-tool-dir-icon--file'" v-html="entry.type === 'directory' ? dirIcon : fileIcon"></span>
            <span class="ai-tool-dir-name">{{ entry.name }}</span>
            <span v-if="entry.type === 'file'" class="ai-tool-dir-size">{{ formatBytes(entry.size) }}</span>
          </div>
        </div>
      </div>

      <!-- search_files 结果:匹配文件列表(对齐 list_directory 渲染,修 results 被丢弃致 body 仅显「找到 N 项」) -->
      <div v-else-if="tc.name === 'search_files' && tc.result && tc.status === 'completed' && parsed" class="ai-tool-dir-list">
        <div class="ai-tool-file-bar">
          <span class="ai-tool-file-icon ai-tool-file-icon--dir" v-html="dirIcon"></span>
          <span class="ai-tool-file-path">{{ parsed?.path }}</span>
          <span v-if="parsed?.pattern" class="ai-tool-file-meta">「{{ parsed?.pattern }}」</span>
          <span class="ai-tool-file-meta">{{ $t('aiTool.foundN', { n: typeof parsed?.total === 'number' ? parsed.total : (parsed?.results?.length || 0) }) }}</span>
        </div>
        <div class="ai-tool-dir-entries">
          <div v-for="(entry, i) in parsed?.results" :key="i" class="ai-tool-dir-entry">
            <span class="ai-tool-dir-icon ai-tool-dir-icon--file" v-html="fileIcon"></span>
            <span class="ai-tool-dir-name">{{ entry.path }}</span>
            <span class="ai-tool-dir-size">{{ formatBytes(entry.size) }}</span>
          </div>
        </div>
      </div>

      <!-- write_file 结果 -->
      <div v-else-if="tc.name === 'write_file' && tc.result && tc.status === 'completed' && parsed" class="ai-tool-write-result">
        <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="var(--df-success)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 11.08V12a10 10 0 11-5.93-9.14"/><polyline points="22 4 12 14.01 9 11.01"/></svg>
        <span class="ai-tool-write-path">{{ parsed?.path }}</span>
        <span class="ai-tool-write-meta">{{ formatBytes(parsed?.bytes_written) }} {{ $t('aiTool.written') }}</span>
        <span v-if="typeof parsed?.old_size === 'number' && parsed.old_size > 0" class="ai-tool-write-meta">{{ $t('aiTool.writeOverwrite', { old: formatBytes(parsed.old_size), neww: formatBytes(parsed?.bytes_written) }) }}</span>
        <span v-if="typeof parsed?.encoding === 'string' && parsed.encoding !== 'utf-8'" class="ai-tool-write-meta">{{ $t('aiTool.encodingLabel', { enc: parsed.encoding }) }}</span>
      </div>

      <!-- patch_file 结果:文件路径/字节增减 + unified diff 红删绿增行级预览(复用 ai-tool-diff-pre/--add/--del/--ctx 既有样式)
           UX-260618-06:后端 generate_diff 产完整 diff,result.diff 字段此前被丢弃,用户无法验证改了什么 → 此分支渲染 diff -->
      <div v-else-if="tc.name === 'patch_file' && tc.result && tc.status === 'completed' && parsed" class="ai-tool-patch-result">
        <div class="ai-tool-file-bar">
          <span class="ai-tool-file-icon" v-html="fileIcon"></span>
          <span class="ai-tool-file-path">{{ parsed?.path }}</span>
          <span class="ai-tool-file-meta">{{ formatSizeDiff(parsed?.size_diff) }} {{ $t('aiTool.bytesUnit') }}</span>
          <span v-if="parsed?.matches_found && parsed.matches_found > 1" class="ai-tool-file-meta">{{ $t('aiTool.patchedMatches', { n: parsed.matches_found }) }}</span>
        </div>
        <div v-if="parsed?.warning" class="ai-tool-approval-reason">⚠ {{ parsed.warning }}</div>
        <div v-if="resultDiffLines.length" class="ai-tool-approval-diff">
          <pre class="ai-tool-diff-pre"><code><span v-for="(ln, idx) in resultDiffLines" :key="idx" class="ai-tool-diff-line" :class="'ai-tool-diff-line--' + ln.kind">{{ ln.text }}{{ '\n' }}</span></code></pre>
        </div>
      </div>

      <!-- run_command 结果:命令行常显 + 可折叠输出(执行后仍能看到跑了什么命令) -->
      <div
        v-else-if="tc.name === 'run_command' && tc.result && tc.status === 'completed'"
        class="ai-tool-cmd-result"
      >
        <div class="ai-tool-cmd-line" :class="{ 'ai-tool-cmd-line--failed': isFailed }">
          <span class="ai-tool-cmd-prompt">$</span>
          <code class="ai-tool-cmd-text">{{ argString(tc.args, 'command') }}</code>
          <button
            v-if="cmdOutput"
            class="ai-tool-cmd-toggle"
            :title="cmdOutputExpanded ? $t('aiTool.collapse') : $t('aiTool.expand')"
            @click="cmdOutputExpanded = !cmdOutputExpanded"
          >{{ cmdOutputExpanded ? $t('aiTool.collapse') : $t('aiTool.expand') }}</button>
        </div>
        <pre v-if="cmdOutputExpanded && cmdOutput" class="ai-tool-cmd-output"><code>{{ cmdOutput }}</code></pre>
      </div>

      <!-- http_request 结果:method + url + 状态码徽标 + 可折叠响应 body(对齐 run_command 模式 UX-260618-08) -->
      <div
        v-else-if="tc.name === 'http_request' && tc.result && tc.status === 'completed' && parsed"
        class="ai-tool-http-result"
      >
        <div class="ai-tool-http-line" :class="{ 'ai-tool-http-line--failed': isFailed }">
          <span class="ai-tool-http-method">{{ parsed?.method || 'GET' }}</span>
          <code class="ai-tool-http-url">{{ parsed?.url }}</code>
          <span class="ai-tool-http-status" :class="{ 'ai-tool-http-status--ok': !isFailed }">{{ parsed?.status }}</span>
          <button
            v-if="parsed?.body"
            class="ai-tool-cmd-toggle"
            :title="httpBodyExpanded ? $t('aiTool.collapse') : $t('aiTool.expand')"
            @click="httpBodyExpanded = !httpBodyExpanded"
          >{{ httpBodyExpanded ? $t('aiTool.collapse') : $t('aiTool.expand') }}</button>
        </div>
        <div v-if="parsed?.truncated && !httpBodyExpanded" class="ai-tool-http-trunc-warn">
          ⚠ {{ $t('aiTool.httpTruncated', { bytes: formatBytes(parsed?.body_bytes) }) }}
        </div>
        <pre v-if="httpBodyExpanded && parsed?.body" class="ai-tool-cmd-output"><code>{{ parsed.body }}</code></pre>
      </div>

      <!-- 通用 CRUD 结果 -->
      <div
        v-else-if="tc.result && tc.status === 'completed'"
        class="ai-tool-result"
        :class="{ 'ai-tool-result--failed': isFailed }"
      >
        <div v-if="isFailed" class="ai-tool-failed-banner">⚠ {{ $t('aiTool.executionFailedHint') }}</div>
        <code>{{ formatToolResult(tc) }}</code>
      </div>
    </div><!-- /.ai-tool-body -->

    <!-- AE-2025-05:High 风险审批二次确认(本卡自治 useConfirm,不与 AiChat 父级 confirmState 串台) -->
    <ConfirmDialog
      :visible="confirmState.visible"
      :msg="confirmState.msg"
      :danger-label="t('common.confirm')"
      @result="answerConfirm"
    />

    <!-- UX-260617-14:审批 loading 超时兜底 toast(零依赖 reactive,复用 AiChat 同款样式) -->
    <Transition name="ai-tool-toast">
      <div v-if="approveToast.visible" class="ai-tool-toast ai-tool-toast--warning">{{ approveToast.msg }}</div>
    </Transition>
  </div>
</template>

<script setup lang="ts">
import { computed, ref, reactive, watch, onBeforeUnmount } from 'vue'
import { useI18n } from 'vue-i18n'
import { useProjectStore } from '@/stores/project'
import { useConfirm } from '@/composables/useConfirm'
import { STREAM_TIMEOUT_MS } from '@/composables/ai/useAiStream'
import {
  shouldKeepOpen,
  isToolFailure,
  formatToolName,
  shortCmd,
  toolArgsEntries,
  formatArgValue,
  parseResult,
  toolCategory,
  toolIcon,
  shortPath,
  formatBytes,
  argString,
  formatSizeDiff,
  combineOutputs,
  formatToolResult,
  toolResultSummary,
  dirIcon,
  fileIcon,
} from '@/composables/ai/useToolCard'
import type { AiToolCallInfo } from '@/api/types'
import ConfirmDialog from './ConfirmDialog.vue'

const { t } = useI18n()
const projectStore = useProjectStore()

// AE-2025-05:本卡自治二次确认状态机。每张 ToolCard 各持一份 confirmState,与 AiChat
// 父级 useConfirm 实例互不影响(并发弹层天然隔离,无需共享)。
const { confirmState, confirmDialog, answerConfirm } = useConfirm()

const props = defineProps<{
  /** 单条工具调用数据 */
  tc: AiToolCallInfo
  /** 卡片级展开(来自父级 expandedCards) */
  isExpanded: boolean
  /** 内容级展开(read_file 的"展开全部",来自父级 expandedTools) */
  isContentExpanded: boolean
}>()

const emit = defineEmits<{
  /** header 点击 → 卡片折叠/展开 */
  toggle: [id: string]
  /** read_file "展开全部/收起" → 内容级展开 */
  'expand-content': [id: string]
  /** 审批按钮 → 父级转发 store.approveToolCall */
  approve: [{ id: string; approved: boolean }]
}>()

// run_command 输出折叠态:失败命令默认展开(便于看 stderr),成功默认折叠
// 注:此处仅状态翻转时调一次,不进模板高频路径,沿用模块级 isToolFailure(props.tc) 无性能问题。
const cmdOutputExpanded = ref(false)
// http_request 响应 body 折叠态:失败(非 2xx)默认展开便于看错误,成功默认折叠
const httpBodyExpanded = ref(false)
// SW-260618-06: cmdOutputExpanded 的 status watch 合并到下方 approving 复位 watch
// (approving/approvingTimer 在下方定义,此处 immediate 会 TDZ,合并 watch 放三 ref 都已定义后)。

/**
 * 审批按钮 loading 态(B-260616-08)。
 * 点击后置 true,禁用两按钮防重复点击并显示 spinner。后端回事件 useAiEvents 把 tc.status
 * 离开 pending_approval(转 completed/rejected)→ watch 自动复位 false,卡片同步切态。
 *
 * 兜底:approve 后状态保持 pending_approval,若后端既不回事件也不转 running(仅在
 * AiApprovalResult/approved 后 hang 在工具执行前),全局看门狗只回滚 running 不会触及本卡。
 * 故此处加本地兜底计时(对齐全局看门狗 STREAM_TIMEOUT_MS),到点复位 loading 让用户能重试/改判。
 */
// UX-260617-14:复用 useAiStream 共享常量(原硬编码 130000,单一来源防漂移)。
const APPROVE_LOADING_TIMEOUT_MS = STREAM_TIMEOUT_MS
const approving = ref(false)
let approvingTimer: ReturnType<typeof setTimeout> | null = null

// ── 审批超时兜底 toast(零依赖,复用 AiChat/Settings 同款 reactive toast 模式) ──
// ToolCard 是子组件无根 toast 挂载点,且可能以分离窗口运行(App.vue 根 toast 不在 DOM),
// 故本卡自管一个最小 toast;仅在审批 loading 超时兜底时短暂提示。
const approveToast = reactive({ visible: false, msg: '' })
let _approveToastTimer: ReturnType<typeof setTimeout> | null = null
function showApproveTimeoutToast(): void {
  approveToast.msg = t('aiTool.approveLoadingTimeout')
  approveToast.visible = true
  if (_approveToastTimer) clearTimeout(_approveToastTimer)
  _approveToastTimer = setTimeout(() => { approveToast.visible = false }, 3000)
}

/**
 * AE-2025-05:High 风险工具白名单(后端 tool_registry.rs RiskLevel::High 对齐)。
 * AiToolCallInfo 无 risk 字段(风险仅存后端),故前端按工具名判定。新增 High 工具时同步加这里。
 * - delete 类(delete_project/purge_project/delete_task/delete_file/restore_project)→ 删除/不可恢复文案
 * - run_command → 执行命令文案
 * - 其余 High(未来扩展) → 通用高危文案
 */
const HIGH_RISK_TOOLS = new Set<string>([
  'delete_task', 'delete_project', 'restore_project', 'purge_project', 'delete_file', 'run_command',
  'http_request',
])

/** High 风险工具的二次确认文案(按操作类型) */
function highRiskConfirmMsg(name: string): string {
  if (name === 'run_command') return t('aiTool.confirmHighExec')
  if (name === 'http_request') return t('aiTool.confirmHighHttp')
  if (name === 'delete_task' || name === 'delete_project' || name === 'purge_project' || name === 'delete_file' || name === 'restore_project') {
    return t('aiTool.confirmHighDelete')
  }
  return t('aiTool.confirmHighGeneric')
}

async function onApprove(approved: boolean) {
  if (approving.value) return        // 防重入
  // AE-2025-05:High 风险 + 批准 → 置 approving 前先二次确认,误点可取消。
  // 拒绝(approved=false)不再二次确认(拒绝本就无害,加确认反增摩擦)。
  if (approved && HIGH_RISK_TOOLS.has(props.tc.name)) {
    const ok = await confirmDialog(highRiskConfirmMsg(props.tc.name))
    if (!ok) return
  }
  approving.value = true
  // 本地兜底:到点强制复位 loading(后端无回执、卡片仍 pending_approval 时给用户重试入口)
  // UX-260617-14:超时复位同时给 warning toast + 控制台日志,原仅静默 approving=false 用户无感知。
  approvingTimer = setTimeout(() => {
    approving.value = false
    approvingTimer = null
    console.warn('[AI] 审批 loading 超时,后端可能未回执,已复位允许重试:', props.tc.id)
    showApproveTimeoutToast()
  }, APPROVE_LOADING_TIMEOUT_MS)
  emit('approve', { id: props.tc.id, approved })
}
// SW-260618-06: 合并双 watch(原 cmdOutputExpanded watch + approving 复位 watch,同源 props.tc.status)。
// immediate 时 s=初始 status:cmdOutputExpanded 仅 completed 初始化(初始非 completed 无副作用);
// approving 初始 false + approvingTimer 初始 null(if null 跳过 clearTimeout),immediate 安全。
watch(() => props.tc.status, (s) => {
  if (s === 'completed') {
    cmdOutputExpanded.value = isToolFailure(props.tc)
    httpBodyExpanded.value = isToolFailure(props.tc)
  }
  if (s !== 'pending_approval') {
    approving.value = false
    if (approvingTimer) { clearTimeout(approvingTimer); approvingTimer = null }
  }
}, { immediate: true })
// 组件卸载清计时器防泄漏
onBeforeUnmount(() => {
  if (approvingTimer) clearTimeout(approvingTimer)
  if (_approveToastTimer) clearTimeout(_approveToastTimer)
})

/** 一次解析结果供模板多次读(模板引用 6 次,computed 避免每次 patch 重 parse) */
const parsed = computed(() => parseResult(props.tc.result))

/**
 * 是否工具失败(completed 语义)。模板多处复用(.ai-tool-card--failed / 状态点 / 失败横幅等,
 * 6+ 引用)。原每处直调 isToolFailure(tc) → 每次 patch 重新 parseResult + 正则,
 * 现下沉为 computed 复用 parsed,patch 间不重算。逻辑与模块级 isToolFailure 同源。
 */
const isFailed = computed(() => {
  const tc = props.tc
  if (tc.status !== 'completed') return false
  const r = parsed.value
  if (tc.name === 'run_command') {
    if (r && typeof r.exit_code === 'number') return r.exit_code !== 0
    if (r) return true
  }
  if (!r) {
    const raw = typeof tc.result === 'string' ? tc.result : ''
    if (!raw) return false
    return /^(执行失败|Error:|Failed:)/m.test(raw)
  }
  return false
})

/** run_command 展开态全量输出(stdout+stderr 合并);空串隐藏 toggle。复用 parsed 不重 parse */
const cmdOutput = computed(() => {
  const r = parsed.value
  if (!r) return ''
  return combineOutputs(r.stdout, r.stderr)
})

/**
 * AE-2025-03: write_file 审批 diff 行解析。
 * 后端 generate_diff 输出 '+新行/-旧行/ 上下文行'，逐行拆 → {kind, text} 供模板按 kind 着色。
 * kind: 'add'(+, 绿)/'del'(-, 红)/'ctx'(空格, 灰)。computed 在 diff 变化(新审批)时重算。
 */
const diffLines = computed(() => {
  const d = props.tc.diff
  if (!d) return []
  return d.split('\n').map(line => {
    // 注意 generate_diff 末尾保留单尾换行 → split 末尾有空串元素,过滤
    if (line === '') return null
    if (line.startsWith('+')) return { kind: 'add', text: line }
    if (line.startsWith('-')) return { kind: 'del', text: line }
    return { kind: 'ctx', text: line }
  }).filter((x): x is { kind: string; text: string } => x !== null)
})

/**
 * UX-260618-06: patch_file 结果 diff 行拆解(复用 diffLines 同款 +add/-del/ ctx 着色)。
 * 数据源不同:diffLines 取 props.tc.diff(审批事件注入,仅 write_file);此 computed 取
 * parsed.diff(patch_file 工具执行结果字段)。两条路径互不干扰。
 * 长 diff 截断到前 120 行防撑爆卡片(unified diff 含上下文行,大改可能很长)。
 */
const resultDiffLines = computed(() => {
  const d = parsed.value?.diff
  if (!d) return []
  const MAX_DIFF_LINES = 120
  const lines = d.split('\n').map(line => {
    if (line === '') return null
    if (line.startsWith('+')) return { kind: 'add', text: line }
    if (line.startsWith('-')) return { kind: 'del', text: line }
    return { kind: 'ctx', text: line }
  }).filter((x): x is { kind: string; text: string } => x !== null)
  if (lines.length > MAX_DIFF_LINES) {
    lines.splice(MAX_DIFF_LINES)
    lines.push({ kind: 'ctx', text: '…' })
  }
  return lines
})

/**
 * 按 id 查项目名(覆盖活跃 + 回收站):审批 delete/restore/purge 各阶段都可能引用项目,
 * 故先查 projects 再查 deletedProjects,找到即返回(提前退出),无则返回 undefined(调用方走 fallback)。
 * 复用 project store 已加载列表,不新增网络请求;响应式由 store 数组保证(find 在 computed/渲染上下文中触发依赖追踪)。
 * 审批卡通常仅 1-2 行引用项目 id,find 比 computed 全量建 Map 更省。
 */
function projectNameById(id: string): string | undefined {
  return projectStore.projects.find(p => p.id === id)?.name
    ?? projectStore.deletedProjects.find(p => p.id === id)?.name
}

/**
 * 按 id 查任务标题:审批 advance_task/run_workflow 的 task_id 时,把裸 UUID 转标题。
 * 复用 project store 已加载的 state.tasks(仅当前项目范围),不新增网络请求。
 * state.tasks 非「全量」(按当前项目/筛选加载),查不到时调用方走裸 id fallback——
 * 审批卡 task_id 多属当前活动项目,命中率足够,跨项目任务降级显 id 可接受。
 */
function taskNameById(id: string): string | undefined {
  return projectStore.tasks.find(t => t.id === id)?.title
}

/** 工具名 → 该工具中代表项目 id 的参数 key(仅项目类工具特化;delete_task 的 id 是任务 id,不在此列) */
const PROJECT_ID_TOOL_ARG: Record<string, 'id' | 'project_id'> = {
  delete_project: 'id',
  restore_project: 'id',
  purge_project: 'id',
  update_project: 'id',
  bind_directory: 'id',
  create_task: 'project_id',
}

/**
 * 工具名 → 该工具中代表任务 id 的参数 key(任务类工具特化)。
 * advance_task 的任务 id 在 "id" key,run_workflow 在 "task_id" key。
 */
const TASK_ID_TOOL_ARG: Record<string, 'id' | 'task_id'> = {
  advance_task: 'id',
  run_workflow: 'task_id',
}

/**
 * 审批参数值展示(AR-3):对项目类工具的 id/project_id 特化——优先回显项目名,查不到则显示
 * 「项目已不存在」提示 + 原 id,避免用户只看到一串裸 id 不知道是什么数据。
 * 非项目工具(如 delete_task 的 id)或其他 key 走通用 formatArgValue,避免误判。
 */
function displayArgValue(arg: { key: string; val: unknown }): string {
  const projectArgKey = PROJECT_ID_TOOL_ARG[props.tc.name]
  if (projectArgKey && arg.key === projectArgKey) {
    const id = typeof arg.val === 'string' ? arg.val : ''
    if (id) {
      const name = projectNameById(id)
      if (name) return t('aiTool.projectLabel', { name })
      return t('aiTool.projectIdNotFound', { id })
    }
  }
  // UX-260618-14:advance_task/run_workflow 的 task_id → 任务标题(store.tasks 查,无则标 id)
  const taskArgKey = TASK_ID_TOOL_ARG[props.tc.name]
  if (taskArgKey && arg.key === taskArgKey) {
    const id = typeof arg.val === 'string' ? arg.val : ''
    if (id) {
      const name = taskNameById(id)
      if (name) return t('aiTool.taskLabel', { name })
      return t('aiTool.taskIdNotFound', { id })
    }
  }
  return formatArgValue(arg.val)
}

/**
 * 工具显示名称(含目标摘要);文件类按 path,项目类(delete/restore/purge/update_project)按 id
 * 解析项目名(复用 projectNameById,无则走 fallback),create_* 用固定文案。
 */
function toolDisplayName(tc: AiToolCallInfo): string {
  const p = argString(tc.args, 'path')
  switch (tc.name) {
    case 'read_file': return p ? `${t('aiTool.readPrefix')} ${shortPath(p)}` : t('aiTool.readFallback')
    case 'list_directory': return p ? `${t('aiTool.dirPrefix')} ${shortPath(p)}` : t('aiTool.dirFallback')
    case 'write_file': return p ? `${t('aiTool.writePrefix')} ${shortPath(p)}` : t('aiTool.writeFallback')
    case 'delete_project':
    case 'restore_project':
    case 'purge_project':
    case 'update_project': {
      const idKey = PROJECT_ID_TOOL_ARG[tc.name]
      const id = idKey ? argString(tc.args, idKey) : ''
      const name = id ? projectNameById(id) : ''
      const prefixKey = `${tc.name.replace('_project', '')}Prefix` as 'deletePrefix' | 'restorePrefix' | 'purgePrefix' | 'updatePrefix'
      const fallbackKey = `${tc.name.replace('_project', '')}Fallback` as 'deleteFallback' | 'restoreFallback' | 'purgeFallback' | 'updateFallback'
      return name ? `${t('aiTool.' + prefixKey)}「${name}」` : t('aiTool.' + fallbackKey)
    }
    case 'create_task': return t('aiTool.createTask')
    case 'create_project': return t('aiTool.createProject')
    case 'run_command': {
      // 折叠态头部也显命令(取首行+截断),无 command 回退通用名
      const cmd = argString(tc.args, 'command')
      return cmd ? `$ ${shortCmd(cmd)}` : formatToolName(tc.name)
    }
    case 'http_request': {
      // 折叠态头部显 method + host(从 url 提取),无 url 回退通用名
      const method = argString(tc.args, 'method') || 'GET'
      const url = argString(tc.args, 'url')
      const host = httpHost(url)
      return host ? `${method} ${host}` : t('aiTool.httpRequestFallback')
    }
    default: return formatToolName(tc.name)
  }
}

/**
 * http_request url → host 显示用(折叠态头部精简,不全显 url)。
 * 取 scheme://host[:port],剥 path/query;非 http(s) url 返回空串回退通用名。
 * 注:用 URL 构造器容错,非法 url 抛错时回退原 url 截断。
 */
function httpHost(raw: string): string {
  if (!raw) return ''
  try {
    const u = new URL(raw)
    if (u.protocol !== 'http:' && u.protocol !== 'https:') return ''
    const portPart = u.port ? `:${u.port}` : ''
    return `${u.hostname}${portPart}`
  } catch {
    return raw.length > 40 ? raw.slice(0, 40) + '…' : raw
  }
}
</script>

<style scoped>
.ai-tool-card {
  background: var(--df-bg-card);
  border: 0.5px solid var(--df-border);
  border-radius: var(--df-radius);
  padding: 0;
  overflow: hidden;
  max-width: var(--df-msg-max-width);
  animation: fadeInUp 0.2s var(--df-ease);
  /* UX-260617-14:相对定位锚定本卡 toast(absolute 子元素) */
  position: relative;
  /* UX-260616-03: border/shadow/opacity 过渡让折叠/失败态切换不突兀。
     不做 body 高度动画(v-show display 切换 + flex gap 会留双倍间距致布局跳变,
     见 ToolCardList 注释);opacity 过渡覆盖自动收起时的视觉跳变(约 200ms)。 */
  transition: border-color 0.2s var(--df-ease), box-shadow 0.2s var(--df-ease), opacity 0.2s var(--df-ease);
}
.ai-tool-card--pending_approval {
  border-color: rgba(240,199,94,0.3);
  box-shadow: 0 0 0 1px rgba(240,199,94,0.06);
}
.ai-tool-card--completed {
  border-color: rgba(61,219,160,0.2);
}
.ai-tool-card--rejected {
  border-color: rgba(240,101,101,0.2);
  opacity: 0.55;
}
/* UX-260616-01: 工具失败态——AR-6 下 status 仍为 completed,故单独覆盖边框/底色与 completed 区分 */
.ai-tool-card--failed {
  border-color: rgba(240,101,101,0.35);
  box-shadow: 0 0 0 1px rgba(240,101,101,0.06);
}
.ai-tool-status-dot--failed {
  background: var(--df-danger);
}
.ai-tool-sub--failed {
  color: var(--df-danger);
}
.ai-tool-failed-banner {
  padding: 4px 10px 2px;
  font-size: 11px;
  font-weight: 500;
  color: var(--df-danger);
  background: var(--df-danger-bg);
  border-bottom: 0.5px solid rgba(240,101,101,0.2);
}
.ai-tool-result--failed {
  background: var(--df-danger-bg);
  border-top-color: rgba(240,101,101,0.2);
}
.ai-tool-result--failed code {
  color: var(--df-danger);
}
.ai-tool-rejected {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 4px 10px 8px 42px;
  font-size: 11px;
  color: var(--df-danger);
}
.ai-tool-rejected-icon {
  font-weight: 700;
}

/* -- 卡片折叠态(running/pending 始终展开,completed/rejected 默认折叠为单行 header) -- */
.ai-tool-card--collapsed .ai-tool-header {
  cursor: pointer;
}
.ai-tool-card--collapsed .ai-tool-header:hover {
  background: rgba(255,255,255,0.03);
}
.ai-tool-body {
  overflow: hidden;
}
.ai-tool-collapse-hint {
  font-size: 10px;
  color: var(--df-text-dim);
  margin-left: auto;
  flex-shrink: 0;
  transition: transform 0.15s var(--df-ease);
}
.ai-tool-collapse-hint.is-open {
  transform: rotate(90deg);
}

/* -- Header -- */
.ai-tool-header {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 8px 10px;
}
.ai-tool-icon-wrap {
  width: 24px;
  height: 24px;
  border-radius: var(--df-radius-sm);
  display: flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
}
.ai-tool-icon-wrap--file-read  { background: rgba(94,175,240,0.12); color: var(--df-info); }
.ai-tool-icon-wrap--file-write { background: var(--df-warning-bg); color: var(--df-warning); }
.ai-tool-icon-wrap--dir        { background: rgba(240,199,94,0.12); color: var(--df-warning); }
.ai-tool-icon-wrap--create     { background: var(--df-success-bg); color: var(--df-success); }
.ai-tool-icon-wrap--delete     { background: var(--df-danger-bg); color: var(--df-danger); }
.ai-tool-icon-wrap--list       { background: var(--df-accent-bg); color: var(--df-accent); }
.ai-tool-icon-wrap--default    { background: var(--df-accent-bg); color: var(--df-accent); }

.ai-tool-header-text {
  display: flex;
  flex-direction: row;
  flex-wrap: wrap;
  align-items: baseline;
  column-gap: 8px;
  row-gap: 1px;
  flex: 1;
  min-width: 0;
}
.ai-tool-name {
  font-size: 12px;
  font-weight: 500;
  color: var(--df-text);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  flex-shrink: 0;
}
.ai-tool-sub {
  font-size: 10px;
  color: var(--df-text-dim);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* -- Status dot (脉冲动画) -- */
.ai-tool-status-dot {
  width: 7px;
  height: 7px;
  border-radius: 50%;
  flex-shrink: 0;
}
.ai-tool-status-dot--running {
  background: var(--df-info);
  animation: pulseGlow 1.5s ease-in-out infinite;
  box-shadow: 0 0 6px var(--df-info);
}
.ai-tool-status-dot--pending_approval {
  background: var(--df-warning);
  animation: pulseGlow 1.5s ease-in-out infinite;
  box-shadow: 0 0 6px rgba(240,199,94,0.3);
}
.ai-tool-status-dot--completed { background: var(--df-success); }
.ai-tool-status-dot--rejected { background: var(--df-danger); }

/* -- Skeleton (running 状态) -- */
.ai-tool-skeleton {
  padding: 4px 10px 10px 42px;
  display: flex;
  flex-direction: column;
  gap: 5px;
}
.ai-tool-skeleton-line {
  height: 8px;
  border-radius: var(--df-radius-xs);
  background: linear-gradient(90deg, var(--df-bg) 25%, var(--df-bg-card-hover) 50%, var(--df-bg) 75%);
  background-size: 200% 100%;
  animation: shimmer 1.5s infinite;
}
@keyframes shimmer {
  0%   { background-position: 200% 0; }
  100% { background-position: -200% 0; }
}

/* -- 审批参数展示 -- */
.ai-tool-approval-args {
  padding: 4px 10px 6px;
  margin-bottom: 6px;
  border-top: 0.5px solid var(--df-border);
  border-bottom: 0.5px solid var(--df-border);
  background: var(--df-bg-elevated, rgba(0, 0, 0, 0.03));
}
.ai-tool-arg {
  display: flex;
  gap: 8px;
  padding: 2px 0;
  font-size: 11px;
  line-height: 1.5;
}
.ai-tool-arg-key {
  flex-shrink: 0;
  min-width: 64px;
  color: var(--df-text-secondary);
  font-family: var(--df-font-mono);
}
.ai-tool-arg-val {
  flex: 1;
  word-break: break-all;
  white-space: pre-wrap;
  font-family: var(--df-font-mono);
  color: var(--df-text-primary);
}
.ai-tool-approval-reason {
  padding: 4px 10px 6px;
  margin-bottom: 6px;
  font-size: 11px;
  color: var(--df-warning, #d97706);
  background: var(--df-warning-bg, rgba(217, 119, 6, 0.08));
  border-radius: var(--df-radius-sm, 4px);
}

/* AE-2025-03: write_file 审批 diff 预览区（红删绿增行级，复用 --df-danger/--df-success token） */
.ai-tool-approval-diff {
  margin: 0 10px 8px;
  border: 0.5px solid var(--df-border);
  border-radius: var(--df-radius-sm, 4px);
  overflow: hidden;
}
.ai-tool-diff-pre {
  margin: 0;
  padding: 6px 0;
  max-height: 240px;
  overflow: auto;
  font-family: var(--df-font-mono);
  font-size: 11px;
  line-height: 1.5;
  background: var(--df-bg-card);
}
.ai-tool-diff-pre code {
  display: block;
  white-space: pre;
}
.ai-tool-diff-line {
  display: block;
  padding: 0 10px;
  white-space: pre-wrap;
  word-break: break-all;
}
.ai-tool-diff-line--add { color: var(--df-success, #3ddb9c); background: var(--df-success-bg, rgba(61, 219, 160, 0.08)); }
.ai-tool-diff-line--del { color: var(--df-danger, #f06565); background: var(--df-danger-bg, rgba(240, 101, 101, 0.08)); }
.ai-tool-diff-line--ctx { color: var(--df-text-dim, #888); }

/* -- 审批按钮 -- */
.ai-tool-actions {
  display: flex;
  gap: 6px;
  padding: 0 10px 8px;
}
.ai-tool-btn {
  padding: 5px 12px;
  font-size: 11px;
  font-weight: 500;
  border-radius: var(--df-radius-sm);
  border: 0.5px solid var(--df-border);
  cursor: pointer;
  display: flex;
  align-items: center;
  gap: 4px;
  transition: all 0.15s var(--df-ease);
  font-family: var(--df-font-sans);
}
.ai-tool-btn--approve {
  background: var(--df-success-bg);
  color: var(--df-success);
}
.ai-tool-btn--approve:hover {
  background: var(--df-success);
  color: #fff;
  border-color: var(--df-success);
}
.ai-tool-btn--reject {
  background: var(--df-danger-bg);
  color: var(--df-danger);
}
.ai-tool-btn--reject:hover {
  background: var(--df-danger);
  color: #fff;
  border-color: var(--df-danger);
}
/* 审批 loading(B-260616-08):IPC 送达待后端回执期间禁用两按钮防重复点击,spinner 转在原位 */
.ai-tool-btn:disabled {
  opacity: 0.6;
  cursor: not-allowed;
}
.ai-tool-btn--approve:disabled:hover,
.ai-tool-btn--reject:disabled:hover {
  /* 覆盖 hover 高亮,disabled 态不闪色 */
  background: var(--df-success-bg);
  color: var(--df-success);
  border-color: var(--df-border);
}
.ai-tool-btn--reject:disabled:hover {
  background: var(--df-danger-bg);
  color: var(--df-danger);
}
.ai-tool-btn-spinner {
  width: 12px;
  height: 12px;
  border: 1.5px solid currentColor;
  border-top-color: transparent;
  border-radius: 50%;
  display: inline-block;
  flex-shrink: 0;
  animation: aiToolBtnSpin 0.7s linear infinite;
}
@keyframes aiToolBtnSpin {
  to { transform: rotate(360deg); }
}

/* -- File Content (read_file) -- */
.ai-tool-file-content {
  border-top: 0.5px solid var(--df-border);
}
.ai-tool-file-bar {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 5px 10px;
  background: color-mix(in srgb, var(--df-bg) 80%, transparent);
  border-bottom: 0.5px solid var(--df-border);
}
.ai-tool-file-icon {
  display: flex;
  align-items: center;
  color: var(--df-info);
  flex-shrink: 0;
}
.ai-tool-file-icon--dir {
  color: var(--df-warning);
}
.ai-tool-file-path {
  font-family: var(--df-font-mono);
  font-size: 10px;
  color: var(--df-accent);
  flex: 1;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.ai-tool-file-meta {
  font-family: var(--df-font-mono);
  font-size: 9px;
  color: var(--df-text-dim);
  flex-shrink: 0;
}
.ai-tool-expand-btn {
  font-family: var(--df-font-sans);
  font-size: 10px;
  color: var(--df-text-dim);
  background: rgba(255,255,255,0.04);
  border: 0.5px solid var(--df-border);
  border-radius: var(--df-radius-sm);
  padding: 2px 8px;
  cursor: pointer;
  flex-shrink: 0;
  transition: all 0.15s var(--df-ease);
}
.ai-tool-expand-btn:hover {
  color: var(--df-text-secondary);
  background: rgba(255,255,255,0.08);
  border-color: var(--df-border-strong);
}
.ai-tool-file-pre {
  margin: 0;
  padding: 10px 12px;
  background: color-mix(in srgb, var(--df-bg) 60%, transparent);
  font-family: var(--df-font-mono);
  font-size: 11px;
  line-height: 1.6;
  color: var(--df-text-secondary);
  overflow-x: auto;
  tab-size: 4;
}
.ai-tool-file-pre--collapsed {
  max-height: 180px;
  /* hidden→auto: 默认高度内可直接滚轮看完整内容，不必先点"展开全部" */
  overflow-y: auto;
  position: relative;
}
.ai-tool-file-pre--collapsed::after {
  content: '';
  position: absolute;
  bottom: 0;
  left: 0;
  right: 0;
  height: 48px;
  background: linear-gradient(transparent, color-mix(in srgb, var(--df-bg) 90%, transparent));
  pointer-events: none;
}

/* -- Directory List (list_directory) -- */
.ai-tool-dir-list {
  border-top: 0.5px solid var(--df-border);
}
.ai-tool-dir-entries {
  padding: 4px 0;
  max-height: 220px;
  overflow-y: auto;
}
.ai-tool-dir-entry {
  display: flex;
  align-items: center;
  gap: 7px;
  padding: 3px 12px;
  font-size: 11px;
  transition: background 0.1s;
}
.ai-tool-dir-entry:hover {
  background: rgba(255,255,255,0.03);
}
.ai-tool-dir-entry--directory .ai-tool-dir-name {
  font-weight: 500;
  color: var(--df-text);
}
.ai-tool-dir-icon {
  display: flex;
  align-items: center;
  flex-shrink: 0;
}
.ai-tool-dir-icon--folder { color: var(--df-warning); }
.ai-tool-dir-icon--file   { color: var(--df-text-dim); }
.ai-tool-dir-name {
  font-family: var(--df-font-mono);
  color: var(--df-text-secondary);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  flex: 1;
}
.ai-tool-dir-size {
  font-family: var(--df-font-mono);
  font-size: 9px;
  color: var(--df-text-dim);
  flex-shrink: 0;
  opacity: 0.7;
}

/* -- Write File Result -- */
.ai-tool-write-result {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 8px 10px;
  border-top: 0.5px solid var(--df-border);
  background: rgba(61,219,160,0.04);
}
.ai-tool-write-path {
  font-family: var(--df-font-mono);
  font-size: 11px;
  color: var(--df-accent);
  flex: 1;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.ai-tool-write-meta {
  font-size: 10px;
  color: var(--df-success);
  flex-shrink: 0;
}

/* -- Generic Result -- */
.ai-tool-result {
  margin: 0;
  padding: 8px 10px;
  background: rgba(61,219,160,0.04);
  border-top: 0.5px solid var(--df-border);
  border-radius: 0;
  font-family: var(--df-font-mono);
  font-size: 11px;
  color: var(--df-success);
  max-height: 100px;
  overflow-y: auto;
  white-space: pre-wrap;
  word-break: break-all;
}

/* -- run_command 结果:命令行(常显) + 可折叠输出 -- */
.ai-tool-cmd-result {
  margin: 0;
  border-top: 0.5px solid var(--df-border);
  font-family: var(--df-font-mono);
  font-size: 11px;
}
.ai-tool-cmd-line {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 6px 10px;
  background: rgba(61,219,160,0.04);
}
.ai-tool-cmd-line--failed {
  background: var(--df-danger-bg);
}
.ai-tool-cmd-prompt {
  flex-shrink: 0;
  font-weight: 600;
  color: var(--df-success);
}
.ai-tool-cmd-line--failed .ai-tool-cmd-prompt {
  color: var(--df-danger);
}
.ai-tool-cmd-text {
  flex: 1;
  min-width: 0;
  white-space: pre-wrap;
  word-break: break-all;
  color: var(--df-text);
}
.ai-tool-cmd-toggle {
  flex-shrink: 0;
  padding: 2px 4px;
  border: none;
  border-radius: var(--df-radius-sm);
  background: transparent;
  color: var(--df-text-dim);
  font-size: 10.5px;
  cursor: pointer;
}
.ai-tool-cmd-toggle:hover {
  color: var(--df-text);
  background: var(--df-bg-hover, rgba(255,255,255,0.06));
}
.ai-tool-cmd-output {
  margin: 0;
  padding: 8px 10px;
  max-height: 160px;
  overflow-y: auto;
  background: rgba(0,0,0,0.18);
  color: var(--df-text-dim);
  white-space: pre-wrap;
  word-break: break-all;
  border-top: 0.5px solid var(--df-border);
}

/* -- http_request 结果:method 徽标 + url + 状态码(复用 cmd-line 视觉 token) -- */
.ai-tool-http-result {
  margin: 0;
  border-top: 0.5px solid var(--df-border);
  font-family: var(--df-font-mono);
  font-size: 11px;
}
.ai-tool-http-line {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 6px 10px;
  background: rgba(61,219,160,0.04); /* 成功默认(对齐 cmd-line);失败由 --failed 覆盖 */
}
.ai-tool-http-line--failed {
  background: var(--df-danger-bg);
}
.ai-tool-http-method {
  flex-shrink: 0;
  font-weight: 600;
  padding: 1px 5px;
  border-radius: var(--df-radius-sm);
  background: rgba(255,255,255,0.08);
  color: var(--df-text);
  font-size: 10px;
}
.ai-tool-http-url {
  flex: 1;
  min-width: 0;
  white-space: pre-wrap;
  word-break: break-all;
  color: var(--df-text-dim);
}
.ai-tool-http-status {
  flex-shrink: 0;
  font-weight: 600;
  color: var(--df-danger);
}
.ai-tool-http-status--ok {
  color: var(--df-success);
}
.ai-tool-http-trunc-warn {
  padding: 4px 10px;
  font-size: 10.5px;
  color: var(--df-warning, #f0c75e);
  border-top: 0.5px solid var(--df-border);
}

/* -- UX-260617-14: 审批 loading 超时兜底 toast(复用 AiChat 同款视觉 token) -- */
.ai-tool-toast {
  position: absolute;
  top: 6px;
  left: 50%;
  transform: translateX(-50%);
  padding: 6px 12px;
  border-radius: var(--df-radius-sm);
  font-size: 11px;
  z-index: 100;
  box-shadow: 0 4px 16px rgba(0,0,0,0.3);
  max-width: 90%;
  overflow-wrap: anywhere;
  pointer-events: none;
}
.ai-tool-toast--warning {
  background: var(--df-warning-bg);
  color: var(--df-warning);
  border: 0.5px solid var(--df-warning);
}
.ai-tool-toast-enter-active, .ai-tool-toast-leave-active { transition: opacity 0.2s, transform 0.2s; }
.ai-tool-toast-enter-from, .ai-tool-toast-leave-to { opacity: 0; transform: translate(-50%, -6px); }
</style>
