<script setup>
import {
  computed,
  reactive,
  ref,
  watch,
  nextTick,
  onMounted,
  onBeforeUnmount,
} from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { ElMessage } from 'element-plus'
import axios from 'axios'
import MarkdownIt from 'markdown-it'
import mermaid from 'mermaid'
import {
  ToolCallCard,
  LsResultCard,
  FileReadResultCard,
  BashResultCard,
} from '../components/tools'

const route = useRoute()
const router = useRouter()
const apiBase = import.meta.env.VITE_API_BASE || 'http://localhost:8000/api'
const inputText = ref('')
const uploadFiles = ref([])
const fileList = ref([])
const sending = ref(false)
const runStatus = ref('DONE')
const loadingMessages = ref(false)
const errorMessage = ref('')
const showJumpDown = ref(false)
const lastSeq = ref(0)
const eventSource = ref(null)
const sseRetryTimer = ref(null)
const archiveLoading = ref(false)
const sampleImage =
  'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="220" height="140" viewBox="0 0 220 140"><defs><linearGradient id="g" x1="0%" y1="0%" x2="100%" y2="100%"><stop stop-color="%23e0e9ff" offset="0%"/><stop stop-color="%23c7d8ff" offset="50%"/><stop stop-color="%23e8f1ff" offset="100%"/></linearGradient></defs><rect width="220" height="140" rx="12" fill="url(%23g)"/><text x="50%" y="52%" dominant-baseline="middle" text-anchor="middle" fill="%231d4ed8" font-family="Arial" font-size="16">mock image</text></svg>'
const markdown = new MarkdownIt({
  html: false,
  linkify: true,
  breaks: true,
  typographer: true,
})
const defaultFence = markdown.renderer.rules.fence
markdown.renderer.rules.fence = (tokens, idx, options, env, self) => {
  const token = tokens[idx]
  const lang = (token.info || '').trim().toLowerCase()
  if (lang === 'mermaid') {
    const code = markdown.utils.escapeHtml(token.content || '')
    return `<div class="mermaid">${code}</div>`
  }
  return defaultFence
    ? defaultFence(tokens, idx, options, env, self)
    : self.renderToken(tokens, idx, options)
}
const defaultLinkOpen = markdown.renderer.rules.link_open
markdown.renderer.rules.link_open = (tokens, idx, options, env, self) => {
  tokens[idx].attrSet('target', '_blank')
  tokens[idx].attrSet('rel', 'noreferrer noopener')
  return defaultLinkOpen
    ? defaultLinkOpen(tokens, idx, options, env, self)
    : self.renderToken(tokens, idx, options)
}
const renderMarkdown = (text = '') => markdown.render(text || '')
let mermaidInitialized = false
const setupMermaid = () => {
  if (mermaidInitialized || typeof window === 'undefined') return
  mermaid.initialize({ startOnLoad: false, securityLevel: 'strict' })
  mermaidInitialized = true
}
const renderMermaid = () => {
  if (typeof window === 'undefined') return
  const nodes = document.querySelectorAll('.bubble-text .mermaid:not([data-processed])')
  if (!nodes.length) return
  mermaid
    .run({ nodes })
    .catch(() => {
      /* ignore render errors */
    })
}

const sessionId = ref(
  route.params.id
    ? String(route.params.id)
    : localStorage.getItem('ars-current-session') || ''
)

const summary = reactive({
  title: '未命名会话',
  status: 'OPEN',
  updatedAt: '--',
  lastSeq: '--',
  tags: [],
})

const messagesState = ref([])
const seenDedupeKeys = new Set()
const finalizedRunIds = new Set()
// Typewriter effect state for streaming deltas
const typingStates = new Map()
const typingIntervalMs = 24
// Tool calls grouped by run_id: run_id -> { toolCallId -> toolCallData }
const runToolCalls = new Map()

// 解析工具调用/结果的 content (JSON 字符串)
const parseToolContent = (content) => {
  if (!content) return {}
  try {
    return JSON.parse(content)
  } catch {
    return {}
  }
}

// 获取 tool_call_id
const getToolCallId = (msg = {}) => msg.payload?.tool_call_id || null

// 获取父级 run_id（工具调用所属的主运行）
const getParentRunId = (msg = {}) => msg.payload?.parent_run_id || msg.payload?.run_id || null

// 判断是否为工具相关消息
const isToolMessage = (msg = {}) =>
  msg.type === 'tool.call' || msg.type === 'tool.result'

// 判断是否为工具级别的 run.status（需要隐藏）
const isToolRunStatus = (msg = {}) =>
  msg.type === 'run.status' && msg.payload?.tool_call_id

// 创建工具调用对象
const createToolCall = (msg) => {
  const toolCallId = getToolCallId(msg)
  const toolName = msg.payload?.name || 'unknown'
  const isCall = msg.type === 'tool.call'
  const isResult = msg.type === 'tool.result'

  // 解析 content JSON
  let parsedContent = {}
  if (msg.content) {
    try {
      parsedContent = JSON.parse(msg.content)
    } catch (e) {
      console.warn('Failed to parse tool content:', msg.content, e)
    }
  }

  const result = {
    toolCallId,
    toolName,
    toolInput: isCall ? (msg.payload?.arguments || parsedContent) : null,
    toolResult: isResult ? parsedContent : null,
    toolStatus: isResult ? (msg.payload?.status || 'SUCCESS') : 'RUNNING',
    toolError: msg.payload?.error || null,
    truncated: msg.payload?.truncated || false,
    seq: msg.seq ?? 0,
    time: normalizeTime(msg.created_at),
  }
  // 调试日志
  console.log('[createToolCall]', msg.type, toolName, result)
  return result
}

// 合并工具调用（call + result）
const mergeToolCall = (existing, newData) => {
  // 如果没有现有数据，直接返回新数据
  if (!existing) return newData
  // 如果没有新数据，直接返回现有数据
  if (!newData) return existing

  return {
    ...existing,
    ...newData,  // newData 的字段优先，但保留 existing 中 newData 没有的字段
    // 确保关键字段不丢失
    toolCallId: newData.toolCallId || existing.toolCallId,
    toolName: newData.toolName || existing.toolName,
    toolInput: newData.toolInput !== null ? newData.toolInput : existing.toolInput,
    toolResult: newData.toolResult !== null ? newData.toolResult : existing.toolResult,
    toolStatus: newData.toolStatus || existing.toolStatus,
    toolError: newData.toolError || existing.toolError,
    truncated: newData.truncated || existing.truncated,
    seq: Math.max(existing.seq || 0, newData.seq || 0),
    time: newData.time || existing.time,
  }
}

// 收集工具调用到 run
const collectToolCall = (msg) => {
  const toolCallId = getToolCallId(msg)
  const parentRunId = getParentRunId(msg)
  console.log('[collectToolCall] type:', msg.type, 'toolCallId:', toolCallId, 'parentRunId:', parentRunId, 'msg:', msg)
  if (!toolCallId || !parentRunId) return

  if (!runToolCalls.has(parentRunId)) {
    runToolCalls.set(parentRunId, new Map())
  }
  const toolsMap = runToolCalls.get(parentRunId)
  const existing = toolsMap.get(toolCallId)
  const newToolCall = createToolCall(msg)
  toolsMap.set(toolCallId, mergeToolCall(existing, newToolCall))
}

// 获取某个 run 的所有工具调用（按 seq 排序）
const getToolCallsForRun = (runId) => {
  const toolsMap = runToolCalls.get(runId)
  if (!toolsMap) return []
  const result = Array.from(toolsMap.values()).sort((a, b) => a.seq - b.seq)
  console.log('[getToolCallsForRun] runId:', runId, 'tools:', result)
  return result
}
const getRunId = (msg = {}) => msg.payload?.run_id || null
const shouldGroupByRun = (msg = {}) => {
  const runId = getRunId(msg)
  return Boolean(
    runId &&
      (msg.type === 'message.delta' ||
        msg.type === 'agent.message' ||
        msg.type === 'error')
  )
}
const markDedupeKey = (msg = {}) => {
  const key = msg.dedupe_key
  if (!key) return false
  if (seenDedupeKeys.has(key)) return true
  seenDedupeKeys.add(key)
  return false
}
const stopTyping = (messageId) => {
  const state = typingStates.get(messageId)
  if (state?.timer) {
    clearInterval(state.timer)
  }
  typingStates.delete(messageId)
}
const stopAllTyping = () => {
  Array.from(typingStates.keys()).forEach((messageId) => {
    stopTyping(messageId)
  })
}
const resetMessageCache = () => {
  seenDedupeKeys.clear()
  finalizedRunIds.clear()
  runToolCalls.clear()
  stopAllTyping()
}
const normalizeTime = (val) =>
  val
    ? new Date(val).toLocaleTimeString('zh-CN', {
        hour12: false,
        hour: '2-digit',
        minute: '2-digit',
      })
    : '--:--'

const buildMessageId = (msg = {}, seq = 0, runId = null) =>
  runId
    ? `run-${runId}`
    : msg.dedupe_key || msg.message_id || `msg-${seq || Math.random()}`

const normalizeMessage = (msg = {}, options = {}) => {
  const seq = msg.seq ?? lastSeq.value ?? 0
  const runId =
    options.runId ?? (options.groupByRun ? getRunId(msg) : null)
  const statusText =
    msg.type === 'error'
      ? msg.payload?.message ||
        msg.payload?.error ||
        msg.content ||
        '系统错误'
      : msg.content
  const text =
    options.text ?? statusText ?? (options.allowEmpty ? '' : '（空内容）')
  const roleUpper = msg.role?.toUpperCase()
  const normalizedRole =
    roleUpper === 'USER' ? 'user' : roleUpper === 'SYSTEM' ? 'system' : 'agent'
  const author =
    roleUpper === 'USER' ? '你' : roleUpper === 'SYSTEM' ? '系统' : 'ARS'
  // 获取该 run 的工具调用
  const toolCalls = runId ? getToolCallsForRun(runId) : []

  return {
    id: options.id || buildMessageId(msg, seq, runId),
    seq,
    role: normalizedRole,
    author,
    time: normalizeTime(msg.created_at),
    text,
    attachments:
      msg.payload?.attachments?.map((file) => ({
        name: file.name || '附件',
        type: file.type || 'file',
        size: file.size || '',
        url: file.url || sampleImage,
      })) || [],
    type: msg.type,
    runId,
    dedupeKey: msg.dedupe_key || null,
    payload: msg.payload || null,
    toolCalls, // 附加工具调用列表
  }
}

const extractRunStatus = (msg = {}) => {
  const status = msg.payload?.status || msg.content
  return status === 'RUNNING' || status === 'DONE' || status === 'FAILED'
    ? status
    : null
}

const applyRunStatus = (msg = {}) => {
  if (msg.type !== 'run.status') return false
  const status = extractRunStatus(msg)
  if (!status) return false
  runStatus.value = status
  const runId = getRunId(msg)
  if (status === 'FAILED' && runId) {
    finalizedRunIds.add(runId)
    stopTyping(`run-${runId}`)
  }
  if (typeof msg.seq === 'number') {
    lastSeq.value = Math.max(lastSeq.value || 0, msg.seq)
    summary.lastSeq = lastSeq.value
  }
  return true
}

const refreshLastSeq = (list = []) => {
  const maxSeq = list.reduce((max, item) => {
    const seq = typeof item.seq === 'number' ? item.seq : 0
    return seq > max ? seq : max
  }, lastSeq.value || 0)
  lastSeq.value = maxSeq
  summary.lastSeq = maxSeq
}

const appendDeltaToList = (normalized, runIndexMap, msg) => {
  const runId = getRunId(msg)
  const deltaText = msg.content || ''
  const index =
    runId && runIndexMap.has(runId) ? runIndexMap.get(runId) : -1
  if (index >= 0) {
    const current = normalized[index]
    const existingText = current.text === '（空内容）' ? '' : current.text
    normalized[index] = {
      ...current,
      text: `${existingText}${deltaText}`,
      type: msg.type || current.type,
      dedupeKey: msg.dedupe_key || current.dedupeKey,
      seq: msg.seq ?? current.seq,
    }
    return
  }
  const base = normalizeMessage(msg, {
    groupByRun: shouldGroupByRun(msg),
    allowEmpty: true,
    text: deltaText,
  })
  normalized.push(base)
  if (base.runId) {
    runIndexMap.set(base.runId, normalized.length - 1)
  }
}

const normalizeMessageList = (list = []) => {
  let latestStatus = runStatus.value
  const normalized = []
  const runIndexMap = new Map()
  resetMessageCache()

  // 第一遍：收集 finalizedRunIds 和所有工具调用
  list.forEach((msg) => {
    if (!msg) return
    if (msg.type === 'agent.message') {
      const runId = getRunId(msg)
      if (runId) finalizedRunIds.add(runId)
    }
    if (msg.type === 'run.status') {
      const status = extractRunStatus(msg)
      if (status === 'FAILED') {
        const runId = getRunId(msg)
        if (runId) finalizedRunIds.add(runId)
      }
    }
    // 收集工具调用
    if (isToolMessage(msg)) {
      collectToolCall(msg)
    }
  })

  // 第二遍：构建消息列表
  list.forEach((msg) => {
    if (!msg) return
    // 跳过工具消息（已收集到 runToolCalls）
    if (isToolMessage(msg)) return
    // 跳过工具级别的 run.status
    if (isToolRunStatus(msg)) return
    // 处理主运行的 run.status
    if (msg.type === 'run.status') {
      const status = extractRunStatus(msg)
      if (status) latestStatus = status
      return
    }
    if (markDedupeKey(msg)) return

    const runId = getRunId(msg)
    if (msg.type === 'message.delta') {
      if (runId && finalizedRunIds.has(runId)) return
      appendDeltaToList(normalized, runIndexMap, msg)
      return
    }
    const normalizedMsg = normalizeMessage(msg, {
      groupByRun: shouldGroupByRun(msg),
    })
    normalized.push(normalizedMsg)
    if (normalizedMsg.runId) {
      runIndexMap.set(normalizedMsg.runId, normalized.length - 1)
    }
  })
  runStatus.value = latestStatus
  refreshLastSeq(list)
  return normalized
}

const upsertMessage = (msg) => {
  const list = messagesState.value
  const idx = list.findIndex(
    (m) => m.id === msg.id || (msg.seq && m.seq === msg.seq)
  )
  if (idx >= 0) {
    const next = [...list]
    next[idx] = msg
    messagesState.value = next
  } else {
    messagesState.value = [...list, msg]
  }
  lastSeq.value = Math.max(lastSeq.value || 0, msg.seq || 0)
  summary.lastSeq = lastSeq.value
  summary.updatedAt =
    msg.time ||
    summary.updatedAt ||
    new Date().toLocaleString('zh-CN', {
      hour12: false,
      month: '2-digit',
      day: '2-digit',
      hour: '2-digit',
      minute: '2-digit',
    })
}

const updateMessageText = (messageId, text) => {
  const list = messagesState.value
  const idx = list.findIndex((item) => item.id === messageId)
  if (idx < 0) return
  const next = [...list]
  next[idx] = { ...next[idx], text }
  messagesState.value = next
}

const startTyping = (messageId, targetText) => {
  if (!messageId) return
  if (typeof window === 'undefined') {
    updateMessageText(messageId, targetText)
    return
  }
  const state = typingStates.get(messageId) || { targetText: '', timer: null }
  state.targetText = targetText
  typingStates.set(messageId, state)
  if (state.timer) return
  state.timer = window.setInterval(() => {
    const current = messagesState.value.find((item) => item.id === messageId)
    if (!current) {
      stopTyping(messageId)
      return
    }
    const currentText = current.text || ''
    if (state.targetText.length <= currentText.length) {
      if (currentText !== state.targetText) {
        updateMessageText(messageId, state.targetText)
      }
      stopTyping(messageId)
      return
    }
    const remaining = state.targetText.length - currentText.length
    const step = Math.max(1, Math.min(8, Math.ceil(remaining / 60)))
    const nextText = state.targetText.slice(
      0,
      Math.min(state.targetText.length, currentText.length + step)
    )
    updateMessageText(messageId, nextText)
  }, typingIntervalMs)
}

const appendDeltaMessage = (msg) => {
  const runId = getRunId(msg)
  if (runId && finalizedRunIds.has(runId)) return
  const messageId = runId ? `run-${runId}` : buildMessageId(msg, msg.seq)
  const existing =
    runId && messagesState.value.length
      ? messagesState.value.find((item) => item.runId === runId)
      : null
  const existingText =
    existing && existing.text && existing.text !== '（空内容）'
      ? existing.text
      : ''
  const pendingState = typingStates.get(messageId)
  const baseText = pendingState?.targetText ?? existingText
  const deltaText = msg.content || ''
  const nextText = `${baseText}${deltaText}`
  const normalized = normalizeMessage(msg, {
    id: messageId,
    groupByRun: shouldGroupByRun(msg),
    allowEmpty: true,
    text: existingText,
  })
  if (existing?.attachments?.length && !normalized.attachments?.length) {
    normalized.attachments = existing.attachments
  }
  upsertMessage(normalized)
  if (nextText !== existingText) {
    startTyping(normalized.id, nextText)
  }
}

// 处理工具消息的 SSE 更新
const handleToolMessage = (msg) => {
  // 收集工具调用
  collectToolCall(msg)

  // 找到对应 run 的消息并更新其 toolCalls
  const parentRunId = getParentRunId(msg)
  if (!parentRunId) return

  const list = messagesState.value
  const existingIndex = list.findIndex(
    (item) => item.runId === parentRunId
  )

  if (existingIndex >= 0) {
    // 更新已存在消息的 toolCalls
    const existing = list[existingIndex]
    const next = [...list]
    next[existingIndex] = {
      ...existing,
      toolCalls: getToolCallsForRun(parentRunId),
    }
    messagesState.value = next
  } else {
    // 该 run 的 agent.message 还没到，创建临时占位消息
    const placeholderMsg = {
      id: `run-${parentRunId}`,
      seq: msg.seq ?? 0,
      role: 'agent',
      author: 'ARS',
      time: normalizeTime(msg.created_at),
      text: '',
      attachments: [],
      type: 'agent.pending',
      runId: parentRunId,
      dedupeKey: null,
      payload: null,
      toolCalls: getToolCallsForRun(parentRunId),
    }
    messagesState.value = [...list, placeholderMsg]
  }
}

const handleIncomingMessage = (msg) => {
  if (!msg) return
  // 忽略工具级别的 run.status
  if (isToolRunStatus(msg)) return
  if (applyRunStatus(msg)) return
  if (markDedupeKey(msg)) return

  // 处理工具消息
  if (isToolMessage(msg)) {
    handleToolMessage(msg)
    return
  }

  if (msg.type === 'message.delta') {
    appendDeltaMessage(msg)
    return
  }
  if (msg.type === 'agent.message') {
    const runId = getRunId(msg)
    if (runId) {
      finalizedRunIds.add(runId)
      stopTyping(`run-${runId}`)
    }
  }
  if (msg.type === 'error') {
    const runId = getRunId(msg)
    if (runId) finalizedRunIds.add(runId)
  }
  const normalized = normalizeMessage(msg, {
    groupByRun: shouldGroupByRun(msg),
  })
  upsertMessage(normalized)
}

const normalizedSummary = (data = {}) => ({
  title: data.session_name || '未命名会话',
  status: data.status || 'OPEN',
  updatedAt: data.updated_at?.slice(0, 16) || '--',
  lastSeq: data.last_seq ?? '--',
  tags: data.last_message_type ? [data.last_message_type] : [],
})

const runActive = computed(() => runStatus.value === 'RUNNING')
const inputLocked = computed(() => summary.status === 'CLOSED')
const messages = computed(() => messagesState.value || [])

const formatPayload = (payload) => {
  try {
    return JSON.stringify(payload ?? {}, null, 2)
  } catch (e) {
    return String(payload ?? '')
  }
}

const togglePayload = (id) => {
  const list = messagesState.value
  const idx = list.findIndex((item) => item.id === id)
  if (idx < 0) return
  const next = [...list]
  next[idx] = {
    ...next[idx],
    showPayload: !next[idx].showPayload,
  }
  messagesState.value = next
}

const handleFileChange = (uploadFile, uploadFilesList) => {
  fileList.value = uploadFilesList
  uploadFiles.value = uploadFilesList.map((file) => ({
    uid: file.uid,
    name: file.name,
    type: file.raw?.type?.startsWith('image/') ? 'image' : 'file',
    size: file.size ? `${Math.ceil(file.size / 1024)}KB` : '',
    url: file.url || (file.raw ? URL.createObjectURL(file.raw) : sampleImage),
  }))
}

const handleFileRemove = (file, list) => {
  const toAttachment = (item) => ({
    uid: item.uid,
    name: item.name,
    type:
      item.raw?.type?.startsWith('image/') || item.type?.startsWith('image/')
        ? 'image'
        : 'file',
    size: item.size ? `${Math.ceil(item.size / 1024)}KB` : '',
    url: item.url || (item.raw ? URL.createObjectURL(item.raw) : sampleImage),
  })

  if (Array.isArray(list)) {
    fileList.value = list
    uploadFiles.value = list.map(toAttachment)
    return
  }

  const filtered = fileList.value.filter((item) => item.uid !== file.uid)
  fileList.value = filtered
  uploadFiles.value = filtered.map(toAttachment)
}

const handleComposerKeydown = (event) => {
  if (event.key === 'Enter' && event.ctrlKey) {
    event.preventDefault()
    handleSend()
  }
}

const handleSend = () => {
  if (sending.value) return
  if (runActive.value) {
    ElMessage.info('Agent 正在处理中，请稍候再试')
    return
  }
  if (summary.status === 'CLOSED') {
    ElMessage.warning('会话已归档，无法发送')
    return
  }
  if (!sessionId.value) {
    ElMessage.warning('请先在左侧选择或新建会话')
    return
  }
  const token = localStorage.getItem('ars-token') || ''
  if (!token) {
    ElMessage.warning('请先登录')
    router.replace({ name: 'login' })
    return
  }
  if (!inputText.value.trim() && !uploadFiles.value.length) {
    ElMessage.warning('请输入内容或选择附件')
    return
  }

  const payload =
    uploadFiles.value.length > 0
      ? {
          attachments: uploadFiles.value.map((f) => ({
            name: f.name,
            type: f.type,
            size: f.size,
            url: f.url,
          })),
        }
      : null

  const body = {
    role: 'USER',
    type: 'user.prompt',
    content: inputText.value.trim(),
    payload,
    dedupe_key: `ui-${Date.now()}`,
  }

  sending.value = true
  axios
    .post(`${apiBase}/sessions/${sessionId.value}/messages`, body, {
      headers: { Authorization: `Bearer ${token}` },
    })
    .then((resp) => {
      const data = resp.data?.data || resp.data || {}
      const now = new Date()
      const nextMsg = normalizeMessage({
        ...data,
        role: 'USER',
        content: inputText.value.trim(),
        payload:
          uploadFiles.value.length > 0
            ? { attachments: uploadFiles.value }
            : null,
        created_at:
          data.created_at ||
          now.toISOString().slice(0, 19).replace('T', ' '),
      })
      upsertMessage(nextMsg)
      inputText.value = ''
      uploadFiles.value = []
      fileList.value = []
      nextTick(() => scrollToBottom())
      ElMessage.success('已发送')
    })
    .catch((err) => {
      const message =
        err.response?.data?.message ||
        err.message ||
        '发送失败，请稍后再试'
      ElMessage.error(message)
      if (err.response?.status === 401) router.replace({ name: 'login' })
    })
    .finally(() => {
      sending.value = false
    })
}

const handleArchive = async () => {
  if (!sessionId.value) return
  const token = localStorage.getItem('ars-token') || ''
  if (!token) {
    router.replace({ name: 'login' })
    return
  }
  archiveLoading.value = true
  try {
    const { data } = await axios.post(
      `${apiBase}/sessions/${sessionId.value}/archive`,
      {},
      { headers: { Authorization: `Bearer ${token}` } }
    )
    const info = data.data || data
    Object.assign(summary, normalizedSummary(info))
    lastSeq.value = info.last_seq || lastSeq.value
    ElMessage.success('会话已归档')
  } catch (err) {
    const message =
      err.response?.data?.message || '归档失败，请稍后再试'
    ElMessage.error(message)
    if (err.response?.status === 401) router.replace({ name: 'login' })
  } finally {
    archiveLoading.value = false
  }
}

const fetchSummary = async () => {
  const token = localStorage.getItem('ars-token') || ''
  if (!token) {
    router.replace({ name: 'login' })
    return
  }
  if (!sessionId.value) return
  try {
    const { data } = await axios.get(`${apiBase}/sessions/${sessionId.value}`, {
      headers: { Authorization: `Bearer ${token}` },
    })
    const info = data.data || data
    Object.assign(summary, normalizedSummary(info))
    if (info.last_seq) {
      lastSeq.value = info.last_seq
    }
  } catch (err) {
    // 后端可能不支持单条查询，容忍 404
    if (err.response?.status === 401) router.replace({ name: 'login' })
  }
}

const fetchMessages = async () => {
  const token = localStorage.getItem('ars-token') || ''
  if (!token) {
    router.replace({ name: 'login' })
    return
  }
  if (!sessionId.value) return
  loadingMessages.value = true
  errorMessage.value = ''
  try {
    const { data } = await axios.get(
      `${apiBase}/sessions/${sessionId.value}/messages`,
      {
        params: { after_seq: 0, limit: 200 },
        headers: { Authorization: `Bearer ${token}` },
      }
    )
    const list = data.data || []
    messagesState.value = normalizeMessageList(list)
    if (list.length) {
      const last = list[list.length - 1]
      lastSeq.value = last.seq || 0
      Object.assign(summary, {
        title: summary.title || '未命名会话',
        status: last.status || summary.status,
        lastSeq: last.seq ?? summary.lastSeq,
        updatedAt:
          last.created_at?.slice(0, 16) ||
          summary.updatedAt ||
          new Date().toLocaleString('zh-CN', {
            hour12: false,
            month: '2-digit',
            day: '2-digit',
            hour: '2-digit',
            minute: '2-digit',
          }),
      })
    }
  } catch (err) {
    const message =
      err.response?.data?.message || '获取消息失败，请稍后再试'
    errorMessage.value = message
    ElMessage.error(message)
    if (err.response?.status === 401) router.replace({ name: 'login' })
  } finally {
    loadingMessages.value = false
  }
}

const fetchSingleMessage = async (messageId) => {
  const token = localStorage.getItem('ars-token') || ''
  if (!token || !sessionId.value || !messageId) return
  try {
    const { data } = await axios.get(
      `${apiBase}/sessions/${sessionId.value}/messages/${messageId}`,
      { headers: { Authorization: `Bearer ${token}` } }
    )
    const detail = data.data || data
    handleIncomingMessage(detail)
  } catch (err) {
    if (err.response?.status === 401) router.replace({ name: 'login' })
  }
}

const scrollToBottom = () => {
  window.scrollTo({
    top:
      document.documentElement.scrollHeight ||
      document.body.scrollHeight ||
      0,
    behavior: 'smooth',
  })
}

const handleWindowScroll = () => {
  const doc = document.documentElement
  const top = window.scrollY || doc.scrollTop || 0
  const distance = doc.scrollHeight - top - window.innerHeight
  showJumpDown.value = distance > 160
}

const stopSse = () => {
  if (sseRetryTimer.value) {
    clearTimeout(sseRetryTimer.value)
    sseRetryTimer.value = null
  }
  if (eventSource.value) {
    eventSource.value.close()
    eventSource.value = null
  }
}

const checkSseAuth = async () => {
  const token = localStorage.getItem('ars-token') || ''
  if (!token) {
    router.replace({ name: 'login' })
    return false
  }
  try {
    await axios.get(`${apiBase}/me`, {
      headers: { Authorization: `Bearer ${token}` },
    })
    return true
  } catch (err) {
    if (err.response?.status === 401) {
      router.replace({ name: 'login' })
      return false
    }
    return true
  }
}

const startSse = () => {
  if (typeof window === 'undefined' || typeof EventSource === 'undefined')
    return
  if (!sessionId.value) return
  const token = localStorage.getItem('ars-token') || ''
  if (!token) {
    router.replace({ name: 'login' })
    return
  }

  stopSse()
  const url = new URL(`${apiBase}/sessions/${sessionId.value}/sse`)
  url.searchParams.set('after_seq', lastSeq.value || 0)
  url.searchParams.set('limit', 200)
  url.searchParams.set('token', token)

  const es = new EventSource(url.toString())
  es.onmessage = (event) => {
    try {
      const data = JSON.parse(event.data || '{}')
      if (!data) return
      if (!data.content && data.message_id && !data.type) {
        fetchSingleMessage(data.message_id)
        return
      }
      handleIncomingMessage(data)
      nextTick(() => scrollToBottom())
    } catch (e) {
      // ignore parse error
    }
  }
  es.onerror = () => {
    stopSse()
    checkSseAuth().then((authed) => {
      if (!authed) return
      sseRetryTimer.value = window.setTimeout(() => startSse(), 3000)
    })
  }
  eventSource.value = es
}

const applySession = () => {
  if (!sessionId.value) return
  stopSse()
  resetMessageCache()
  lastSeq.value = 0
  runStatus.value = 'DONE'
  localStorage.setItem('ars-current-session', sessionId.value)
  fetchSummary()
  fetchMessages().then(() => {
    nextTick(() => {
      scrollToBottom()
      startSse()
    })
  })
}

watch(
  () => route.params.id,
  (val) => {
    sessionId.value =
      val !== undefined
        ? String(val)
        : localStorage.getItem('ars-current-session') || ''
    applySession()
  },
  { immediate: true }
)

watch(
  messages,
  () => {
    nextTick(() => {
      setupMermaid()
      renderMermaid()
    })
  },
  { deep: true }
)

onMounted(() => {
  setupMermaid()
  window.addEventListener('scroll', handleWindowScroll, { passive: true })
  nextTick(() => {
    handleWindowScroll()
    renderMermaid()
  })
})

onBeforeUnmount(() => {
  window.removeEventListener('scroll', handleWindowScroll)
  stopAllTyping()
  stopSse()
})
</script>

<template>
  <div class="chat-shell">
    <section class="session-hero">
      <div class="hero-left">
        <p class="eyebrow">会话详情</p>
        <h2>{{ summary.title || '未命名会话' }}</h2>
        <div class="hero-meta">
          <el-tag
            :type="summary.status === 'OPEN' ? 'success' : summary.status === 'LOCKED' ? 'warning' : 'info'"
            effect="plain"
            size="small"
          >
            {{ summary.status || 'UNKNOWN' }}
          </el-tag>
          <span class="muted">最近更新 · {{ summary.updatedAt || '--' }}</span>
          <span class="muted">last_seq · {{ summary.lastSeq ?? '--' }}</span>
        </div>
        <div class="chips">
          <span v-for="tag in summary.tags || []" :key="tag" class="chip">
            {{ tag }}
          </span>
        </div>
      </div>
      <div class="hero-right">
        <div class="stat">
          <p class="stat-label">历史消息</p>
          <p class="stat-value">{{ messages.length }}</p>
        </div>
        <div class="stat">
          <p class="stat-label">草稿字数</p>
          <p class="stat-value">{{ inputText.length }}</p>
        </div>
        <el-button
          size="small"
          :loading="archiveLoading"
          :disabled="summary.status === 'CLOSED'"
          @click="handleArchive"
        >
          {{ summary.status === 'CLOSED' ? '已归档' : '归档会话' }}
        </el-button>
      </div>
    </section>

    <section class="chat-window">
      <div class="history">
        <el-skeleton v-if="loadingMessages" animated :rows="4" class="skeleton" />
        <el-alert
          v-else-if="errorMessage"
          :title="errorMessage"
          type="error"
          show-icon
          :closable="false"
          class="history-error"
        />
        <el-empty
          v-else-if="!messages.length"
          description="暂无消息"
          :image-size="100"
        />
        <template v-else>
          <div
            v-for="msg in messages"
            :key="msg.id"
            class="bubble"
            :class="[
              msg.role === 'user'
                ? 'bubble-user'
                : msg.role === 'system'
                ? 'bubble-system'
                : 'bubble-agent',
            ]"
          >
            <div class="bubble-head">
              <span class="author">{{ msg.author }}</span>
              <span class="time">{{ msg.time }}</span>
              <template v-if="msg.type === 'error'">
                <el-tag size="small" type="danger" effect="plain">
                  系统错误
                </el-tag>
              </template>
              <template v-else-if="msg.type === 'agent.pending'">
                <el-tag size="small" type="warning" effect="plain">
                  正在执行
                </el-tag>
              </template>
              <template v-else>
                <el-tag
                  v-if="msg.role === 'agent'"
                  size="small"
                  type="info"
                  effect="plain"
                >
                  ARS 回复
                </el-tag>
                <el-tag
                  v-else-if="msg.role === 'system'"
                  size="small"
                  type="warning"
                  effect="plain"
                >
                  系统
                </el-tag>
                <el-tag
                  v-else
                  size="small"
                  type="primary"
                  effect="plain"
                >
                  提示词
                </el-tag>
              </template>
            </div>
            <template v-if="msg.type === 'error'">
              <div class="error-card">
                <p class="bubble-text error-title">
                  {{ msg.text || '系统错误' }}
                </p>
                <button type="button" class="payload-toggle" @click="togglePayload(msg.id)">
                  {{ msg.showPayload ? '收起详情' : '查看详情' }}
                </button>
                <pre v-if="msg.showPayload" class="payload-json">
{{ formatPayload(msg.payload) }}
                </pre>
              </div>
            </template>
            <template v-else>
              <div
                class="bubble-text"
                v-html="renderMarkdown(msg.text)"
              />
            </template>
            <!-- 工具调用列表 (放在 agent 气泡底部) -->
            <div v-if="msg.toolCalls?.length" class="tool-calls-list">
              <template v-for="(tool, idx) in msg.toolCalls" :key="tool.toolCallId">
                <ToolCallCard
                  v-if="tool.toolName"
                  :tool-name="tool.toolName"
                  :tool-input="tool.toolInput"
                  :is-latest="tool.toolStatus === 'RUNNING' || (idx === msg.toolCalls.length - 1 && !msg.toolCalls.some(t => t.toolStatus === 'RUNNING'))"
                >
                  <div v-if="tool.toolStatus === 'RUNNING'" class="tool-loading">
                    <el-icon class="is-loading"><i class="el-icon-loading" /></el-icon>
                    <span>正在执行...</span>
                  </div>
                  <div v-else-if="tool.toolError" class="tool-error">
                    <el-alert :title="tool.toolError" type="error" :closable="false" />
                  </div>
                  <template v-else-if="tool.toolResult">
                    <LsResultCard
                      v-if="tool.toolName === 'ls'"
                      :result="tool.toolResult"
                    />
                    <FileReadResultCard
                      v-else-if="tool.toolName === 'file_read'"
                      :result="tool.toolResult"
                    />
                    <BashResultCard
                      v-else-if="tool.toolName === 'bash'"
                      :result="tool.toolResult"
                    />
                    <pre v-else class="tool-json">{{ JSON.stringify(tool.toolResult, null, 2) }}</pre>
                  </template>
                </ToolCallCard>
              </template>
            </div>
            <div v-if="msg.attachments?.length" class="attachment-list">
              <div
                v-for="file in msg.attachments"
                :key="file.name"
                class="attachment"
                :class="file.type"
              >
                <div
                  v-if="file.type === 'image'"
                  class="attachment-thumb"
                  :style="{ backgroundImage: file.url ? `url(${file.url})` : undefined }"
                />
                <div class="attachment-meta">
                  <span class="name">{{ file.name }}</span>
                  <span class="size">{{ file.size || '图像' }}</span>
                </div>
              </div>
            </div>
          </div>
        </template>
      </div>

      <div class="composer-dock">
        <button
          v-if="showJumpDown"
          type="button"
          class="jump-down"
          @click="scrollToBottom"
        >
          ↓ 回到底部
        </button>
        <div class="composer">
          <el-upload
            class="upload"
            multiple
            accept="image/*,.pdf,.txt,.doc,.docx,.md"
            :auto-upload="false"
            :disabled="inputLocked"
            :show-file-list="false"
            :file-list="fileList"
            :on-change="handleFileChange"
            :on-remove="handleFileRemove"
          >
            <button type="button" class="add-btn" aria-label="上传文件">
              +
            </button>
          </el-upload>
          <el-input
            v-model="inputText"
            type="textarea"
            :autosize="{ minRows: 3, maxRows: 8 }"
            class="composer-input"
            :placeholder="summary.status === 'CLOSED' ? '会话已归档，无法继续发送' : '输入消息，支持粘贴大段文本与附件...'"
            :disabled="inputLocked"
            @keydown="handleComposerKeydown"
          />
          <div class="send-wrap">
            <el-button
              color="#1d4ed8"
              class="send-btn"
              :loading="sending || runActive"
              :disabled="inputLocked || runActive"
              @click="handleSend"
            >
              发送
            </el-button>
            <span class="send-hint">
              <span class="keycap">Ctrl</span> +
              <span class="keycap">Enter</span> 发送
            </span>
          </div>
        </div>
      </div>

      <div v-if="uploadFiles.length" class="pending-attachments">
        <p class="pending-title">待发送附件</p>
        <div class="pending-list">
          <div
            v-for="file in uploadFiles"
            :key="file.uid"
            class="pending-chip"
          >
            <div
              v-if="file.type === 'image'"
              class="pending-thumb"
              :style="{ backgroundImage: file.url ? `url(${file.url})` : undefined }"
            />
            <div class="pending-meta">
              <span class="name">{{ file.name }}</span>
              <span class="size">{{ file.size || '文件' }}</span>
            </div>
            <button
              type="button"
              class="remove"
              aria-label="移除附件"
              @click="handleFileRemove(file)"
            >
              ×
            </button>
          </div>
        </div>
      </div>
    </section>
  </div>
</template>

<style scoped>
.chat-shell {
  display: flex;
  flex-direction: column;
  gap: 16px;
  min-height: 80vh;
  color: var(--text-primary);
}

.session-hero {
  display: flex;
  justify-content: space-between;
  gap: 18px;
  padding: 18px 20px;
  border-radius: 16px;
  background: var(--panel-hero-bg);
  border: 1px solid var(--panel-hero-border);
  box-shadow: var(--shadow-1);
}

.hero-left h2 {
  margin: 6px 0;
  color: var(--text-primary);
  font-size: 24px;
}

.eyebrow {
  margin: 0;
  font-size: 13px;
  letter-spacing: 0.08em;
  color: var(--text-muted);
  text-transform: uppercase;
}

.hero-meta {
  display: flex;
  align-items: center;
  gap: 10px;
  color: var(--text-muted);
  font-size: 13px;
}

.muted {
  color: var(--text-muted);
}

.chips {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin-top: 8px;
}

.chip {
  padding: 6px 10px;
  border-radius: 10px;
  background: var(--chip-bg);
  color: var(--chip-text);
  font-size: 12px;
  letter-spacing: 0.01em;
}

.hero-right {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
  gap: 10px;
  min-width: 260px;
}

.stat {
  padding: 12px 14px;
  border-radius: 12px;
  background: var(--surface);
  border: 1px solid var(--border-soft);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.8);
}

.stat-label {
  margin: 0 0 4px;
  color: var(--text-muted);
  font-size: 13px;
}

.stat-value {
  margin: 0;
  font-size: 20px;
  font-weight: 700;
  color: var(--text-primary);
}

.chat-window {
  display: flex;
  flex-direction: column;
  gap: 12px;
  padding: 16px;
  border-radius: 16px;
  background: var(--surface);
  border: 1px solid var(--border-soft);
  box-shadow: var(--shadow-1);
  align-items: center;
}

.history {
  display: flex;
  flex-direction: column;
  gap: 12px;
  width: min(1120px, 100%);
  padding: 0 16px;
  margin: 0 auto;
}

.skeleton {
  padding: 12px 8px;
}

.history-error {
  margin-bottom: 8px;
}

.composer-dock {
  position: sticky;
  bottom: 0;
  width: 100%;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 8px;
  padding: 8px 0;
  background: linear-gradient(180deg, transparent, var(--overlay-weak));
}

.jump-down {
  border: 1px solid rgba(37, 99, 235, 0.2);
  background: var(--surface-strong);
  color: var(--accent);
  border-radius: 999px;
  padding: 6px 12px;
  box-shadow: 0 10px 20px rgba(17, 24, 39, 0.08);
  cursor: pointer;
  transition: transform 0.12s ease, box-shadow 0.12s ease;
}

.jump-down:hover {
  transform: translateY(-1px);
  box-shadow: 0 14px 28px rgba(17, 24, 39, 0.12);
}

.bubble {
  padding: 14px 16px;
  border-radius: 16px;
  border: 1px solid var(--border-soft);
  max-width: 1040px;
  box-shadow: var(--shadow-2);
  width: min(1040px, calc(100% - 24px));
  align-self: center;
  background: var(--surface-strong);
  word-break: break-word;
  position: relative;
}

.bubble-user {
  border-color: var(--bubble-user-border);
  background: var(--bubble-user-bg);
}

.bubble-agent {
  border-color: var(--bubble-agent-border);
}
.bubble-system {
  border-color: var(--bubble-system-border);
  background: var(--bubble-system-bg);
}

/* 工具调用列表容器 */
.tool-calls-list {
  margin-top: 12px;
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.tool-loading {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 12px;
  color: var(--text-muted);
  font-size: 14px;
}

.tool-loading .is-loading {
  animation: spin 1s linear infinite;
}

@keyframes spin {
  from { transform: rotate(0deg); }
  to { transform: rotate(360deg); }
}

.tool-error {
  padding: 8px;
}

.tool-json {
  margin: 0;
  padding: 12px;
  background: var(--code-bg);
  color: var(--code-text);
  font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
  font-size: 13px;
  overflow: auto;
  max-height: 300px;
  border-radius: 0 0 12px 12px;
}

.bubble-head {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-bottom: 6px;
}

.author {
  font-weight: 700;
  color: var(--text-primary);
}

.time {
  font-size: 12px;
  color: var(--text-muted);
}

.bubble-text {
  margin: 0 0 8px;
  color: var(--text-primary);
  line-height: 1.7;
  font-size: 15px;
}
.bubble-text :deep(p) {
  margin: 0 0 10px;
}
.bubble-text :deep(p:last-child) {
  margin-bottom: 0;
}
.bubble-text :deep(ul),
.bubble-text :deep(ol) {
  padding-left: 20px;
  margin: 0 0 12px;
}
.bubble-text :deep(code) {
  background: var(--surface-subtle);
  border-radius: 6px;
  padding: 2px 6px;
  font-size: 13px;
}
.bubble-text :deep(pre) {
  margin: 10px 0;
  padding: 12px;
  border-radius: 10px;
  background: var(--code-bg);
  color: var(--code-text);
  font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
  overflow: auto;
  border: 1px solid var(--code-border);
}
.bubble-text :deep(table) {
  width: 100%;
  margin: 12px 0;
  border-collapse: separate;
  border-spacing: 0;
  border: 1.5px solid var(--table-border);
  border-radius: 12px;
  overflow: hidden;
  background: var(--surface-strong);
}
.bubble-text :deep(th),
.bubble-text :deep(td) {
  padding: 10px 12px;
  border-bottom: 1px solid var(--table-divider);
  border-right: 1px solid var(--table-divider);
  color: var(--text-primary);
  text-align: left;
}
.bubble-text :deep(tr:last-child td) {
  border-bottom: none;
}
.bubble-text :deep(th:last-child),
.bubble-text :deep(td:last-child) {
  border-right: none;
}
.bubble-text :deep(th) {
  background: linear-gradient(180deg, var(--surface), var(--surface-subtle));
  font-weight: 700;
}
.bubble-text :deep(.mermaid) {
  margin: 10px 0;
  padding: 12px;
  border-radius: 12px;
  background: linear-gradient(135deg, var(--mermaid-bg1), var(--mermaid-bg2));
  border: 1px solid rgba(37, 99, 235, 0.14);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.8);
}
.error-card {
  display: flex;
  flex-direction: column;
  gap: 8px;
  padding: 10px 12px;
  border-radius: 12px;
  background: var(--error-card-bg);
  border: 1px solid var(--error-card-border);
  box-shadow: 0 8px 16px rgba(0, 0, 0, 0.12);
}
.error-title {
  color: #b91c1c;
  font-weight: 700;
}
.payload-toggle {
  align-self: flex-start;
  padding: 6px 10px;
  border-radius: 10px;
  border: 1px solid var(--error-card-border);
  background: var(--surface-subtle);
  color: #f87171;
  cursor: pointer;
  transition: all 0.12s ease;
  font-weight: 600;
}
.payload-toggle:hover {
  background: rgba(248, 113, 113, 0.12);
  transform: translateY(-1px);
  box-shadow: 0 6px 16px rgba(0, 0, 0, 0.08);
}
.payload-json {
  margin: 0;
  padding: 12px;
  border-radius: 10px;
  background: var(--code-bg);
  color: var(--code-text);
  font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
  font-size: 13px;
  overflow: auto;
  max-height: 240px;
  white-space: pre;
}

.attachment-list {
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.attachment {
  display: grid;
  grid-template-columns: 52px 1fr;
  gap: 10px;
  align-items: center;
  padding: 8px 10px;
  border-radius: 10px;
  background: var(--attachment-bg);
  word-break: break-word;
}

.attachment.file {
  grid-template-columns: 1fr;
}

.attachment-thumb {
  width: 52px;
  height: 52px;
  border-radius: 12px;
  background-size: cover;
  background-position: center;
  background-image: linear-gradient(135deg, var(--mermaid-bg1), var(--mermaid-bg2));
  border: 1px solid var(--border-soft);
}

.attachment-meta .name {
  display: block;
  font-weight: 600;
  color: var(--text-primary);
}

.attachment-meta .size {
  display: block;
  color: var(--text-muted);
  font-size: 12px;
}

.composer {
  display: grid;
  grid-template-columns: auto 1fr auto;
  gap: 10px;
  align-items: flex-start;
  padding: 14px;
  border-radius: 14px;
  background: var(--composer-bg);
  border: 1px solid var(--border-soft);
  width: min(1120px, 100%);
  box-shadow: var(--shadow-1);
}

.add-btn {
  width: 44px;
  height: 44px;
  border-radius: 12px;
  border: 1px solid var(--border-soft);
  background: var(--surface-strong);
  box-shadow: 0 8px 18px rgba(17, 24, 39, 0.08);
  font-size: 24px;
  color: var(--accent);
  cursor: pointer;
  transition: transform 0.15s ease, box-shadow 0.15s ease;
}

.add-btn:hover {
  transform: translateY(-2px);
  box-shadow: 0 12px 24px rgba(17, 24, 39, 0.12);
}

.composer-input :deep(.el-textarea__inner) {
  background: var(--input-bg);
  border-radius: 12px;
  border: 1px solid var(--border-soft);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.8);
  min-height: 110px;
}

.send-btn {
  height: 44px;
  border-radius: 12px;
  font-weight: 700;
  padding: 0 18px;
}

.send-wrap {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 6px;
}

.send-hint {
  font-size: 12px;
  color: var(--text-muted);
  letter-spacing: 0.02em;
}

.keycap {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 2px 6px;
  border-radius: 8px;
  border: 1px solid var(--border-soft);
  background: var(--surface-strong);
  color: var(--text-muted);
  font-size: 11px;
  font-weight: 600;
}

.pending-attachments {
  padding: 10px 0 0;
  border-top: 1px dashed var(--border-soft);
  margin-top: 4px;
}

.pending-title {
  margin: 0 0 8px;
  font-size: 13px;
  color: var(--text-muted);
}

.pending-list {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
}

.pending-chip {
  display: grid;
  grid-template-columns: 48px 1fr 16px;
  gap: 8px;
  align-items: center;
  padding: 8px 10px;
  border-radius: 12px;
  background: var(--pending-bg);
  border: 1px solid var(--pending-border);
}

.pending-thumb {
  width: 48px;
  height: 48px;
  border-radius: 10px;
  background-size: cover;
  background-position: center;
  background-image: linear-gradient(135deg, #e0e7ff, #c7d2fe);
}

.pending-meta .name {
  display: block;
  font-weight: 600;
  color: var(--text-primary);
}

.pending-meta .size {
  display: block;
  font-size: 12px;
  color: var(--text-subtle);
}

.remove {
  cursor: pointer;
  color: var(--text-primary);
  font-size: 16px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
}

@media (max-width: 900px) {
  .session-hero {
    flex-direction: column;
  }

  .chat-window {
    padding: 12px;
  }

  .composer {
    grid-template-columns: 1fr;
    width: 100%;
  }

  .add-btn {
    width: 100%;
  }

  .send-btn {
    width: 100%;
  }

  .send-wrap {
    width: 100%;
    align-items: stretch;
  }
}
</style>
