<script setup>
import { ref, reactive, computed, onMounted } from 'vue'
import { NEmpty, NSpin, NIcon, NPopconfirm, NButton, useMessage } from 'naive-ui'
import { getWorkflowsWithRuns, getRunDetail, deleteRun } from '../../api/runHistory'
import {
  PlayCircleOutline,
  ChevronForwardOutline,
  TrashOutline,
  ChatbubbleEllipsesOutline,
  SparklesOutline,
  FlaskOutline,
  RocketOutline,
  ColorWandOutline,
  GitBranchOutline,
  ExitOutline,
  DownloadOutline
} from '@vicons/ionicons5'
import { unwrapNodeOutput, nodeOutputStatus, nodeOutputMessage } from '../../composables/useWorkflowRunner'
import AgentThinkingPanel from '../../components/agent-thinking/AgentThinkingPanel.vue'
import ThinkingLive from '../../components/agent-thinking/ThinkingLive.vue'
import { renderMarkdown } from '../../utils/markdown'
import { trimLeadingBlankLines } from '../../utils/hermesThinking'
import { logTypeLabel as sharedLogTypeLabel, isToolLogType } from '../../utils/logLabels'
import {
  UNKNOWN_NODE_TYPE_INFO,
  parseVisitVariables,
  buildReplayVisit
} from '../../utils/visitChain'
// 公共日志类型上色（10 类型，三处共用：ChatMessageList / WorkflowEditor / RunHistory）
import '../../styles/log-types.css'

const message = useMessage()

const loading = ref(false)
const workflows = ref([])   // [{ workflowId, workflowName, runs }]
const selectedRun = ref(null)
const runDetail = ref(null) // { run, nodes, visits, mode }
const expandedLogs = reactive({})
const expandedContext = reactive({})
/** 思考过程折叠状态：key=visitUuid，未设置默认收起（与 WorkflowEditor 保持一致） */
const expandedThinking = reactive({})
/** 折叠的智能体（workflowId 集合），默认全部展开 */
const collapsedWfs = reactive({})

function toggleWfCollapse(workflowId) {
  collapsedWfs[workflowId] = !collapsedWfs[workflowId]
}

const nodeTypeInfo = {
  userInput:   { color: '#10B981', typeLabel: '用户输入', icon: ChatbubbleEllipsesOutline },
  llm:         { color: '#8B5CF6', typeLabel: '大模型',   icon: SparklesOutline },
  skill:       { color: '#06B6D4', typeLabel: '技能',     icon: FlaskOutline },
  agent:       { color: '#F59E0B', typeLabel: '智能体',   icon: RocketOutline },
  smartAction: { color: '#EC4899', typeLabel: '智能操作', icon: ColorWandOutline },
  condition:   { color: '#F97316', typeLabel: '条件',     icon: GitBranchOutline },
  knowledgeRetrieval: { color: '#6366F1', typeLabel: '知识库检索' },
  output:      { color: '#EF4444', typeLabel: '输出',     icon: ExitOutline }
}

/**
 * v2.0：DAG / AGENTIC 模式统一为 Visit 卡片列表。
 *
 * <p>优先用后端 runDetail.visits（v2.0 起两种模式都持久化 Visit）；
 * 旧 DAG 运行记录无 Visit 数据时，从 runDetail.nodes 兜底转换为 visit 格式：
 *   - 每个节点一条 Visit，iterSeq=1，fromNodeId=null
 *   - 元数据从 node 字段直接取；logs/output/contextSnapshot 来自 WorkflowRunNode</p>
 *
 * <p>转换后字段与 buildReplayVisit 保持一致，模板统一。</p>
 */
const replayVisits = computed(() => {
  const detail = runDetail.value
  if (!detail) return []

  // 路径 1：v2.0+ 数据（DAG / AGENTIC 均持久化 Visit）
  const visits = detail.visits
  if (Array.isArray(visits) && visits.length > 0) {
    return visits.map(v => {
      const info = nodeTypeInfo[v.nodeType] || UNKNOWN_NODE_TYPE_INFO
      // 合并 info.icon（nodeTypeInfo 自带）与 visitTypeInfo 兜底（UNKNOWN_NODE_TYPE_INFO 无 icon）
      const merged = { ...info, icon: info.icon || null }
      return { ...buildReplayVisit(v, merged), icon: merged.icon }
    })
  }

  // 路径 2：旧 DAG 数据兜底（无 Visit，用 nodes 转换为 visit 格式）
  const nodes = detail.nodes
  if (!Array.isArray(nodes) || nodes.length === 0) return []
  return nodes.map((n, idx) => buildVisitFromLegacyNode(n, idx))
})

/**
 * 旧 DAG 运行记录兜底：把 WorkflowRunNode 转为 visit 格式。
 * 构造伪 Visit 对象（logsJson=n.logs / outputJson=n.output / variablesJson=n.contextSnapshot），
 * 复用 buildReplayVisit 的解析路径，避免重复实现 logs 还原逻辑。
 */
function buildVisitFromLegacyNode(n, idx) {
  const info = nodeTypeInfo[n.nodeType] || UNKNOWN_NODE_TYPE_INFO
  const merged = { ...info, icon: info.icon || PlayCircleOutline }
  const fakeVisit = {
    visitUuid: `legacy-${n.nodeId}-${idx}`,
    nodeId: n.nodeId,
    nodeType: n.nodeType,
    nodeLabel: n.label || n.nodeId || '节点',
    iterSeq: 1,
    fromVisitUuid: null,
    fromNodeId: null,
    sortOrder: idx,
    status: (n.status || '').toLowerCase(),
    errorMsg: n.error || null,
    outputJson: n.output || null,
    variablesJson: n.contextSnapshot || '{}',
    logsJson: n.logs || '',
    decisionJson: null
  }
  const rebuilt = buildReplayVisit(fakeVisit, merged)
  // 旧节点保留了 contextSnapshot，注入回 contextSnapshot 字段（buildReplayVisit 默认置 null）
  rebuilt.contextSnapshot = parseJson(n.contextSnapshot) || null
  return { ...rebuilt, icon: merged.icon }
}

/**
 * 当前运行的元数据 { runId, workingDirFiles }。
 * 重构后这两个字段不再注入 output 节点 envelope，而是作为 workflow-run 的属性持久化：
 *   - runId：直接读 runDetail.run.runId（8 位 UUID 字符串）
 *   - workingDirFiles：从 runDetail.run.workingDirFiles JSON 字符串解析为 List<String>
 */
const outputMeta = computed(() => {
  const run = runDetail.value?.run
  if (!run) return { runId: null, workingDirFiles: null }
  const filesRaw = parseJson(run.workingDirFiles)
  return {
    runId: run.runId || null,
    workingDirFiles: Array.isArray(filesRaw) ? filesRaw : null,
  }
})

onMounted(loadData)

async function loadData() {
  loading.value = true
  try {
    const res = await getWorkflowsWithRuns()
    workflows.value = res.data || []
  } catch (e) {
    message.error('加载运行记录失败')
  } finally {
    loading.value = false
  }
}

async function selectRun(run) {
  selectedRun.value = run
  // reactive 对象无 .value，需逐键删除以正确重置折叠状态
  Object.keys(expandedLogs).forEach(k => delete expandedLogs[k])
  Object.keys(expandedContext).forEach(k => delete expandedContext[k])
  Object.keys(expandedThinking).forEach(k => delete expandedThinking[k])
  try {
    const res = await getRunDetail(run.id)
    runDetail.value = res.data
  } catch (e) {
    message.error('加载运行详情失败')
  }
}

async function handleDelete(runId) {
  try {
    await deleteRun(runId)
    message.success('已删除')
    if (selectedRun.value?.id === runId) {
      selectedRun.value = null
      runDetail.value = null
    }
    await loadData()
  } catch (e) {
    message.error('删除失败')
  }
}

function statusText(s) {
  return { running: '执行中', success: '成功', failed: '失败', skipped: '跳过' }[s?.toLowerCase()] || s || '未知'
}

function statusClass(s) {
  return 'status-' + (s || 'unknown').toLowerCase()
}

function formatTime(t) {
  if (!t) return '-'
  // 兼容数组和字符串格式
  if (Array.isArray(t)) {
    const [y, m, d, h, min, s] = t
    return `${y}-${String(m).padStart(2, '0')}-${String(d).padStart(2, '0')} ${String(h).padStart(2, '0')}:${String(min).padStart(2, '0')}:${String(s || 0).padStart(2, '0')}`
  }
  return String(t).replace('T', ' ').substring(0, 19)
}

function duration(run) {
  if (!run.startedAt || !run.completedAt) return '-'
  let start = new Date(Array.isArray(run.startedAt) ? run.startedAt.join('-') : run.startedAt)
  let end = new Date(Array.isArray(run.completedAt) ? run.completedAt.join('-') : run.completedAt)
  let diff = Math.round((end - start) / 1000)
  if (diff < 60) return diff + ' 秒'
  if (diff < 3600) return Math.floor(diff / 60) + ' 分 ' + (diff % 60) + ' 秒'
  return Math.floor(diff / 3600) + ' 时 ' + Math.floor((diff % 3600) / 60) + ' 分'
}

function parseJson(str) {
  if (!str) return null
  try { return JSON.parse(str) } catch { return null }
}

// ========== Visit 卡片折叠状态：key=visitUuid（v2.0 与 WorkflowEditor 一致）==========
function visitTypeInfo(nodeType) {
  return nodeTypeInfo[nodeType] || UNKNOWN_NODE_TYPE_INFO
}

function toggleLogs(visitUuid) {
  expandedLogs[visitUuid] = !expandedLogs[visitUuid]
}

function toggleContext(visitUuid) {
  expandedContext[visitUuid] = !expandedContext[visitUuid]
}

function toggleThinking(visitUuid) {
  expandedThinking[visitUuid] = !expandedThinking[visitUuid]
}

/**
 * 将 contextSnapshot 中的值格式化为可显示字符串：
 * - 对象/数组：JSON.stringify（缩进 2）
 * - 字符串：原样返回，超长时由 CSS 限制高度
 * - 其他：String(value)
 */
function formatContextValue(value) {
  if (value === null || value === undefined) return 'null'
  if (typeof value === 'object') return JSON.stringify(value, null, 2)
  return String(value)
}

function logTypeLabel(type) {
  return sharedLogTypeLabel(type)
}

/**
 * 格式化日志时间戳为 HH:mm:ss。
 * 与 useWorkflowRunner.js 中 formatTimestamp 实现保持一致。
 */
function formatTimestamp(ts) {
  if (!ts) return ''
  const d = new Date(ts)
  return d.toLocaleTimeString('zh-CN', { hour12: false })
}

/**
 * 下载某次运行的工作目录 zip。
 * 后端接口：GET /api/workflows/{workflowId}/runs/{runId}/download
 * 响应已是 attachment + Content-Disposition，浏览器直接导航即可触发下载，
 * 无需经 axios 拦截器（拦截器要求 res.code===200，二进制响应会失败）。
 */
function downloadWorkspace(run) {
  if (!run?.workflowId || !run?.runId) {
    message.error('运行信息缺失，无法下载')
    return
  }
  window.location.href = `/api/workflows/${run.workflowId}/runs/${run.runId}/download`
}
</script>

<template>
  <div class="run-history-page">
    <div class="page-header">
      <h2>运行记录</h2>
    </div>

    <div v-if="loading" style="text-align: center; padding: 60px;">
      <n-spin size="large" />
    </div>

    <n-empty v-else-if="workflows.length === 0" description="暂无运行记录" style="margin-top: 80px;" />

    <div v-else class="history-layout">
      <!-- 左侧：工作流列表 -->
      <div class="left-panel">
        <div v-for="wf in workflows" :key="wf.workflowId" class="wf-group">
          <div class="wf-group-title" @click="toggleWfCollapse(wf.workflowId)">
            <n-icon size="12" class="wf-collapse-icon" :class="{ collapsed: collapsedWfs[wf.workflowId] }">
              <ChevronForwardOutline />
            </n-icon>
            <n-icon size="14"><PlayCircleOutline /></n-icon>
            <span>{{ wf.displayName || wf.workflowName }}</span>
            <span class="wf-run-count">{{ wf.runs.length }}</span>
          </div>
          <div v-if="wf.tags && wf.tags.length" class="wf-tags">
            <span v-for="tag in wf.tags" :key="tag.tagId" class="wf-tag">{{ tag.tagName }}</span>
          </div>
          <div v-show="!collapsedWfs[wf.workflowId]">
            <div
              v-for="run in wf.runs"
              :key="run.id"
              class="run-item"
              :class="{ active: selectedRun?.id === run.id }"
              @click="selectRun(run)"
            >
              <div class="run-item-top">
                <span class="run-status-dot" :class="statusClass(run.status)"></span>
                <span class="run-id">#{{ run.runId }}</span>
                <span class="run-badge" :class="statusClass(run.status)">{{ statusText(run.status) }}</span>
              </div>
              <div class="run-item-bottom">
                <span>{{ formatTime(run.startedAt) }}</span>
                <span v-if="run.completedAt" class="run-duration">{{ duration(run) }}</span>
              </div>
              <n-popconfirm @positive-click.stop="handleDelete(run.id)">
                <template #trigger>
                  <button class="run-delete-btn" @click.stop>
                    <n-icon size="12"><TrashOutline /></n-icon>
                  </button>
                </template>
                确定删除此运行记录？
              </n-popconfirm>
            </div>
          </div>
        </div>
      </div>

      <!-- 右侧：运行详情 -->
      <div class="right-panel">
        <div v-if="!runDetail" class="empty-detail">
          <n-empty description="选择左侧运行记录查看详情" />
        </div>
        <template v-else>
          <div class="detail-header">
            <div class="detail-title">
              <span class="run-status-dot" :class="statusClass(runDetail.run.status)"></span>
              运行 #{{ runDetail.run.runId }}
              <span class="run-badge" :class="statusClass(runDetail.run.status)">{{ statusText(runDetail.run.status) }}</span>
            </div>
            <div class="detail-meta">
              <span>开始: {{ formatTime(runDetail.run.startedAt) }}</span>
              <span v-if="runDetail.run.completedAt">完成: {{ formatTime(runDetail.run.completedAt) }}</span>
              <span v-if="runDetail.run.completedAt">耗时: {{ duration(runDetail.run) }}</span>
            </div>
            <div v-if="runDetail.run.error" class="detail-error">{{ runDetail.run.error }}</div>
          </div>

          <!-- 节点结果列表 -->
          <div class="detail-nodes">
            <!-- v2.0：DAG / AGENTIC 模式统一为 Visit 卡片链（与 WorkflowEditor 视觉一致）
                 每张卡片等价于"一次节点执行"，按 sortOrder 升序全局编号 #1 #2 ...
                 DAG 模式：每节点 1 张卡片，无 iterSeq/fromNodeId/Planner 决策徽章
                 AGENTIC 模式：同节点多次访问时出现多张卡片，由 iterSeq / fromNodeId / Planner 决策区分 -->
            <div v-if="replayVisits.length > 0" class="visit-chain">
              <div v-for="(v, idx) in replayVisits" :key="v.visitUuid || idx"
                   class="node-result-card visit-card"
                   :style="{ borderLeftColor: v.color }">
                <div class="nrc-header">
                  <div class="nrc-icon" :style="{ background: v.color }">
                    <n-icon size="12"><component :is="v.icon || visitTypeInfo(v.nodeType).icon" /></n-icon>
                  </div>
                  <div class="nrc-info">
                    <span class="nrc-label">
                      <span class="nrc-iter">#{{ idx + 1 }}</span>
                      {{ v.nodeLabel || v.nodeId }}
                    </span>
                    <span class="nrc-type">{{ v.typeLabel || visitTypeInfo(v.nodeType).typeLabel }}</span>
                  </div>
                  <span v-if="v.iterSeq && v.iterSeq > 1" class="vc-iter-badge">第 {{ v.iterSeq }} 次</span>
                  <span class="nrc-status" :class="statusClass(v.status)">{{ statusText(v.status) }}</span>
                </div>

                <!-- AGENTIC 自由跳转标识：仅当 decisionJson.offCanvas=true（非画布跳转）才展示，
                     换行显示以免挤占 #X + 节点名 + 第 N 次 + 状态 这一行的空间 -->
                <div v-if="v.offCanvas && v.fromNodeId" class="vc-from" :title="'从 ' + v.fromNodeId + ' 跳转（非画布连线）'">
                  自由跳转自 {{ v.fromNodeId }}
                </div>

                <!-- 实时状态位（已完成，currentStatus 为空，组件自动隐藏） -->
                <ThinkingLive :status="v.currentStatus || ''" />

                <!-- 流式思考过程（从持久化 THINKING 日志还原） -->
                <div v-if="v.streamingThinking" class="nrc-thinking">
                  <div class="nrc-thinking-header" @click="toggleThinking(v.visitUuid)" style="cursor: pointer">
                    <span class="nrc-thinking-tag">思考过程</span>
                    <span class="nrc-thinking-static">（{{ v.streamingThinking.length }} 字）</span>
                    <span class="nrc-thinking-expand" :class="{ expanded: expandedThinking[v.visitUuid] === true }">▶</span>
                  </div>
                  <pre v-show="expandedThinking[v.visitUuid] === true"
                       class="nrc-thinking-body">{{ trimLeadingBlankLines(v.streamingThinking) }}</pre>
                </div>

                <!-- Hermes 工具调用 / 待办清单（运行记录页无 pendingAsk） -->
                <AgentThinkingPanel
                  v-if="(v.toolHistory && v.toolHistory.length) || (v.todos && v.todos.length)"
                  :node="v"
                />

                <!-- 执行日志（与思考过程同款框，默认折叠） -->
                <div v-if="v.logs && v.logs.length" class="nrc-logs">
                  <div class="nrc-logs-header" @click="toggleLogs(v.visitUuid)" style="cursor: pointer">
                    <span class="nrc-logs-tag">执行日志</span>
                    <span class="nrc-logs-count">（{{ v.logs.length }}）</span>
                    <span class="nrc-logs-expand" :class="{ expanded: expandedLogs[v.visitUuid] === true }">▶</span>
                  </div>
                  <div v-show="expandedLogs[v.visitUuid] === true" class="nrc-logs-body">
                    <div class="nrc-log" v-for="(log, logIdx) in v.logs" :key="logIdx"
                         :class="'log-type-' + (log.type || 'info').toLowerCase()">
                      <span class="nrc-log-time">{{ formatTimestamp(log.timestamp) }}</span>
                      <span class="nrc-log-tag">{{ logTypeLabel(log.type) }}</span>
                      <template v-if="isToolLogType(log.type)">
                        <span class="nrc-log-tool">{{ log.message }}</span>
                        <span v-if="log.detail" class="nrc-log-msg">{{ log.detail }}</span>
                      </template>
                      <template v-else>
                        <span class="nrc-log-msg">{{ log.message }}</span>
                        <div v-if="log.detail" class="nrc-log-detail">{{ log.detail }}</div>
                      </template>
                    </div>
                  </div>
                </div>

                <!-- 节点输出（envelope 失败时显示红色徽章 + message） -->
                <div class="nrc-vars" v-if="unwrapNodeOutput(v.output) && Object.keys(unwrapNodeOutput(v.output)).length">
                  <div v-if="nodeOutputStatus(v.output) === 400" class="nrc-env-fail">
                    <span class="nrc-env-badge failed">失败</span>
                    <span class="nrc-env-msg">{{ nodeOutputMessage(v.output) }}</span>
                  </div>
                  <div class="nrc-var" v-for="(value, key) in unwrapNodeOutput(v.output)" :key="key">
                    <span class="nrc-var-key" :style="{ color: v.color }">{{ key }}</span>
                    <span class="nrc-var-value md-body" v-if="typeof value === 'string'" v-html="renderMarkdown(value)"></span>
                    <span class="nrc-var-value" v-else>{{ typeof value === 'object' ? JSON.stringify(value, null, 2) : String(value) }}</span>
                  </div>
                </div>

                <!-- 错误信息（失败 Visit 独有） -->
                <div v-if="v.errorMsg" class="vc-error">{{ v.errorMsg }}</div>

                <!-- 完整上下文快照（节点执行后的全部 variables，debug 视图）
                     v2.0 Visit：contextSnapshot 来自 buildReplayVisit 注入（仅 legacy 节点有）；
                     否则回退到 variablesJson（v2.0+ 数据） -->
                <div v-if="v.contextSnapshot || Object.keys(parseVisitVariables(v.variablesJson)).length"
                     class="nrc-ctx-toggle" @click="toggleContext(v.visitUuid)">
                  <span class="nrc-ctx-icon" :class="{ expanded: expandedContext[v.visitUuid] }">▶</span>
                  <span class="nrc-ctx-label">查看完整上下文（{{ Object.keys(v.contextSnapshot || parseVisitVariables(v.variablesJson)).length }} 个变量）</span>
                </div>
                <div v-if="expandedContext[v.visitUuid] && (v.contextSnapshot || parseVisitVariables(v.variablesJson))"
                     class="nrc-ctx-panel">
                  <div class="nrc-ctx-var" v-for="(value, key) in (v.contextSnapshot || parseVisitVariables(v.variablesJson))" :key="key">
                    <span class="nrc-ctx-key">{{ key }}</span>
                    <span class="nrc-ctx-value">{{ formatContextValue(value) }}</span>
                  </div>
                </div>
              </div>
            </div>
          </div>

          <!-- 工作空间下载：下载本次运行的工作目录 zip（应用 .agentignore 过滤） -->
          <div class="detail-footer">
            <!-- 元数据：运行ID + 工作空间文件列表，与编辑器 .nrc-clear-row 出现时机一致 -->
            <div v-if="outputMeta.runId" class="run-meta-id">
              <span class="run-meta-label">运行ID：</span>
              <code class="run-meta-value">{{ outputMeta.runId }}</code>
            </div>
            <details v-if="outputMeta.workingDirFiles" class="run-meta-files">
              <summary>工作空间文件列表（{{ outputMeta.workingDirFiles.length }}）</summary>
              <ul class="run-meta-files-list">
                <li v-for="f in outputMeta.workingDirFiles" :key="f">{{ f }}</li>
              </ul>
            </details>
            <n-button quaternary size="small" @click="downloadWorkspace(runDetail.run)">
              <template #icon>
                <n-icon size="14"><DownloadOutline /></n-icon>
              </template>
              下载工作空间
            </n-button>
          </div>
        </template>
      </div>
    </div>
  </div>
</template>

<style scoped>
.run-history-page {
  padding: 24px 32px;
  height: 100vh;
  display: flex;
  flex-direction: column;
  overflow: hidden;
}

.page-header {
  margin-bottom: 20px;
  flex-shrink: 0;
}

.page-header h2 {
  font-size: 20px;
  font-weight: 700;
  color: var(--text-primary, #e0e0e0);
  margin: 0;
}

.history-layout {
  flex: 1;
  display: flex;
  gap: 20px;
  min-height: 0;
}

/* 左侧面板 */
.left-panel {
  width: 320px;
  flex-shrink: 0;
  overflow-y: auto;
  padding-right: 8px;
}

.wf-group {
  margin-bottom: 20px;
}

.wf-group-title {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 14px;
  font-weight: 600;
  color: var(--text-primary, #e0e0e0);
  margin-bottom: 8px;
  padding: 4px;
  cursor: pointer;
  user-select: none;
  border-radius: 6px;
  transition: background 0.15s;
}

.wf-group-title:hover { background: rgba(255,255,255,0.04); }

.wf-collapse-icon {
  transition: transform 0.2s;
  color: var(--text-tertiary, #888);
}

/* 默认展开：图标旋转 90° 朝下；折叠时复原朝右 */
.wf-collapse-icon:not(.collapsed) { transform: rotate(90deg); }

.wf-run-count {
  font-size: 11px;
  background: rgba(99, 102, 241, 0.15);
  color: #818cf8;
  padding: 1px 6px;
  border-radius: 8px;
  margin-left: auto;
}

.wf-tags {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  padding: 0 4px 8px;
}

.wf-tag {
  font-size: 10px;
  padding: 2px 8px;
  border: 1px solid rgba(255, 255, 255, 0.35);
  border-radius: 10px;
  color: rgba(255, 255, 255, 0.6);
  white-space: nowrap;
  line-height: 1.4;
}

.run-item {
  position: relative;
  padding: 10px 12px;
  border-radius: 8px;
  border: 1px solid var(--border-color, rgba(255,255,255,0.08));
  margin-bottom: 6px;
  cursor: pointer;
  transition: all 0.15s;
}

.run-item:hover {
  background: rgba(37, 99, 235, 0.06);
  border-color: rgba(37, 99, 235, 0.2);
}

.run-item.active {
  background: rgba(37, 99, 235, 0.1);
  border-color: rgba(37, 99, 235, 0.3);
}

.run-item-top {
  display: flex;
  align-items: center;
  gap: 6px;
  margin-bottom: 4px;
}

.run-status-dot {
  width: 6px;
  height: 6px;
  border-radius: 50%;
  flex-shrink: 0;
}

.run-status-dot.status-running { background: #818cf8; }
.run-status-dot.status-success { background: #34d399; }
.run-status-dot.status-failed  { background: #f87171; }

.run-id {
  font-size: 12px;
  font-weight: 600;
  color: var(--text-primary, #e0e0e0);
  font-family: 'Cascadia Code', monospace;
}

.run-badge {
  font-size: 10px;
  padding: 1px 5px;
  border-radius: 4px;
  font-weight: 600;
  margin-left: auto;
}

.run-badge.status-running  { background: rgba(99,102,241,0.15); color: #818cf8; }
.run-badge.status-success   { background: rgba(16,185,129,0.15); color: #34d399; }
.run-badge.status-failed    { background: rgba(239,68,68,0.15);  color: #f87171; }

.run-item-bottom {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 11px;
  color: var(--text-tertiary, #888);
}

.run-duration {
  margin-left: auto;
  color: var(--text-secondary, #aaa);
}

.run-delete-btn {
  position: absolute;
  top: 6px;
  right: 6px;
  background: none;
  border: none;
  color: var(--text-tertiary, #666);
  cursor: pointer;
  padding: 2px;
  border-radius: 4px;
  opacity: 0;
  transition: opacity 0.15s;
}

.run-item:hover .run-delete-btn {
  opacity: 1;
}

.run-delete-btn:hover {
  color: #f87171;
  background: rgba(239,68,68,0.1);
}

/* 右侧详情面板 */
.right-panel {
  flex: 1;
  overflow-y: auto;
  min-width: 0;
}

.empty-detail {
  display: flex;
  align-items: center;
  justify-content: center;
  height: 100%;
}

.detail-footer {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 6px;
  padding: 16px 0 4px;
  margin-top: 12px;
  border-top: 1px dashed var(--border-color, rgba(255,255,255,0.08));
}

/* 元数据：运行ID */
.run-meta-id {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  font-size: 12px;
  color: var(--text-secondary, #aaa);
}

.run-meta-label {
  color: var(--text-secondary, #888);
}

.run-meta-value {
  font-family: 'Cascadia Code', 'Fira Code', 'JetBrains Mono', ui-monospace, monospace;
  font-size: 11px;
  color: var(--primary, #c084fc);
  background: rgba(168, 85, 247, 0.08);
  padding: 1px 6px;
  border-radius: 3px;
}

/* 元数据：工作空间文件列表（<details> 折叠） */
.run-meta-files {
  width: 100%;
  max-width: 640px;
  font-size: 12px;
  background: rgba(255, 255, 255, 0.02);
  border: 1px solid rgba(255, 255, 255, 0.06);
  border-radius: 4px;
  padding: 4px 10px;
}

.run-meta-files > summary {
  cursor: pointer;
  user-select: none;
  color: var(--primary, #c084fc);
  list-style: none;
}

.run-meta-files > summary::-webkit-details-marker {
  display: none;
}

.run-meta-files > summary::before {
  content: '▸';
  display: inline-block;
  margin-right: 6px;
  transition: transform 0.15s;
}

.run-meta-files[open] > summary::before {
  transform: rotate(90deg);
}

.run-meta-files-list {
  margin: 6px 0 2px;
  padding-left: 20px;
  font-family: 'Cascadia Code', 'Fira Code', 'JetBrains Mono', ui-monospace, monospace;
  font-size: 11px;
  color: var(--text-secondary, #ccc);
  max-height: 220px;
  overflow-y: auto;
}

.run-meta-files-list li {
  line-height: 1.6;
  word-break: break-all;
}

.detail-header {
  margin-bottom: 16px;
  padding-bottom: 12px;
  border-bottom: 1px solid var(--border-color, rgba(255,255,255,0.08));
}

.detail-title {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 16px;
  font-weight: 600;
  color: var(--text-primary, #e0e0e0);
  margin-bottom: 6px;
}

.detail-meta {
  display: flex;
  gap: 16px;
  font-size: 12px;
  color: var(--text-tertiary, #888);
}

.detail-error {
  margin-top: 8px;
  padding: 8px 12px;
  background: rgba(239,68,68,0.1);
  border: 1px solid rgba(239,68,68,0.2);
  border-radius: 6px;
  color: #f87171;
  font-size: 12px;
}

/* 节点结果卡片 - 复用 WorkflowEditor 样式 */
.node-result-card {
  background: rgba(255,255,255,0.03);
  border-radius: 8px;
  border-left: 3px solid #666;
  margin-bottom: 10px;
  overflow: hidden;
}

.nrc-header {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 8px 10px;
  border-bottom: 1px solid rgba(255,255,255,0.04);
}

.nrc-icon {
  width: 22px;
  height: 22px;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 5px;
  color: #fff;
  flex-shrink: 0;
}

.nrc-info {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 1px;
}

.nrc-label {
  font-size: 12px;
  font-weight: 600;
  color: var(--text-primary, #e0e0e0);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.nrc-type {
  font-size: 10px;
  color: var(--text-tertiary, #666);
}

.nrc-status {
  font-size: 10px;
  padding: 2px 6px;
  border-radius: 4px;
  font-weight: 600;
  flex-shrink: 0;
}

.nrc-status.status-running  { background: rgba(99,102,241,0.15); color: #818cf8; }
.nrc-status.status-success   { background: rgba(16,185,129,0.15); color: #34d399; }
.nrc-status.status-failed    { background: rgba(239,68,68,0.15);  color: #f87171; }
.nrc-status.status-skipped   { background: rgba(107,114,128,0.15); color: #9ca3af; }

.nrc-vars { padding: 6px 10px; }

/* envelope 失败提示条：status=400 时显示在变量列表顶部 */
.nrc-env-fail {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 4px 8px;
  margin-bottom: 6px;
  background: rgba(239, 68, 68, 0.08);
  border-left: 2px solid #ef4444;
  border-radius: 3px;
  font-size: 11px;
}

.nrc-env-badge {
  font-weight: 600;
  padding: 1px 6px;
  border-radius: 3px;
  flex-shrink: 0;
}

.nrc-env-badge.failed {
  background: rgba(239, 68, 68, 0.15);
  color: #f87171;
}

.nrc-env-msg {
  color: #f87171;
  word-break: break-all;
}

.nrc-var {
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: 4px 0;
  border-bottom: 1px solid rgba(255,255,255,0.02);
}

.nrc-var:last-child { border-bottom: none; }

.nrc-var-key {
  font-family: 'Cascadia Code', 'Fira Code', monospace;
  font-size: 11px;
  font-weight: 600;
}

.nrc-var-value {
  font-family: 'Cascadia Code', 'Fira Code', monospace;
  font-size: 11px;
  color: var(--text-secondary, #aaa);
  white-space: pre-wrap;
  word-break: break-all;
  max-height: 120px;
  overflow-y: auto;
}

/* md-body：字符串输出值用 markdown 富文本渲染，覆盖等宽字体/折行设置 */
.nrc-var-value.md-body {
  font-family: inherit;
  font-size: 12px;
  line-height: 1.6;
  color: var(--text-primary, #e0e0e0);
  white-space: normal;
  word-break: break-word;
  max-height: 240px;
  overflow-y: auto;
}
.nrc-var-value.md-body :deep(*:first-child) { margin-top: 0; }
.nrc-var-value.md-body :deep(*:last-child) { margin-bottom: 0; }
.nrc-var-value.md-body :deep(p) { margin: 4px 0; }
.nrc-var-value.md-body :deep(h1),
.nrc-var-value.md-body :deep(h2),
.nrc-var-value.md-body :deep(h3),
.nrc-var-value.md-body :deep(h4),
.nrc-var-value.md-body :deep(h5),
.nrc-var-value.md-body :deep(h6) {
  margin: 8px 0 4px;
  font-weight: 700;
  color: #f1f5f9;
}
.nrc-var-value.md-body :deep(h6) { font-size: 13px; }
.nrc-var-value.md-body :deep(h5) { font-size: 14px; }
.nrc-var-value.md-body :deep(h4) { font-size: 15px; }
.nrc-var-value.md-body :deep(h3) { font-size: 16px; }
.nrc-var-value.md-body :deep(h2) { font-size: 17px; }
.nrc-var-value.md-body :deep(h1) { font-size: 18px; }
.nrc-var-value.md-body :deep(ul),
.nrc-var-value.md-body :deep(ol) { margin: 4px 0; padding-left: 22px; }
.nrc-var-value.md-body :deep(li) { margin: 2px 0; }
.nrc-var-value.md-body :deep(blockquote) {
  margin: 6px 0;
  padding: 2px 10px;
  border-left: 3px solid rgba(168, 85, 247, 0.5);
  background: rgba(168, 85, 247, 0.06);
  color: var(--text-secondary, #b0b0b0);
}
.nrc-var-value.md-body :deep(code) {
  font-family: 'Cascadia Code', 'Fira Code', ui-monospace, monospace;
  font-size: 12px;
  padding: 1px 5px;
  border-radius: 3px;
  background: rgba(255, 255, 255, 0.08);
  color: #fbbf24;
}
.nrc-var-value.md-body :deep(pre) {
  margin: 6px 0;
  padding: 8px 10px;
  background: rgba(0, 0, 0, 0.32);
  border: 1px solid rgba(255, 255, 255, 0.08);
  border-radius: 4px;
  overflow-x: auto;
}
.nrc-var-value.md-body :deep(pre code) {
  background: transparent;
  color: var(--text-primary, #e0e0e0);
  padding: 0;
  font-size: 11.5px;
  line-height: 1.5;
}
.nrc-var-value.md-body :deep(a) {
  color: #93c5fd;
  border-bottom: 1px dashed rgba(147, 197, 253, 0.4);
  text-decoration: none;
}
.nrc-var-value.md-body :deep(table) {
  width: 100%;
  margin: 6px 0;
  border-collapse: collapse;
  font-size: 12px;
  display: block;
  overflow-x: auto;
}
.nrc-var-value.md-body :deep(thead) { background: rgba(168, 85, 247, 0.12); }
.nrc-var-value.md-body :deep(th),
.nrc-var-value.md-body :deep(td) {
  padding: 4px 8px;
  border: 1px solid rgba(255, 255, 255, 0.1);
  text-align: left;
  vertical-align: top;
}
.nrc-var-value.md-body :deep(th) { font-weight: 600; color: #e2e8f0; }
.nrc-var-value.md-body :deep(tbody tr:nth-child(2n+1)) { background: rgba(255, 255, 255, 0.02); }
.nrc-var-value.md-body :deep(.katex-display) {
  margin: 6px 0;
  overflow-x: auto;
  overflow-y: hidden;
}
.nrc-var-value.md-body :deep(.katex) { font-size: 1.05em; color: #e2e8f0; }

/* 流式思考过程区块（与 WorkflowEditor 保持一致：紫色框 + 等宽字体） */
.nrc-thinking {
  margin: 6px 10px;
  border-radius: 6px;
  background: rgba(168, 85, 247, 0.05);
  border: 1px solid rgba(168, 85, 247, 0.2);
  overflow: hidden;
  /* 与 .tool-calls-wrap / .todo-panel / .nrc-logs 保持一致的等宽字体 */
  font-family: 'Cascadia Code', 'Fira Code', 'JetBrains Mono', ui-monospace, monospace;
}

.nrc-thinking-header {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 5px 10px;
  font-size: 11px;
  background: rgba(168, 85, 247, 0.08);
}

.nrc-thinking-tag {
  color: #c084fc;
  font-weight: 600;
}

.nrc-thinking-static {
  color: #888;
  font-size: 10px;
}

.nrc-thinking-expand {
  margin-left: auto;
  font-size: 10px;
  color: var(--text-tertiary, #666);
  transition: transform 0.2s;
  flex-shrink: 0;
}
.nrc-thinking-expand.expanded {
  transform: rotate(90deg);
}

.nrc-thinking-body {
  margin: 0;
  padding: 8px 12px;
  max-height: 320px;
  overflow: auto;
  font-family: 'Cascadia Code', 'Fira Code', 'JetBrains Mono', ui-monospace, monospace;
  font-size: 11px;
  line-height: 1.6;
  color: var(--text-secondary, #b0b0b0);
  white-space: pre-wrap;
  word-break: break-word;
}

.nrc-error {
  padding: 6px 10px;
  color: #f87171;
  font-size: 11px;
}

/* 完整上下文快照折叠区 */
.nrc-ctx-toggle {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 6px 10px;
  cursor: pointer;
  border-top: 1px solid rgba(255,255,255,0.04);
  font-size: 11px;
  color: var(--text-secondary, #aaa);
  user-select: none;
}

.nrc-ctx-toggle:hover { background: rgba(99,102,241,0.05); }

.nrc-ctx-icon {
  font-size: 9px;
  color: var(--text-tertiary, #888);
  transition: transform 0.2s;
}

.nrc-ctx-icon.expanded { transform: rotate(90deg); }

.nrc-ctx-label { font-weight: 500; }

.nrc-ctx-panel {
  padding: 6px 10px;
  border-top: 1px solid rgba(255,255,255,0.04);
  max-height: 280px;
  overflow-y: auto;
  background: rgba(0,0,0,0.15);
}

.nrc-ctx-var {
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: 4px 0;
  border-bottom: 1px solid rgba(255,255,255,0.02);
}

.nrc-ctx-var:last-child { border-bottom: none; }

.nrc-ctx-key {
  font-family: 'Cascadia Code', 'Fira Code', monospace;
  font-size: 11px;
  font-weight: 600;
  color: #818cf8;
}

.nrc-ctx-value {
  font-family: 'Cascadia Code', 'Fira Code', monospace;
  font-size: 11px;
  color: var(--text-secondary, #ccc);
  white-space: pre-wrap;
  word-break: break-all;
  max-height: 80px;
  overflow-y: auto;
}

.nrc-logs {
  margin: 6px 10px;
  border-radius: 6px;
  background: rgba(168, 85, 247, 0.05);
  border: 1px solid rgba(168, 85, 247, 0.2);
  overflow: hidden;
  font-family: 'Cascadia Code', 'Fira Code', 'JetBrains Mono', ui-monospace, monospace;
}

/* 日志区 header：与 .nrc-thinking-header 视觉一致 */
.nrc-logs-header {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 5px 10px;
  font-size: 11px;
  background: rgba(168, 85, 247, 0.08);
  cursor: pointer;
  user-select: none;
}

.nrc-logs-tag {
  color: #c084fc;
  font-weight: 600;
}

.nrc-logs-count {
  color: #888;
  font-size: 10px;
}

/* 展开箭头：与 .nrc-thinking-expand 一致 */
.nrc-logs-expand {
  margin-left: auto;
  font-size: 10px;
  color: var(--text-tertiary, #666);
  transition: transform 0.2s;
  flex-shrink: 0;
}

.nrc-logs-expand.expanded {
  transform: rotate(90deg);
}

.nrc-logs-body {
  max-height: 360px;
  overflow-y: auto;
}

.nrc-log {
  padding: 5px 10px;
  border-bottom: 1px solid rgba(168, 85, 247, 0.1);
  font-size: 11px;
  line-height: 1.5;
}

.nrc-log:last-child { border-bottom: none; }

.nrc-log-time {
  font-family: inherit;
  font-size: 10px;
  color: var(--text-tertiary, #555);
  margin-right: 6px;
}

.nrc-log-tag {
  font-size: 10px;
  padding: 1px 4px;
  border-radius: 3px;
  margin-right: 6px;
  font-weight: 600;
}

.nrc-log-tool {
  font-size: 10px;
  color: #fbbf24;
  font-weight: 600;
  margin-right: 6px;
}

/* v1.5：log-type-* 上色规则已迁至 src/styles/log-types.css（10 类型三处共用） */

.nrc-log-msg {
  display: block;
  margin-top: 2px;
  color: var(--text-secondary, #ccc);
  word-break: break-all;
  white-space: pre-wrap;
}

.nrc-log-detail {
  margin-top: 3px;
  font-family: 'Cascadia Code', 'Fira Code', monospace;
  font-size: 10px;
  color: var(--text-tertiary, #888);
  background: rgba(0,0,0,0.2);
  padding: 4px 6px;
  border-radius: 4px;
  white-space: pre-wrap;
  word-break: break-all;
  max-height: 100px;
  overflow-y: auto;
}

/* ============== AGENTIC 模式：Visit 链 ============== */
/* 与 DAG 节点卡片视觉统一：visit-card 复用 .node-result-card 的圆角/边框/无 padding 风格，
   左边色条由 :style="borderLeftColor" 按节点类型上色（与 DAG 一致），
   不再按状态（running/success/failed）覆盖；状态色由 .nrc-status 胶囊表示。
   同时叠加 .visit-card 类作为标记，便于将来差异化微调。 */
.visit-chain {
  margin-bottom: 16px;
}

.visit-card {
  margin-bottom: 10px;
  font-size: 12px;
}

/* Visit 卡片内部 body：恢复 DAG 卡片 header 之下各子块的 padding（与 .nrc-vars 一致） */
.vc-body {
  padding: 6px 10px;
}

/* #序号徽章：DAG / Agentic 共用，紫色胶囊 */
.nrc-iter {
  font-family: 'Consolas', 'Monaco', monospace;
  font-size: 10px;
  color: #818cf8;
  background: rgba(129, 140, 248, 0.14);
  padding: 1px 6px;
  border-radius: 3px;
  margin-right: 4px;
  vertical-align: 1px;
}

.vc-iter-badge {
  font-size: 10px;
  color: #e0a526;
  background: rgba(224, 165, 38, 0.12);
  padding: 1px 5px;
  border-radius: 3px;
  flex-shrink: 0;
}

.vc-from {
  display: block;
  margin: 4px 0 2px;
  padding: 2px 6px;
  font-size: 11px;
  color: #06b6d4;
  background: rgba(6, 182, 212, 0.12);
  border-radius: 3px;
  border-left: 2px solid #06b6d4;
}

.vc-error {
  color: #ef4444;
  background: rgba(239, 68, 68, 0.08);
  padding: 4px 8px;
  border-radius: 3px;
  margin: 4px 0;
  font-size: 11px;
}

.vc-output,
.vc-vars-details {
  margin-top: 6px;
}

.vc-section-title {
  font-size: 10px;
  color: #9ca3af;
  text-transform: uppercase;
  letter-spacing: 0.5px;
  margin-bottom: 3px;
}

.vc-var {
  display: flex;
  gap: 8px;
  padding: 3px 0;
  border-bottom: 1px solid rgba(255, 255, 255, 0.04);
  align-items: flex-start;
}

.vc-var:last-child {
  border-bottom: none;
}

.vc-var-key {
  color: #a5b4fc;
  font-family: 'Consolas', 'Monaco', monospace;
  font-size: 11px;
  min-width: 100px;
  max-width: 200px;
  flex-shrink: 0;
  word-break: break-all;
}

.vc-var-value {
  color: #d1d5db;
  font-size: 11px;
  word-break: break-word;
  flex: 1;
  white-space: pre-wrap;
}

/* md-body：字符串输出值用 markdown 富文本渲染，覆盖等宽字体/折行设置 */
.vc-var-value.md-body {
  font-family: inherit;
  font-size: 12px;
  line-height: 1.6;
  color: var(--text-primary, #e0e0e0);
  white-space: normal;
  word-break: break-word;
  max-height: 240px;
  overflow-y: auto;
}

.vc-vars-details summary {
  cursor: pointer;
  font-size: 11px;
  color: #9ca3af;
  padding: 3px 0;
  user-select: none;
}

.vc-vars-details summary:hover {
  color: #d1d5db;
}

</style>
