import React from "react";
import { fmtClock, fmtDate } from "./time";
import { LoadingTitle } from "./loading-indicator";
import { Background, BaseEdge, Controls, Handle, MarkerType, Position, ReactFlow, type Edge, type Node } from "@xyflow/react";
import { TraceView, opencodeTracePort } from "./trace";
import { errorMessage, requestJson as requestUniDeskJson } from "./unidesk-error";
import { UniDeskErrorBanner } from "./unidesk-error-banner";
import { useNotification } from "./notification-context";

type AnyRecord = Record<string, any>;

const h = React.createElement;
const { useEffect } = React;
const useState: any = React.useState;
const useRef: any = React.useRef;

const pipelineInputPorts: AnyRecord[] = [
  { id: "in-left", side: "left", position: Position.Left, style: { top: "50%" } },
  { id: "in-top-left", side: "top", slot: "left", slotIndex: -1, position: Position.Top, style: { left: "28%" } },
  { id: "in-top-mid", side: "top", slot: "mid", slotIndex: 0, position: Position.Top, style: { left: "50%" } },
  { id: "in-top-right", side: "top", slot: "right", slotIndex: 1, position: Position.Top, style: { left: "72%" } },
  { id: "in-bottom-left", side: "bottom", slot: "left", slotIndex: -1, position: Position.Bottom, style: { left: "28%" } },
  { id: "in-bottom-mid", side: "bottom", slot: "mid", slotIndex: 0, position: Position.Bottom, style: { left: "50%" } },
  { id: "in-bottom-right", side: "bottom", slot: "right", slotIndex: 1, position: Position.Bottom, style: { left: "72%" } },
];

const pipelineOutputPorts: AnyRecord[] = [
  { id: "out-right", position: Position.Right, style: { top: "50%" } },
];

const pipelineOverlapEdgePalette = ["#4eb7a8", "#d7a13a", "#69aee8", "#e0835f", "#b7d86b", "#d98bd2", "#5fc6bf"];
const pipelineNodeWidth = 236;
const pipelineNodeHeight = 88;
const pipelineAutoRefreshMs = 15000;
const pipelineSnapshotRunLimit = 10;
const pipelineGanttTimeAxisWidth = 96;
const pipelineGanttNodeColumnWidth = 72;
const pipelineGanttHeaderHeight = 64;
const pipelineGanttArrowTipInsetPx = 12;

function isDocumentVisible(): boolean {
  return typeof document === "undefined" || document.visibilityState !== "hidden";
}

function pipelinePercent(value: any, fallback: number): number {
  const number = Number.parseFloat(String(value || ""));
  return Number.isFinite(number) ? number / 100 : fallback;
}

function pipelineEndpointScore(port: AnyRecord, loads: Map<string, number>, laneHint: number): number {
  const side = String(port.side || "");
  if (side !== "top" && side !== "bottom") return 0;
  const slotIndex = Number(port.slotIndex || 0);
  const midId = side === "top" ? "in-top-mid" : "in-bottom-mid";
  const load = loads.get(port.id) || 0;
  const midLoad = loads.get(midId) || 0;
  if (slotIndex === 0) return midLoad === 0 ? -26 : 28 + load * 74;
  const lanePreference = laneHint === 0 ? Math.abs(slotIndex) * 2 : (Math.sign(laneHint) === Math.sign(slotIndex) ? -3 : 3);
  if (midLoad > 0 && load === 0) return -14 + lanePreference;
  return 8 + load * 74 + lanePreference;
}

function pipelineSmoothPath(points: Array<{ x: number; y: number }>): string {
  const compact = points.filter((point, index) => {
    const previous = points[index - 1];
    return !previous || Math.abs(previous.x - point.x) > 0.5 || Math.abs(previous.y - point.y) > 0.5;
  });
  if (compact.length < 2) return "";
  let path = `M ${compact[0].x},${compact[0].y}`;
  let last = compact[0];
  for (let index = 1; index < compact.length - 1; index += 1) {
    const previous = compact[index - 1];
    const current = compact[index];
    const next = compact[index + 1];
    const previousDistance = Math.hypot(current.x - previous.x, current.y - previous.y);
    const nextDistance = Math.hypot(next.x - current.x, next.y - current.y);
    const radius = Math.min(64, previousDistance * 0.42, nextDistance * 0.42);
    if (radius < 2) {
      path += ` L ${current.x},${current.y}`;
      last = current;
      continue;
    }
    const entry = {
      x: current.x + ((previous.x - current.x) / previousDistance) * radius,
      y: current.y + ((previous.y - current.y) / previousDistance) * radius,
    };
    const exit = {
      x: current.x + ((next.x - current.x) / nextDistance) * radius,
      y: current.y + ((next.y - current.y) / nextDistance) * radius,
    };
    if (Math.abs(entry.x - last.x) > 0.5 || Math.abs(entry.y - last.y) > 0.5) path += ` L ${entry.x},${entry.y}`;
    path += ` Q ${current.x},${current.y} ${exit.x},${exit.y}`;
    last = exit;
  }
  const end = compact[compact.length - 1];
  return `${path} L ${end.x},${end.y}`;
}

function pipelineCurvePath(sourceX: number, sourceY: number, targetX: number, targetY: number, targetPosition: Position, laneOffset: number, routeMode = ""): string {
  const forward = targetX >= sourceX;
  const horizontalDistance = Math.max(1, Math.abs(targetX - sourceX));
  const verticalDistance = Math.abs(targetY - sourceY);
  const handleOffset = Math.max(34, Math.min(118, horizontalDistance * 0.26));
  const scatter = Math.min(280, Math.abs(laneOffset));
  const directShortLine = forward && targetPosition === Position.Left && scatter < 4 && verticalDistance < 28 && horizontalDistance < 420;
  if (directShortLine) return `M ${sourceX},${sourceY} C ${sourceX + handleOffset},${sourceY} ${targetX - handleOffset},${targetY} ${targetX},${targetY}`;
  const directForwardLeft = forward && targetPosition === Position.Left && (routeMode === "direct-forward-left" || (horizontalDistance <= 260 && verticalDistance <= 210));
  if (directForwardLeft) {
    const bend = Math.max(42, Math.min(140, horizontalDistance * 0.48));
    const fanoutLift = Math.max(-28, Math.min(28, laneOffset * 0.18));
    return `M ${sourceX},${sourceY} C ${sourceX + bend},${sourceY + fanoutLift} ${targetX - bend},${targetY} ${targetX},${targetY}`;
  }

  if (forward) {
    const sourceRailX = sourceX + handleOffset;
    if (targetPosition === Position.Top || targetPosition === Position.Bottom) {
      const verticalSign = targetPosition === Position.Top ? -1 : 1;
      const railY = targetY + verticalSign * (54 + scatter * 0.42);
      return pipelineSmoothPath([
        { x: sourceX, y: sourceY },
        { x: sourceRailX, y: sourceY },
        { x: sourceRailX + Math.min(120, horizontalDistance * 0.18), y: railY },
        { x: targetX, y: railY },
        { x: targetX, y: targetY + verticalSign * 34 },
        { x: targetX, y: targetY },
      ]);
    }
    const targetRailX = targetX - handleOffset;
    const railY = (sourceY + targetY) / 2 + laneOffset;
    return pipelineSmoothPath([
      { x: sourceX, y: sourceY },
      { x: sourceRailX, y: sourceY },
      { x: sourceRailX + Math.min(110, horizontalDistance * 0.16), y: railY },
      { x: targetRailX - Math.min(90, horizontalDistance * 0.12), y: railY },
      { x: targetRailX, y: targetY },
      { x: targetX, y: targetY },
    ]);
  }

  const targetSign = targetPosition === Position.Bottom ? 1 : targetPosition === Position.Top ? -1 : (laneOffset >= 0 ? 1 : -1);
  const detourX = Math.max(sourceX, targetX) + 92 + Math.min(180, scatter * 0.52);
  const railY = targetSign < 0
    ? Math.min(sourceY, targetY) - 84 - scatter * 0.62
    : Math.max(sourceY, targetY) + 84 + scatter * 0.62;
  if (targetPosition === Position.Top || targetPosition === Position.Bottom) {
    return pipelineSmoothPath([
      { x: sourceX, y: sourceY },
      { x: sourceX + handleOffset, y: sourceY },
      { x: detourX, y: railY },
      { x: targetX, y: railY },
      { x: targetX, y: targetY + targetSign * 38 },
      { x: targetX, y: targetY },
    ]);
  }
  return pipelineSmoothPath([
    { x: sourceX, y: sourceY },
    { x: sourceX + handleOffset, y: sourceY },
    { x: detourX, y: railY },
    { x: targetX - handleOffset, y: railY },
    { x: targetX - handleOffset, y: targetY },
    { x: targetX, y: targetY },
  ]);
}

function PipelineFlowNode({ data }: AnyRecord) {
  return h("div", { className: "pipeline-flow-node-body" },
    pipelineInputPorts.map((port) => h(Handle, {
      key: port.id,
      id: port.id,
      type: "target",
      position: port.position,
      isConnectable: false,
      className: `pipeline-flow-handle input ${port.side} slot-${port.slot || "mid"}`,
      style: port.style,
    })),
    pipelineOutputPorts.map((port) => h(Handle, {
      key: port.id,
      id: port.id,
      type: "source",
      position: port.position,
      isConnectable: false,
      className: "pipeline-flow-handle output right",
      style: port.style,
    })),
    data?.label,
  );
}

function PipelineCurveEdge({ id, sourceX, sourceY, targetX, targetY, targetPosition, markerEnd, markerStart, style, data }: AnyRecord) {
  const laneOffset = Number(data?.laneOffset || 0);
  const path = pipelineCurvePath(sourceX, sourceY, targetX, targetY, targetPosition, laneOffset, String(data?.routeMode || ""));
  return h(BaseEdge, { id, path, markerEnd, markerStart, style, interactionWidth: 28 });
}

const pipelineEdgeTypes: any = { pipelineCurve: PipelineCurveEdge };
const pipelineNodeTypes: any = { pipelineNode: PipelineFlowNode };


function fmtClockValue(value: any): string {
  if (!value) return "--";
  const date = new Date(value);
  if (Number.isNaN(date.getTime())) return "--";
  return fmtClock(date);
}

function fmtDurationMs(value: any): string {
  const ms = Number(value);
  if (!Number.isFinite(ms) || ms < 0) return "--";
  const seconds = Math.round(ms / 1000);
  if (seconds < 60) return `${seconds}s`;
  if (seconds < 3600) return `${Math.floor(seconds / 60)}m ${seconds % 60}s`;
  return `${Math.floor(seconds / 3600)}h ${Math.floor((seconds % 3600) / 60)}m`;
}

function fmtCount(value: any): string {
  const number = Number(value);
  if (!Number.isFinite(number)) return "--";
  return number.toLocaleString("zh-CN");
}

function fmtPercent(value: any): string {
  const number = Number(value);
  if (!Number.isFinite(number)) return "--";
  return `${Math.round(Math.max(0, Math.min(1, number)) * 100)}%`;
}

function isRecord(value: any): value is AnyRecord {
  return typeof value === "object" && value !== null && !Array.isArray(value);
}

function asArray(value: any): any[] {
  return Array.isArray(value) ? value : [];
}

function timeMs(value: any): number | null {
  if (!value) return null;
  const date = new Date(value);
  return Number.isNaN(date.getTime()) ? null : date.getTime();
}

function isoFromMs(value: number | null | undefined): string {
  return Number.isFinite(Number(value)) ? new Date(Number(value)).toISOString() : "";
}

function firstIso(...values: any[]): string {
  for (const value of values) {
    const ms = timeMs(value);
    if (ms !== null) return new Date(ms).toISOString();
  }
  return "";
}

function latestIso(...values: any[]): string {
  const candidates = values.map(timeMs).filter((value): value is number => value !== null);
  return candidates.length > 0 ? new Date(Math.max(...candidates)).toISOString() : "";
}

function terminalStatus(status: any): boolean {
  return ["succeeded", "failed", "skipped", "cancelled", "canceled", "completed"].includes(String(status || "").toLowerCase());
}

function runningStatus(status: any): boolean {
  const value = statusValue(status).toLowerCase();
  return ["running", "active", "in-progress", "in_progress"].includes(value);
}

function statusCounts(items: any[], key = "status"): AnyRecord {
  return items.reduce((counts: AnyRecord, item: any) => {
    const status = String(item?.[key] || "unknown").toLowerCase();
    counts[status] = (counts[status] || 0) + 1;
    return counts;
  }, {});
}

function parseJsonLine(value: any): AnyRecord | null {
  if (!value || typeof value !== "string") return null;
  try {
    const parsed = JSON.parse(value) as unknown;
    return isRecord(parsed) ? parsed : null;
  } catch {
    return null;
  }
}

function tailSummary(lines: any[]): AnyRecord {
  const records = lines.map(parseJsonLine).filter((item): item is AnyRecord => Boolean(item));
  const timestamps = records.flatMap((record) => [record.timestamp, record.createdAt, record.updatedAt]).filter(Boolean);
  const lastAt = latestIso(...timestamps);
  const eventKinds = Array.from(new Set(records.map((record) => String(record.event || record.action || record.type || "")).filter(Boolean))).slice(0, 3);
  return {
    total: lines.length,
    parsed: records.length,
    lastAt,
    eventKinds,
  };
}

function summarizeValue(value: any): string {
  if (value === null || value === undefined) return "--";
  if (typeof value === "boolean") return value ? "是" : "否";
  if (typeof value === "number") return String(value);
  if (typeof value === "string") return value.length > 80 ? `${value.slice(0, 77)}...` : value;
  if (Array.isArray(value)) return `${value.length} 项`;
  if (typeof value === "object") return `${Object.keys(value).length} 字段`;
  return String(value);
}

function previewText(value: any, maxLength = 280): string {
  if (value === null || value === undefined) return "";
  const text = typeof value === "string" ? value : String(value);
  const normalized = text.replace(/\r\n/gu, "\n").trim();
  return normalized.length > maxLength ? `${normalized.slice(0, Math.max(0, maxLength - 1))}...` : normalized;
}

function statusValue(value: any): string {
  if (typeof value === "string") return value;
  if (isRecord(value)) return String(value.status || value.state || value.phase || "unknown");
  return "unknown";
}

function tokenFacts(tokens: any): string[] {
  if (!isRecord(tokens)) return [];
  return [
    typeof tokens.total === "number" ? `tokens ${tokens.total}` : "",
    typeof tokens.input === "number" ? `in ${tokens.input}` : "",
    typeof tokens.output === "number" ? `out ${tokens.output}` : "",
    typeof tokens.reasoning === "number" ? `reason ${tokens.reasoning}` : "",
    typeof tokens.cacheRead === "number" ? `cache read ${tokens.cacheRead}` : "",
    typeof tokens.cacheWrite === "number" ? `cache write ${tokens.cacheWrite}` : "",
  ].filter(Boolean);
}

function kvItems(items: any[]): any[] {
  return items.filter((item) => item && item.value !== undefined && item.value !== null && String(item.value) !== "");
}

function PipelineKvGrid({ items }: AnyRecord) {
  const safeItems = kvItems(asArray(items));
  return h("div", { className: "pipeline-kv-grid" }, safeItems.map((item: AnyRecord) => h("span", { key: item.label },
    h("b", null, item.label),
    h("span", null, item.value),
  )));
}

function PipelineChipRow({ items }: AnyRecord) {
  const safeItems = asArray(items).map((item) => String(item || "")).filter(Boolean);
  if (safeItems.length === 0) return null;
  return h("div", { className: "pipeline-chip-row" }, safeItems.map((item, index) => h("span", { key: `${index}-${item}` }, item)));
}

function findProcedureRun(details: any, interval: any): AnyRecord | null {
  const procedureRunId = String(interval?.procedureRunId || "");
  const procedures = asArray(details?.procedureRuns);
  return procedures.find((procedure: any) => String(procedureRunIdOf(procedure)) === procedureRunId) || procedures.at(-1) || null;
}

function findProcedureRunById(details: any, procedureRunId: string): AnyRecord | null {
  const targetId = String(procedureRunId || "");
  if (!targetId) return null;
  return asArray(details?.procedureRuns).find((procedure: any) => procedureRunIdOf(procedure) === targetId) || null;
}

function procedureAttemptCount(procedure: any): number {
  return asArray(procedure?.attempts).length;
}

function procedureTraceStepCount(procedure: any): number {
  return asArray(procedure?.attempts).reduce((sum: number, attempt: any) => sum + opencodeSteps(attempt).length, 0);
}

function opencodeSteps(attempt: any): AnyRecord[] {
  return asArray(attempt?.opencodeMessages?.steps).filter(isRecord);
}

function partStatusTone(part: any): string {
  const status = String(part?.status || "").toLowerCase();
  if (["error", "failed", "failure"].includes(status)) return "failed";
  if (["completed", "succeeded", "success"].includes(status)) return "succeeded";
  if (["running", "started", "in_progress"].includes(status)) return "running";
  return "unknown";
}

function pipelineStructuredSummary(value: any): AnyRecord {
  if (Array.isArray(value)) {
    const first = value[0];
    const keys = isRecord(first) ? Object.keys(first).slice(0, 4) : [];
    return {
      shape: "array",
      facts: [`${value.length} items`, ...keys.map((key) => `key ${key}`)],
      kv: [],
    };
  }
  if (isRecord(value)) {
    const entries = Object.entries(value);
    const primitiveEntries = entries.filter(([, item]) => item === null || ["string", "number", "boolean"].includes(typeof item)).slice(0, 6);
    return {
      shape: "object",
      facts: [`${entries.length} fields`, ...entries.slice(0, 4).map(([key]) => `key ${key}`)],
      kv: primitiveEntries.map(([key, item]) => ({ label: key, value: summarizeValue(item) })),
    };
  }
  return {
    shape: typeof value,
    facts: [summarizeValue(value)],
    kv: [],
  };
}

function readBalancedStructuredBlock(text: string, startIndex: number): AnyRecord | null {
  const opening = text[startIndex];
  if (opening !== "{" && opening !== "[") return null;
  const pairs: AnyRecord = { "{": "}", "[": "]" };
  const stack = [opening];
  let inString = false;
  let escaped = false;
  for (let index = startIndex + 1; index < text.length; index += 1) {
    const char = text[index];
    if (inString) {
      if (escaped) {
        escaped = false;
        continue;
      }
      if (char === "\\") {
        escaped = true;
        continue;
      }
      if (char === "\"") inString = false;
      continue;
    }
    if (char === "\"") {
      inString = true;
      continue;
    }
    if (char === "{" || char === "[") {
      stack.push(char);
      continue;
    }
    if (char === "}" || char === "]") {
      const expected = pairs[stack[stack.length - 1]];
      if (char !== expected) return null;
      stack.pop();
      if (stack.length === 0) return { raw: text.slice(startIndex, index + 1), end: index + 1 };
    }
  }
  return null;
}

function appendPipelineStructuredAwareText(blocks: AnyRecord[], text: string, defaultLabel = "structured payload"): void {
  const source = String(text || "").trim();
  if (!source) return;
  const standalonePattern = /(^|\n)(?=[\[{])/gu;
  let cursor = 0;
  while (cursor < source.length) {
    standalonePattern.lastIndex = cursor;
    const match = standalonePattern.exec(source);
    if (!match) {
      const remainder = source.slice(cursor).trim();
      if (remainder) blocks.push({ type: "text", text: remainder });
      return;
    }
    const start = match.index + match[1].length;
    const before = source.slice(cursor, start).trim();
    if (before) blocks.push({ type: "text", text: before });
    const parsedBlock = readBalancedStructuredBlock(source, start);
    if (!parsedBlock) {
      blocks.push({ type: "text", text: `${defaultLabel}: structured payload preview unavailable` });
      const nextBoundary = source.indexOf("\n\n", start);
      cursor = nextBoundary >= 0 ? nextBoundary + 2 : source.length;
      continue;
    }
    try {
      blocks.push({ type: "structured", label: defaultLabel, value: JSON.parse(parsedBlock.raw) });
    } catch {
      blocks.push({ type: "text", text: `${defaultLabel}: structured payload` });
    }
    cursor = parsedBlock.end;
    while (cursor < source.length && /\s/u.test(source[cursor])) cursor += 1;
  }
}

function pipelineStructuredTextBlocks(text: any): AnyRecord[] {
  const source = typeof text === "string" ? text.replace(/\r\n/gu, "\n").trim() : "";
  if (!source) return [];
  const blocks: AnyRecord[] = [];
  let cursor = 0;
  const headingPattern = /(^|\n)([^\n]{1,120}):\n(?=[\[{])/gu;
  while (cursor < source.length) {
    headingPattern.lastIndex = cursor;
    const match = headingPattern.exec(source);
    if (!match) {
      appendPipelineStructuredAwareText(blocks, source.slice(cursor));
      break;
    }
    const matchStart = match.index + match[1].length;
    appendPipelineStructuredAwareText(blocks, source.slice(cursor, matchStart));
    const label = String(match[2] || "").trim();
    const jsonStart = headingPattern.lastIndex;
    const parsedBlock = readBalancedStructuredBlock(source, jsonStart);
    if (!parsedBlock) {
      blocks.push({ type: "text", text: `${label}: structured payload preview unavailable` });
      const nextBoundary = source.indexOf("\n\n", jsonStart);
      cursor = nextBoundary >= 0 ? nextBoundary + 2 : source.length;
      continue;
    }
    try {
      const parsed = JSON.parse(parsedBlock.raw);
      blocks.push({ type: "structured", label, value: parsed });
      cursor = parsedBlock.end;
      while (cursor < source.length && /\s/u.test(source[cursor])) cursor += 1;
      continue;
    } catch {
      const fallbackChunk = `${label}: structured payload`;
      blocks.push({ type: "text", text: fallbackChunk });
      cursor = parsedBlock.end;
    }
  }
  return blocks;
}

function PipelineStructuredPayloadBlock({ label, value }: AnyRecord) {
  const summary = pipelineStructuredSummary(value);
  return h("section", { className: "pipeline-structured-payload" },
    h("div", { className: "pipeline-structured-payload-head" },
      h("b", null, label || "structured payload"),
      h("span", null, summary.shape),
    ),
    h(PipelineChipRow, { items: summary.facts }),
    summary.kv.length > 0 ? h(PipelineKvGrid, { items: summary.kv }) : null,
  );
}

function pipelineSessionFacts(steps: any[], sessionIds: string[]): string[] {
  const agents = uniqueStrings(steps.map((step: any) => step?.agent)).slice(0, 3);
  const models = uniqueStrings(steps.map((step: any) => step?.model)).slice(0, 3);
  const sessions = sessionIds.length <= 2
    ? sessionIds.map((sessionId) => `session ${sessionId}`)
    : [`sessions ${sessionIds.length}`, ...sessionIds.slice(0, 2).map((sessionId) => `session ${sessionId}`)];
  return [
    ...agents.map((agent) => `agent ${agent}`),
    ...models.map((model) => `model ${model}`),
    ...sessions,
  ];
}

function opencodeStepKey(step: any, fallbackIndex = 0): string {
  const key = String(step?.messageId || step?.index || "");
  return key || `step-${fallbackIndex}`;
}

function PipelineOpenCodeTrace({ steps, sessionIds, sessionFacts, matchedStepKey }: AnyRecord) {
  const safeSteps = asArray(steps);
  const matchedIndex = safeSteps.findIndex((step: any, index: number) => opencodeStepKey(step, index) === matchedStepKey);
  const matchedStep = matchedIndex >= 0 ? safeSteps[matchedIndex] : null;
  const stepTimes = safeSteps.flatMap((step: any) => [timeMs(step?.createdAt), timeMs(step?.completedAt)]).filter((value): value is number => value !== null);
  const startMs = stepTimes.length > 0 ? Math.min(...stepTimes) : null;
  const endMs = stepTimes.length > 0 ? Math.max(...stepTimes) : null;
  const durationMs = startMs !== null && endMs !== null ? Math.max(0, endMs - startMs) : null;
  const toolCount = safeSteps.reduce((sum: number, step: any) => sum + asArray(step?.parts).filter((part: any) => String(part?.type || "").toLowerCase() === "tool").length, 0);
  const textCount = safeSteps.reduce((sum: number, step: any) => sum + asArray(step?.parts).filter((part: any) => ["text", "reasoning"].includes(String(part?.type || "").toLowerCase())).length, 0);
  const failedToolCount = safeSteps.reduce((sum: number, step: any) => sum + asArray(step?.parts).filter((part: any) => String(part?.type || "").toLowerCase() === "tool" && partStatusTone(part) === "failed").length, 0);
  const traceFacts = [
    `${safeSteps.length} steps`,
    `${sessionIds.length} sessions`,
    `${textCount} messages`,
    `${toolCount} tools`,
    durationMs !== null ? `duration ${fmtDurationMs(durationMs)}` : "",
    failedToolCount > 0 ? `${failedToolCount} failed tools` : "",
  ].filter(Boolean);
  const matchedFacts = matchedStep ? [
    `Step ${matchedStep?.index ?? matchedIndex + 1}`,
    String(matchedStep?.role || "role --"),
    matchedStep?.model ? `model ${matchedStep.model}` : "",
    matchedStep?.finish ? `finish ${matchedStep.finish}` : "",
    matchedStep?.durationMs !== undefined && matchedStep?.durationMs !== null ? `duration ${fmtDurationMs(matchedStep.durationMs)}` : "",
  ].filter(Boolean) : [];

  return h("section", { className: "pipeline-trace-timeline", "data-testid": "pipeline-step-timeline" },
    h("div", { className: "pipeline-trace-head" },
      h("div", null,
        h("b", null, "OpenCode Trace"),
        h("span", null, "Trace 使用 Code Queue 统一样式展示完整 agent loop；Pipeline 旧 step/message/tool 卡片样式已废弃。"),
      ),
      h("div", { className: "pipeline-trace-session-head", "data-testid": "pipeline-step-timeline-session" },
        h("span", null, traceFacts.join(" / ") || "Trace"),
        sessionFacts.length > 0 ? h(PipelineChipRow, { items: sessionFacts }) : null,
      ),
    ),
    matchedStep ? h("div", { className: "pipeline-trace-focus", "data-testid": "pipeline-trace-matched-step" },
      h("span", { className: "codex-output-channel" }, "Matched"),
      h("strong", null, `Gantt selection -> ${matchedFacts.join(" / ")}`),
      h("time", null, `${fmtClockValue(matchedStep?.createdAt)} -> ${fmtClockValue(matchedStep?.completedAt)}`),
    ) : null,
    h(TraceView, {
      port: opencodeTracePort,
      input: safeSteps,
      className: "codex-transcript pipeline-trace",
      testId: "pipeline-opencode-step-trace",
      emptyText: "暂无 OpenCode Trace 输出",
      keepRecentToolCalls: 3,
    }),
  );
}

function structuredEventRecords(value: any): AnyRecord[] {
  return asArray(value).flatMap((item: any) => {
    if (isRecord(item)) return [item];
    const parsed = parseJsonLine(item);
    return parsed ? [parsed] : [];
  });
}

function eventKind(record: any): string {
  return String(record?.event || record?.action || record?.requestedAction || record?.type || "").toLowerCase();
}

function eventTimestampIso(record: any): string {
  return firstIso(record?.timestamp, record?.createdAt, record?.updatedAt, record?.startedAt, record?.finishedAt);
}

function eventTimestampMs(record: any): number | null {
  return timeMs(eventTimestampIso(record));
}

function attemptLabel(attempt: any): string {
  return String(attempt?.attempt || attempt?.id || "");
}

function uniqueStrings(values: any[]): string[] {
  const seen = new Set<string>();
  const ordered: string[] = [];
  for (const value of values) {
    const text = String(value || "");
    if (!text || seen.has(text)) continue;
    seen.add(text);
    ordered.push(text);
  }
  return ordered;
}

function sourceKindLabel(value: any): string {
  switch (String(value || "").toLowerCase()) {
    case "monitor": return "monitor";
    case "webui": return "webui";
    case "cli": return "cli";
    case "system": return "runner";
    default: return String(value || "--");
  }
}

function controlActionValue(record: any): string {
  return String(record?.requestedAction || record?.action || "").toLowerCase();
}

function controlActionLabel(record: any): string {
  switch (controlActionValue(record)) {
    case "guide": return "引导";
    case "modify": return "修改";
    case "approve": return "审核通过";
    case "restart": return "重启";
    case "redo": return "重做";
    default: return String(record?.requestedAction || record?.action || "控制");
  }
}

function eventLabel(record: any): string {
  switch (eventKind(record)) {
    case "initial-prompt-delivered": return "初始 prompt";
    case "append-prompt-delivered": return "追加 prompt";
    case "append-prompt-queued": return "追加 prompt 已排队";
    case "monitor-prompt-delivered": return "Monitor prompt";
    case "node-long-running-observation": return "长任务观察";
    case "node-finished": return "节点完成";
    case "oa-policy-downstream-evaluated": return "OA 下游策略";
    case "control-command-queued": return `${controlActionLabel(record)} 已发起`;
    case "control-command-applied": return `${controlActionLabel(record)} 已生效`;
    case "control-command-ignored": return `${controlActionLabel(record)} 已忽略`;
    default: return String(record?.event || record?.action || record?.requestedAction || "event");
  }
}

function eventPreview(record: any): string {
  return previewText(record?.promptPreview || record?.reasonPreview || record?.prompt || record?.reason || "", 240);
}

function eventTextBlocks(record: any): AnyRecord[] {
  const prompt = String(record?.prompt || "");
  const reason = String(record?.reason || record?.restartReason || "");
  const promptPreview = prompt ? "" : String(record?.promptPreview || "");
  const reasonPreview = reason ? "" : String(record?.reasonPreview || "");
  return [
    prompt || promptPreview ? { label: prompt ? "prompt" : "prompt preview", value: prompt || promptPreview } : null,
    reason || reasonPreview ? { label: reason ? "reason" : "reason preview", value: reason || reasonPreview } : null,
    asArray(record?.resetNodeIds).length > 0 ? { label: "reset nodes", value: asArray(record.resetNodeIds).join(", ") } : null,
    asArray(record?.runningResetNodeIds).length > 0 ? { label: "interrupted running nodes", value: asArray(record.runningResetNodeIds).join(", ") } : null,
    asArray(record?.interruptedProcedureRunIds).length > 0 ? { label: "interrupted procedures", value: asArray(record.interruptedProcedureRunIds).join(", ") } : null,
    record?.interruptedProcedureRunId ? { label: "interrupted procedure", value: String(record.interruptedProcedureRunId) } : null,
  ].filter(Boolean) as AnyRecord[];
}

function attemptWindow(attempt: any): AnyRecord {
  const steps = opencodeSteps(attempt);
  const started = steps.map((step: any) => timeMs(step?.createdAt)).filter((value): value is number => value !== null);
  const finished = steps.map((step: any) => timeMs(step?.completedAt) ?? timeMs(step?.createdAt)).filter((value): value is number => value !== null);
  const eventTimes = structuredEventRecords(attempt?.controlEventRecords).map((record) => eventTimestampMs(record)).filter((value): value is number => value !== null);
  const outputTimes = asArray(attempt?.assistantOutputs).map((item: any) => timeMs(item?.updatedAt)).filter((value): value is number => value !== null);
  const startMs = started[0] ?? eventTimes[0] ?? outputTimes[0] ?? null;
  const endMs = finished.at(-1) ?? eventTimes.at(-1) ?? outputTimes.at(-1) ?? startMs;
  return { startMs, endMs };
}

function nearestProcedureForNode(details: any, runId: string, nodeId: string, markerMs: number | null, procedureRunId = ""): AnyRecord | null {
  const procedures = asArray(details?.procedureRuns).filter((procedure: any) => inferProcedureNodeId(procedure, runId) === nodeId);
  if (procedures.length === 0) return null;
  if (procedureRunId) {
    const direct = procedures.find((procedure: any) => procedureRunIdOf(procedure) === procedureRunId);
    if (direct) return direct;
  }
  if (markerMs === null) return procedures.at(-1) || null;
  const containing = procedures.find((procedure: any) => {
    const startMs = timeMs(procedureStartIso(procedure, details));
    const endMs = timeMs(procedureEndIso(procedure, details)) ?? startMs;
    return startMs !== null && endMs !== null && markerMs >= startMs - 1000 && markerMs <= endMs + 1000;
  });
  if (containing) return containing;
  return procedures.slice().sort((left: any, right: any) => {
    const leftStart = timeMs(procedureStartIso(left, details)) ?? markerMs;
    const leftEnd = timeMs(procedureEndIso(left, details)) ?? leftStart;
    const rightStart = timeMs(procedureStartIso(right, details)) ?? markerMs;
    const rightEnd = timeMs(procedureEndIso(right, details)) ?? rightStart;
    const leftDistance = Math.min(Math.abs(leftStart - markerMs), Math.abs(leftEnd - markerMs));
    const rightDistance = Math.min(Math.abs(rightStart - markerMs), Math.abs(rightEnd - markerMs));
    return leftDistance - rightDistance;
  })[0] || null;
}

function nearestAttemptForMarker(procedure: any, marker: any): AnyRecord | null {
  const attempts = asArray(procedure?.attempts).filter(isRecord);
  if (attempts.length === 0) return null;
  const direct = String(marker?.attempt || "");
  if (direct) {
    const matched = attempts.find((attempt: any) => attemptLabel(attempt) === direct);
    if (matched) return matched;
  }
  const markerMs = Number.isFinite(Number(marker?.ms)) ? Number(marker.ms) : null;
  if (markerMs === null) return attempts.at(-1) || null;
  const containing = attempts.find((attempt: any) => {
    const window = attemptWindow(attempt);
    return Number.isFinite(window.startMs) && Number.isFinite(window.endMs) && markerMs >= Number(window.startMs) - 1000 && markerMs <= Number(window.endMs) + 1000;
  });
  if (containing) return containing;
  return attempts.slice().sort((left: any, right: any) => {
    const leftWindow = attemptWindow(left);
    const rightWindow = attemptWindow(right);
    const leftDistance = Math.min(Math.abs(Number(leftWindow.startMs ?? markerMs) - markerMs), Math.abs(Number(leftWindow.endMs ?? markerMs) - markerMs));
    const rightDistance = Math.min(Math.abs(Number(rightWindow.startMs ?? markerMs) - markerMs), Math.abs(Number(rightWindow.endMs ?? markerMs) - markerMs));
    return leftDistance - rightDistance;
  })[0] || attempts.at(-1) || null;
}

function nearestStepForMarker(attempt: any, markerMs: number | null): AnyRecord {
  const steps = opencodeSteps(attempt);
  if (steps.length === 0) return { step: null, stepIndex: -1, stepKey: "" };
  if (markerMs === null) {
    const step = steps[0];
    return { step, stepIndex: 0, stepKey: opencodeStepKey(step, 0) };
  }
  for (let index = 0; index < steps.length; index += 1) {
    const step = steps[index];
    const startMs = timeMs(step?.createdAt) ?? timeMs(step?.completedAt);
    const endMs = timeMs(step?.completedAt) ?? startMs;
    if (startMs !== null && endMs !== null && markerMs >= startMs - 1000 && markerMs <= endMs + 1000) {
      return { step, stepIndex: index, stepKey: opencodeStepKey(step, index) };
    }
  }
  const afterIndex = steps.findIndex((step: any) => {
    const startMs = timeMs(step?.createdAt) ?? timeMs(step?.completedAt);
    return startMs !== null && startMs >= markerMs;
  });
  if (afterIndex >= 0) {
    const step = steps[afterIndex];
    return { step, stepIndex: afterIndex, stepKey: opencodeStepKey(step, afterIndex) };
  }
  const fallbackIndex = Math.max(0, steps.length - 1);
  return { step: steps[fallbackIndex], stepIndex: fallbackIndex, stepKey: opencodeStepKey(steps[fallbackIndex], fallbackIndex) };
}

function pipelineSelectionContext(runDetails: any, selection: any): AnyRecord {
  const runId = String(selection?.runId || runDetails?.runId || "");
  if (String(selection?.mode || "") === "interval") {
    const interval = selection?.interval || {};
    const procedure = findProcedureRun(runDetails, interval) || interval.raw || {};
    return {
      mode: "interval",
      runId,
      interval,
      marker: null,
      nodeId: String(interval?.nodeId || inferProcedureNodeId(procedure, runId) || ""),
      procedure,
      attempt: null,
      matchedStep: null,
      matchedStepIndex: -1,
      matchedStepKey: "",
    };
  }
  const marker = isRecord(selection?.marker) ? selection.marker : {};
  const markerMs = Number.isFinite(Number(marker?.ms)) ? Number(marker.ms) : null;
  const nodeId = String(marker?.nodeId || "");
  const procedure = nodeId ? nearestProcedureForNode(runDetails, runId, nodeId, markerMs, String(marker?.procedureRunId || "")) : null;
  const attempt = procedure ? nearestAttemptForMarker(procedure, marker) : null;
  const matched = attempt ? nearestStepForMarker(attempt, markerMs) : { step: null, stepIndex: -1, stepKey: "" };
  return {
    mode: "event",
    runId,
    interval: null,
    marker,
    nodeId,
    procedure,
    attempt,
    matchedStep: matched.step,
    matchedStepIndex: matched.stepIndex,
    matchedStepKey: matched.stepKey,
  };
}

function PipelineProcedureAttemptList({ procedure, matchedStepKey = "", matchedAttemptId = "" }: AnyRecord) {
  const attempts = asArray(procedure?.attempts);
  if (attempts.length === 0) return h(EmptyState, { title: "暂无 attempt 详情", text: "当前 procedure 还没有可展示的 attempt / OpenCode Trace；若刚点击甘特线，请等待 node 详情抓取完成。" });
  return attempts.map((attempt: any, attemptIndex: number) => {
    const messages = attempt?.opencodeMessages || {};
    const steps = opencodeSteps(attempt);
    const sessionIds = asArray(messages.sessionIds).map((item) => String(item)).filter(Boolean);
    const sessionFacts = pipelineSessionFacts(steps, sessionIds);
    const currentAttemptId = attemptLabel(attempt) || `attempt-${attemptIndex + 1}`;
    const failedToolCount = steps.reduce((sum, step: any) => sum + asArray(step?.parts).filter((part: any) =>
      String(part?.type || "").toLowerCase() === "tool" && partStatusTone(part) === "failed").length, 0);
    return h("article", { key: currentAttemptId, className: `pipeline-attempt-card ${matchedAttemptId === currentAttemptId ? "matched" : ""}` },
      h("div", { className: "pipeline-attempt-head" },
        h("div", null,
          h("strong", null, currentAttemptId),
          h("span", null, messages.source || "opencode"),
        ),
        h("div", { className: "pipeline-attempt-badges" },
          h("span", null, `${steps.length} steps`),
          h("span", null, `${messages.toolCallCount ?? "--"} tools`),
          failedToolCount > 0 ? h("span", { className: "danger" }, `${failedToolCount} failed`) : null,
        ),
      ),
      h(PipelineKvGrid, { items: [
        { label: "messages", value: messages.messageCount ?? "--" },
        { label: "steps", value: messages.stepCount ?? "--" },
        { label: "tools", value: messages.toolCallCount ?? "--" },
        { label: "updated", value: fmtDate(messages.updatedAt) },
        { label: "sessions", value: sessionIds.join(", ") || "--" },
      ] }),
      steps.length === 0 ? h("p", { className: "muted paragraph" }, "当前 attempt 尚未返回 OpenCode Trace；请确认 D601 pipeline-control 已重建并重新抓取。") :
        h(PipelineOpenCodeTrace, { steps, sessionIds, sessionFacts, matchedStepKey }),
    );
  });
}

function pipelineNodeDetailKey(runId: string, nodeId: string): string {
  return `${runId}::${nodeId}`;
}

function pipelineFocusedNodeDetails(nodeDetails: any, runId: string, nodeId: string): AnyRecord | null {
  if (!isRecord(nodeDetails)) return null;
  return String(nodeDetails.runId || "") === runId && String(nodeDetails.nodeId || "") === nodeId ? nodeDetails : null;
}

function mergeFocusedProcedure(baseProcedure: any, focusedProcedure: any): AnyRecord {
  const base = isRecord(baseProcedure) ? baseProcedure : {};
  if (!isRecord(focusedProcedure)) return base;
  const focusedAttempts = asArray(focusedProcedure.attempts);
  const baseAttempts = asArray(base.attempts);
  return {
    ...base,
    ...focusedProcedure,
    attempts: focusedAttempts.length > 0 ? focusedAttempts : baseAttempts,
  };
}

function pipelineGanttDetailPayload(baseDetails: any, nodeDetails: any, runId: string, nodeId: string): any {
  if (!pipelineFocusedNodeDetails(nodeDetails, runId, nodeId)) return baseDetails;
  const nodeProcedures = asArray(nodeDetails.procedureRuns);
  const base = isRecord(baseDetails) ? baseDetails : {};
  return {
    ...base,
    ...nodeDetails,
    // Keep normalized OA control events unless the node endpoint has a focused copy.
    controlEvents: asArray(nodeDetails.controlEvents).length > 0 ? nodeDetails.controlEvents : base.controlEvents,
    procedureRuns: nodeProcedures.length > 0 ? nodeProcedures : base.procedureRuns,
  };
}

function PipelineGanttDetailPanel({ selection, runDetails, nodeDetails, nodeDetailsState, onRaw, onCollapse }: AnyRecord) {
  if (!selection?.mode) {
    return h("aside", { className: "pipeline-gantt-detail-panel empty", "data-testid": "pipeline-gantt-detail-panel" },
      h("div", { className: "pipeline-gantt-detail-head" },
        h("div", null,
          h("span", { className: "panel-eyebrow" }, "Gantt Detail"),
          h(LoadingTitle, { title: "未选择元素", level: 3 }),
        ),
        h("button", { type: "button", className: "ghost-btn mini", onClick: onCollapse, "data-testid": "pipeline-gantt-sidebar-collapse" }, "收起"),
      ),
      h(EmptyState, { title: "选择一条执行线或一个控制点", text: "点击甘特图中的 node 执行线、prompt 点或控制点，在这里查看结构化过程和 OpenCode step。" }),
    );
  }
  const runId = String(selection?.runId || "");
  const selectedNodeId = String(selection?.interval?.nodeId || selection?.marker?.nodeId || "");
  const baseDetails = runDetails?.runId === runId ? runDetails.details : null;
  const focusedNodeDetails = pipelineFocusedNodeDetails(nodeDetails, runId, selectedNodeId);
  const nodeStateMatches = String(nodeDetailsState?.runId || "") === runId && String(nodeDetailsState?.nodeId || "") === selectedNodeId;
  const details = pipelineGanttDetailPayload(baseDetails, focusedNodeDetails, runId, selectedNodeId);
  const epochLoading = (String(runDetails?.runId || "") !== runId || Boolean(runDetails?.loading)) && !details;
  const epochError = String(runDetails?.runId || "") === runId ? String(runDetails?.error || "") : "";
  const nodeError = nodeStateMatches ? String(nodeDetailsState?.error || "") : "";
  const context = details ? pipelineSelectionContext(details, selection) : null;
  const interval = context?.interval || selection?.interval || null;
  const marker = context?.marker || selection?.marker || null;
  const selectedProcedureRunId = String(interval?.procedureRunId || marker?.procedureRunId || "");
  const focusedProcedure = focusedNodeDetails
    ? findProcedureRunById(focusedNodeDetails, selectedProcedureRunId) || findProcedureRun(focusedNodeDetails, interval || { procedureRunId: selectedProcedureRunId })
    : null;
  let procedure = context?.procedure || (details ? findProcedureRun(details, interval || { procedureRunId: selectedProcedureRunId }) : null) || interval?.raw || {};
  if (focusedProcedure && (procedureAttemptCount(procedure) === 0 || procedureTraceStepCount(focusedProcedure) >= procedureTraceStepCount(procedure))) {
    procedure = mergeFocusedProcedure(procedure, focusedProcedure);
  }
  let matchedAttempt = context?.attempt || null;
  let matchedStepKey = String(context?.matchedStepKey || "");
  if (!matchedAttempt && marker && procedureAttemptCount(procedure) > 0) {
    matchedAttempt = nearestAttemptForMarker(procedure, marker);
    matchedStepKey = String(nearestStepForMarker(matchedAttempt, Number.isFinite(Number(marker?.ms)) ? Number(marker.ms) : null).stepKey || "");
  }
  const matchedAttemptId = attemptLabel(matchedAttempt);
  const hasProcedureAttempts = procedureAttemptCount(procedure) > 0;
  const nodeLoading = nodeStateMatches && Boolean(nodeDetailsState?.loading) && !hasProcedureAttempts;
  const loading = Boolean(epochLoading || nodeLoading);
  const error = [hasProcedureAttempts ? "" : epochError, nodeError].filter(Boolean).join(" / ");
  const fetchedAt = nodeStateMatches && nodeDetailsState?.fetchedAt ? nodeDetailsState.fetchedAt : runDetails?.fetchedAt;
  const status = statusValue(procedure?.status || interval?.status || marker?.status || marker?.event);
  const detailTitle = selection?.mode === "event" ? (marker?.label || eventLabel(marker?.raw || marker) || "event") : (context?.nodeId || interval?.nodeId || "node");
  const markerBlocks = marker ? eventTextBlocks(marker?.raw || marker) : [];
  const eventFacts = marker ? [
    eventKind(marker?.raw || marker) ? `event ${eventKind(marker?.raw || marker)}` : "",
    marker?.promptEvent ? `prompt ${marker.promptEvent}` : "",
    marker?.action ? `action ${marker.action}` : "",
    marker?.sourceKind ? `source ${sourceKindLabel(marker.sourceKind)}` : "",
    marker?.sourceNodeId ? `from ${marker.sourceNodeId}` : "",
    marker?.targetNodeId ? `to ${marker.targetNodeId}` : "",
    marker?.snapReason ? `draw ${marker.snapReason}` : "",
  ].filter(Boolean) : [];
  return h("aside", { className: "pipeline-gantt-detail-panel", "data-testid": "pipeline-gantt-detail-panel" },
    h("div", { className: "pipeline-gantt-detail-head" },
      h("div", null,
        h("span", { className: "panel-eyebrow" }, selection?.mode === "event" ? "Gantt Event Detail" : "Gantt Line Detail"),
        h(LoadingTitle, { title: detailTitle, level: 3, loading }),
      ),
      h("div", { className: "pipeline-gantt-detail-head-actions" },
        h(StatusBadge, { status }, status),
        h("button", { type: "button", className: "ghost-btn mini", onClick: onCollapse, "data-testid": "pipeline-gantt-sidebar-collapse" }, "收起"),
      ),
    ),
    marker ? h("article", { className: "pipeline-event-card" },
      h("div", { className: "pipeline-event-card-head" },
        h("strong", null, marker?.label || eventLabel(marker?.raw || marker)),
        h(PipelineChipRow, { items: eventFacts }),
      ),
      h(PipelineKvGrid, { items: [
        { label: "event time", value: fmtDate(marker?.timestampIso || marker?.timestamp || "--") },
        marker?.snapped ? { label: "drawn time", value: fmtDate(marker?.renderedTimestampIso || marker?.ms) } : null,
        { label: "node", value: marker?.nodeId || "--" },
        { label: "procedure", value: marker?.procedureRunId || procedureRunIdOf(procedure) || "--" },
        { label: "attempt", value: marker?.attempt || matchedAttemptId || "--" },
        { label: "source kind", value: marker?.sourceKind ? sourceKindLabel(marker.sourceKind) : "--" },
        { label: "source node", value: marker?.sourceNodeId || "--" },
        { label: "target node", value: marker?.targetNodeId || "--" },
        { label: "command", value: marker?.commandId || marker?.eventId || "--" },
        marker?.snapReason ? { label: "placement", value: marker.snapReason } : null,
      ] }),
      markerBlocks.length > 0 ? h("div", { className: "pipeline-event-blocks" },
        markerBlocks.map((block: AnyRecord, index: number) => h("section", { key: `${block.label}-${index}`, className: "pipeline-event-text-block" },
          h("b", null, block.label),
          h("p", null, block.value),
        )),
      ) : null,
      eventPreview(marker?.raw || marker) ? h("p", { className: "pipeline-text-preview" }, eventPreview(marker?.raw || marker)) : null,
    ) : null,
    h(PipelineKvGrid, { items: [
      { label: "epoch", value: runId || interval?.runId || "--" },
      { label: "node", value: context?.nodeId || interval?.nodeId || marker?.nodeId || "--" },
      { label: "procedure", value: interval?.procedureRunId || marker?.procedureRunId || procedureRunIdOf(procedure) || "--" },
      { label: "started", value: fmtDate(interval?.startedAt || procedure?.startedAt) },
      { label: "finished", value: fmtDate(interval?.finishedAt || procedure?.finishedAt) },
      { label: "duration", value: fmtDurationMs(interval?.durationMs || procedure?.durationMs) },
      { label: "fetched", value: fetchedAt ? fmtClock(fetchedAt) : "--" },
      context?.matchedStep ? { label: "matched step", value: `Step ${context.matchedStep.index ?? context.matchedStepIndex + 1}` } : null,
    ] }),
    h(UniDeskErrorBanner, { error }),
    h("div", { className: "pipeline-gantt-detail-actions" },
      h(RawButton, { title: `Procedure ${interval?.procedureRunId || marker?.procedureRunId || context?.nodeId || "node"}`, data: procedure, onOpen: onRaw, testId: "raw-pipeline-gantt-procedure" }),
      marker ? h(RawButton, { title: `Pipeline event ${marker?.id || marker?.commandId || marker?.eventId || context?.nodeId || "event"}`, data: marker?.raw || marker, onOpen: onRaw, testId: "raw-pipeline-gantt-event" }) : null,
      details ? h(RawButton, { title: `Pipeline run ${runId || "--"}`, data: details, onOpen: onRaw, testId: "raw-pipeline-gantt-node-details" }) : null,
    ),
    !loading && !procedureRunIdOf(procedure) && !marker ? h(EmptyState, { title: "暂无过程详情", text: "当前选择还没有可匹配的 procedure 运行记录。" }) : null,
    !loading && procedureRunIdOf(procedure) ? h(PipelineProcedureAttemptList, { procedure, matchedStepKey, matchedAttemptId }) : null,
  );
}

function GanttHeaderLabel({ value }: AnyRecord) {
  const text = String(value || "--");
  const pieces = text.split(/([_-])/u);
  return h(React.Fragment, null, pieces.map((piece, index) => (
    piece === "-" || piece === "_" ? h(React.Fragment, { key: index }, piece, h("wbr", null)) : h(React.Fragment, { key: index }, piece)
  )));
}

async function requestJson(path: string, options: AnyRecord = {}): Promise<any> {
  return requestUniDeskJson(path, {
    invalidJsonPrefix: "Pipeline 返回了无效 JSON",
    ...options,
  });
}

function StatusBadge({ status, children }: AnyRecord) {
  const normalized = String(status || "unknown").toLowerCase();
  return h("span", { className: `status-badge ${normalized}` }, children || status || "unknown");
}

function MetricCard({ label, value, hint, tone }: AnyRecord) {
  return h("article", { className: `metric-card ${tone || ""}` },
    h("div", { className: "metric-label" }, label),
    h("div", { className: "metric-value" }, value),
    h("div", { className: "metric-hint" }, hint),
  );
}

function Panel({ title, eyebrow, actions, children, className, loading }: AnyRecord) {
  return h("section", { className: `panel ${className || ""}` },
    h("div", { className: "panel-head" },
      h("div", null,
        eyebrow ? h("p", { className: "panel-eyebrow" }, eyebrow) : null,
        h(LoadingTitle, { title, loading }),
      ),
      actions ? h("div", { className: "panel-actions" }, actions) : null,
    ),
    h("div", { className: "panel-body" }, children),
  );
}

function RawButton({ title, data, onOpen, testId }: AnyRecord) {
  return h("button", {
    type: "button",
    className: "ghost-btn",
    "data-testid": testId,
    onClick: () => onOpen(title, data),
  }, "查看原始JSON");
}

function EvidenceIndexRow({ title, subtitle, facts, data, onRaw, testId }: AnyRecord) {
  const safeFacts = asArray(facts).map((item) => String(item || "")).filter(Boolean);
  return h("article", { className: "pipeline-evidence-row" },
    h("div", { className: "pipeline-evidence-main" },
      h("strong", null, title),
      subtitle ? h("span", null, subtitle) : null,
    ),
    h("div", { className: "pipeline-evidence-facts" }, safeFacts.map((fact, index) => h("span", { key: `${index}-${fact.slice(0, 16)}` }, fact))),
    data !== undefined ? h(RawButton, { title, data, onOpen: onRaw, testId }) : null,
  );
}

function EmptyState({ title, text }: AnyRecord) {
  return h("div", { className: "empty-state" }, h("strong", null, title), h("span", null, text));
}

function microserviceRuntime(service: any): AnyRecord {
  return service?.runtime && typeof service.runtime === "object" && !Array.isArray(service.runtime) ? service.runtime : {};
}

function microserviceBackend(service: any): AnyRecord {
  return service?.backend && typeof service.backend === "object" && !Array.isArray(service.backend) ? service.backend : {};
}

function microserviceRepository(service: any): AnyRecord {
  return service?.repository && typeof service.repository === "object" && !Array.isArray(service.repository) ? service.repository : {};
}

function pipelineSnapshotArrays(snapshot: any): { components: any[]; pipelines: any[]; runs: any[] } {
  return {
    components: Array.isArray(snapshot?.registry?.components) ? snapshot.registry.components : [],
    pipelines: Array.isArray(snapshot?.pipelines) ? snapshot.pipelines : [],
    runs: Array.isArray(snapshot?.runs) ? snapshot.runs : [],
  };
}

function pipelineArrayCount(snapshot: any, path: string, fallback: number): number {
  const meta = snapshot?._unidesk?.arrayLimits?.[path];
  const original = Number(meta?.originalLength);
  return Number.isFinite(original) ? original : fallback;
}

function pipelineComponentRef(value: any): string {
  if (!value || typeof value !== "object" || Array.isArray(value)) return "--";
  return `${value.componentClass || "--"}/${value.id || "--"}`;
}

function pipelineComponentRefKey(value: any): string {
  if (!value || typeof value !== "object" || Array.isArray(value)) return "";
  const componentClass = String(value.componentClass || "").trim();
  const id = String(value.id || "").trim();
  return componentClass && id ? `${componentClass}/${id}` : "";
}

function pipelineConfigObject(pipeline: any): AnyRecord {
  return pipeline?.config && typeof pipeline.config === "object" && !Array.isArray(pipeline.config) ? pipeline.config : {};
}

function pipelineConfigNodes(pipeline: any): AnyRecord[] {
  const config = pipelineConfigObject(pipeline);
  const rawNodes = Array.isArray(config.nodes) ? config.nodes : Array.isArray(pipeline?.nodes) ? pipeline.nodes : [];
  const nodeById: Map<string, AnyRecord> = new Map();
  for (const node of rawNodes) {
    const id = String(node?.id || node?.nodeId || "");
    if (id) nodeById.set(id, { ...node, id });
  }
  const edges = pipelineConfigEdges(pipeline);
  const addMissing = (id: string) => {
    if (id && !nodeById.has(id)) nodeById.set(id, { id });
  };
  for (const batch of pipelineGraphBatches(pipeline)) pipelineRawNodeIds(batch).forEach(addMissing);
  for (const edge of edges) {
    addMissing(String(edge?.from || edge?.source || ""));
    addMissing(String(edge?.to || edge?.target || ""));
  }
  return Array.from(nodeById.values());
}

function pipelineConfigEdges(pipeline: any): AnyRecord[] {
  const config = pipelineConfigObject(pipeline);
  return Array.isArray(config.edges) ? config.edges : Array.isArray(pipeline?.edges) ? pipeline.edges : [];
}

function pipelineGraphBatches(pipeline: any): any[] {
  const config = pipelineConfigObject(pipeline);
  return Array.isArray(config.topologicalBatches) ? config.topologicalBatches : Array.isArray(pipeline?.topologicalBatches) ? pipeline.topologicalBatches : [];
}

function pipelineComponentLookup(components: any[]): Map<string, AnyRecord> {
  const byRef: Map<string, AnyRecord> = new Map();
  for (const component of components) {
    const directKey = pipelineComponentRefKey(component);
    if (directKey) byRef.set(directKey, component);
    const refs = Array.isArray(component?.refs) ? component.refs : [];
    for (const ref of refs) {
      const refKey = pipelineComponentRefKey(ref);
      if (refKey) byRef.set(refKey, component);
    }
  }
  return byRef;
}

function pipelineNodeComponent(node: any, componentByRef: Map<string, AnyRecord>): AnyRecord | null {
  const explicit = componentByRef.get(pipelineComponentRefKey(node?.componentRef));
  if (explicit) return explicit;
  const fallbackKey = pipelineComponentRefKey({ componentClass: node?.kind, id: node?.id });
  return fallbackKey ? componentByRef.get(fallbackKey) || null : null;
}

function pipelineRunNodeStatus(run: any, nodeId: string): string {
  const node = pipelineRunNodeRecord(run, nodeId);
  return String(node?.status || "pending");
}

function pipelineRunNodeRecord(run: any, nodeId: string): AnyRecord | null {
  const nodes = Array.isArray(run?.nodes) ? run.nodes : [];
  return nodes.find((item: any) => item?.nodeId === nodeId || item?.id === nodeId) || null;
}

function pipelineStatusCounts(runs: any[]): AnyRecord {
  return runs.reduce((counts: AnyRecord, run: any) => {
    const status = String(run?.status || "unknown").toLowerCase();
    counts[status] = (counts[status] || 0) + 1;
    return counts;
  }, {});
}

function pipelineRunScorers(run: any): AnyRecord[] {
  if (Array.isArray(run?.scorers)) return run.scorers.filter(isRecord);
  if (Array.isArray(run?.summary?.scorers)) return run.summary.scorers.filter(isRecord);
  if (Array.isArray(run?.artifact?.summary?.scorers)) return run.artifact.summary.scorers.filter(isRecord);
  return [];
}

function pipelineDetailedRun(details: any): AnyRecord | null {
  if (isRecord(details?.run)) return details.run;
  if (isRecord(details?.runSummary)) return details.runSummary;
  return null;
}

function mergePipelineRunSummary(base: any, detail: any): AnyRecord | null {
  if (!isRecord(base) && !isRecord(detail)) return null;
  if (!isRecord(base)) return detail;
  if (!isRecord(detail)) return base;
  return {
    ...base,
    ...detail,
    request: isRecord(base.request) || isRecord(detail.request) ? { ...(isRecord(base.request) ? base.request : {}), ...(isRecord(detail.request) ? detail.request : {}) } : detail.request ?? base.request,
    artifact: isRecord(base.artifact) || isRecord(detail.artifact) ? { ...(isRecord(base.artifact) ? base.artifact : {}), ...(isRecord(detail.artifact) ? detail.artifact : {}) } : detail.artifact ?? base.artifact,
    summary: isRecord(base.summary) || isRecord(detail.summary) ? { ...(isRecord(base.summary) ? base.summary : {}), ...(isRecord(detail.summary) ? detail.summary : {}) } : detail.summary ?? base.summary,
  };
}

function pipelineScoreSummary(run: any): AnyRecord {
  const scorers = pipelineRunScorers(run);
  const primary = scorers.find((scorer) => isRecord(scorer?.score)) || scorers[0] || null;
  const score = isRecord(primary?.score) ? primary.score : {};
  const passed = Number(score.passed);
  const total = Number(score.total);
  const ratio = Number(score.ratio);
  const computedRatio = Number.isFinite(ratio)
    ? ratio
    : Number.isFinite(passed) && Number.isFinite(total) && total > 0 ? passed / total : null;
  const percent = computedRatio === null ? null : Math.round(Math.max(0, Math.min(100, computedRatio <= 1 ? computedRatio * 100 : computedRatio)));
  const text = String(score.text || (Number.isFinite(passed) && Number.isFinite(total) ? `${passed}/${total}` : ""));
  return {
    scorer: primary,
    scorers,
    score,
    passed: Number.isFinite(passed) ? passed : null,
    total: Number.isFinite(total) ? total : null,
    percent,
    text,
  };
}

function pipelineScoreText(run: any): string {
  const summary = pipelineScoreSummary(run);
  return summary.text || (summary.scorers.length > 0 ? String(summary.scorer?.status || "pending") : "--");
}

function pipelineScoreTone(run: any): string {
  const summary = pipelineScoreSummary(run);
  if (summary.total > 0 && summary.passed === summary.total) return "succeeded";
  if (summary.total > 0 && summary.passed > 0) return "running";
  if (summary.scorers.length > 0) return "failed";
  return "pending";
}

function pipelineScorerItems(scorer: any): AnyRecord[] {
  return Array.isArray(scorer?.items) ? scorer.items.filter(isRecord) : [];
}

function PipelineScoreBadge({ run }: AnyRecord) {
  const text = pipelineScoreText(run);
  return h("span", { className: `pipeline-score-badge ${pipelineScoreTone(run)}` }, `score ${text}`);
}

function PipelineScoreBoard({ run, onRaw }: AnyRecord) {
  const summary = pipelineScoreSummary(run);
  const scorers = summary.scorers;
  if (!run) return h(EmptyState, { title: "暂无评分", text: "选择一个 epoch 后会显示 scorer 结果。" });
  if (scorers.length === 0) return h("div", { className: "pipeline-score-empty" },
    h("strong", null, "评分器等待中"),
    h("span", null, "DAG 完成后，Pipeline control backend 会把 scorer summary 追加到 run artifact，并通过 UniDesk 显示。"),
  );
  return h("div", { className: "pipeline-score-board", "data-testid": "pipeline-score-board" },
    scorers.map((scorer: AnyRecord, index: number) => {
      const localSummary = pipelineScoreSummary({ scorers: [scorer] });
      const items = pipelineScorerItems(scorer);
      const percent = localSummary.percent ?? 0;
      return h("article", { key: `${scorer.scorerId || scorer.component || index}`, className: `pipeline-score-card ${pipelineScoreTone({ scorers: [scorer] })}` },
        h("div", { className: "pipeline-score-head" },
          h("div", null,
            h("span", null, scorer.scorerId || scorer.component || "scorer"),
            h("strong", null, localSummary.text || scorer.status || "--"),
          ),
          h(StatusBadge, { status: scorer.status || "unknown" }, scorer.status || "unknown"),
        ),
        h("div", { className: "pipeline-score-meter", "aria-label": `score ${percent}%` }, h("span", { style: { width: `${percent}%` } })),
        h("div", { className: "pipeline-score-facts" },
          h("span", null, `${percent}%`),
          h("span", null, scorer.component || "--"),
          h("span", null, scorer.applicationCheckoutRef || "--"),
        ),
        items.length > 0 ? h("div", { className: "pipeline-score-items" }, items.map((item: AnyRecord) => h("span", {
          key: `${item.id || item.filter}`,
          className: `pipeline-score-item ${String(item.status || "").toLowerCase()}`,
          title: `${item.filter || "--"} / ran=${item.ran ?? "?"}`,
        }, h("b", null, item.id || "--"), h("small", null, item.status || "--")))) : h("p", { className: "muted paragraph" }, "当前 scorer 尚未返回 item 级结果。"),
        scorer.error ? h("p", { className: "pipeline-score-error" }, previewText(scorer.error, 360)) : null,
        h("div", { className: "panel-actions inline-actions" }, h(RawButton, { title: `Scorer ${scorer.scorerId || index}`, data: scorer, onOpen: onRaw, testId: "raw-pipeline-score" })),
      );
    }),
  );
}

function pipelineComponentClassCounts(components: any[]): Array<{ name: string; count: number }> {
  const counts = components.reduce((memo: AnyRecord, component: any) => {
    const name = String(component?.componentClass || "unknown");
    memo[name] = (memo[name] || 0) + 1;
    return memo;
  }, {});
  return Object.entries(counts).map(([name, count]) => ({ name, count: Number(count) })).sort((left, right) => right.count - left.count || left.name.localeCompare(right.name));
}

function pipelineRawNodeIds(value: any): string[] {
  if (Array.isArray(value)) return value.map((item) => typeof item === "string" ? item : String(item?.id || item?.nodeId || "")).filter(Boolean);
  if (Array.isArray(value?.nodes)) return pipelineRawNodeIds(value.nodes);
  if (Array.isArray(value?.nodeIds)) return pipelineRawNodeIds(value.nodeIds);
  return [];
}

function pipelineMonitorInputs(node: any): AnyRecord {
  return isRecord(node?.instanceInputs?.monitor) ? node.instanceInputs.monitor : {};
}

function pipelineNodeIsMonitor(node: any, component?: any): boolean {
  if (String(node?.kind || "").toLowerCase() !== "procedure") return false;
  const monitorInputs = pipelineMonitorInputs(node);
  if (node?.instanceInputs?.monitorMode === true || monitorInputs.enabled === true) return true;
  const componentRef = pipelineComponentRef(node?.componentRef);
  return String(component?.id || component?.config?.id || componentRef || "").toLowerCase().includes("monitor");
}

function pipelineMonitorNodeIds(pipelineNodes: AnyRecord[]): string[] {
  return pipelineNodes
    .filter((node: any) => pipelineNodeIsMonitor(node))
    .map((node: any) => String(node?.id || ""))
    .filter(Boolean);
}

function pipelineOrderWithLeadingMonitors(nodeIds: string[], monitorNodeIds: string[]): string[] {
  if (monitorNodeIds.length === 0) return nodeIds;
  const monitorSet = new Set(monitorNodeIds);
  const leading = monitorNodeIds.filter((nodeId) => nodeIds.includes(nodeId));
  if (leading.length === 0) return nodeIds;
  return [...leading, ...nodeIds.filter((nodeId) => !monitorSet.has(nodeId))];
}

function pipelineColumnsWithLeadingMonitors(columns: string[][], monitorNodeIds: string[]): string[][] {
  if (monitorNodeIds.length === 0) return columns;
  const monitorSet = new Set(monitorNodeIds);
  const leading = monitorNodeIds.filter((nodeId) => columns.some((column) => column.includes(nodeId)));
  if (leading.length === 0) return columns;
  const remaining = columns
    .map((column) => column.filter((nodeId) => !monitorSet.has(nodeId)))
    .filter((column) => column.length > 0);
  return [leading, ...remaining];
}

function pipelineGraphColumns(pipeline: any, pipelineNodes: AnyRecord[], pipelineEdges: AnyRecord[]): string[][] {
  const rawBatches = pipelineGraphBatches(pipeline);
  const explicit = rawBatches.map(pipelineRawNodeIds).filter((batch: string[]) => batch.length > 0);
  if (explicit.length > 0) return explicit;

  const ids: string[] = pipelineNodes.map((node: any) => String(node?.id || "")).filter(Boolean);
  const idSet = new Set(ids);
  const incoming: Map<string, number> = new Map(ids.map((id) => [id, 0]));
  const outgoing: Map<string, string[]> = new Map(ids.map((id) => [id, []]));
  for (const edge of pipelineEdges) {
    const from = String(edge?.from || edge?.source || "");
    const to = String(edge?.to || edge?.target || "");
    if (!idSet.has(from) || !idSet.has(to)) continue;
    outgoing.get(from)?.push(to);
    incoming.set(to, (incoming.get(to) || 0) + 1);
  }
  const levels = new Map<string, number>();
  const queue = ids.filter((id) => (incoming.get(id) || 0) === 0);
  for (const id of queue) levels.set(id, 0);
  while (queue.length > 0) {
    const current = queue.shift()!;
    const nextLevel = (levels.get(current) || 0) + 1;
    for (const next of outgoing.get(current) || []) {
      incoming.set(next, Math.max(0, (incoming.get(next) || 0) - 1));
      levels.set(next, Math.max(levels.get(next) || 0, nextLevel));
      if ((incoming.get(next) || 0) === 0) queue.push(next);
    }
  }
  ids.forEach((id) => { if (!levels.has(id)) levels.set(id, 0); });
  const maxLevel = Math.max(0, ...Array.from(levels.values()));
  return Array.from({ length: maxLevel + 1 }, (_, level) => ids.filter((id) => levels.get(id) === level)).filter((batch) => batch.length > 0);
}

function pipelineGraphNodeOrder(pipeline: any, pipelineNodes: AnyRecord[], pipelineEdges: AnyRecord[]): string[] {
  const rawBatches = pipelineGraphBatches(pipeline);
  const explicit = rawBatches.map(pipelineRawNodeIds).filter((batch: string[]) => batch.length > 0);
  const ordered = explicit.length > 0 ? explicit.flatMap((batch) => batch) : (() => {
    const ids = pipelineNodes.map((node: any) => String(node?.id || "")).filter(Boolean);
    const idSet = new Set(ids);
    const forwardEdges = pipelineEdges.filter((edge: AnyRecord) => String(edge?.edgeType || "").toLowerCase() !== "rework");
    const incoming = new Map(ids.map((id) => [id, 0]));
    const outgoing = new Map(ids.map((id) => [id, [] as string[]]));
    for (const edge of forwardEdges) {
      const from = String(edge?.from || edge?.source || "");
      const to = String(edge?.to || edge?.target || "");
      if (!idSet.has(from) || !idSet.has(to)) continue;
      outgoing.get(from)?.push(to);
      incoming.set(to, (incoming.get(to) || 0) + 1);
    }
    const levels = new Map<string, number>();
    const queue = ids.filter((id) => (incoming.get(id) || 0) === 0);
    for (const id of queue) levels.set(id, 0);
    while (queue.length > 0) {
      const current = queue.shift()!;
      const nextLevel = (levels.get(current) || 0) + 1;
      for (const next of outgoing.get(current) || []) {
        incoming.set(next, Math.max(0, (incoming.get(next) || 0) - 1));
        levels.set(next, Math.max(levels.get(next) || 0, nextLevel));
        if ((incoming.get(next) || 0) === 0) queue.push(next);
      }
    }
    ids.forEach((id) => { if (!levels.has(id)) levels.set(id, 0); });
    const maxLevel = Math.max(0, ...Array.from(levels.values()));
    return Array.from({ length: maxLevel + 1 }, (_, level) => ids.filter((id) => levels.get(id) === level)).flatMap((batch) => batch);
  })();
  const seen = new Set(ordered);
  for (const node of pipelineNodes) {
    const nodeId = String(node?.id || "");
    if (!nodeId || seen.has(nodeId)) continue;
    ordered.push(nodeId);
    seen.add(nodeId);
  }
  return pipelineOrderWithLeadingMonitors(ordered, pipelineMonitorNodeIds(pipelineNodes));
}

function pipelineFlowEdgeKey(edge: AnyRecord): string {
  return `${edge.source}->${edge.target}-${edge.index}`;
}

function pipelineFlowElements(pipeline: any, latestRun: any, components: any[]): { nodes: Node[]; edges: Edge[] } {
  const pipelineNodes = pipelineConfigNodes(pipeline);
  const pipelineEdges = pipelineConfigEdges(pipeline);
  const componentByRef = pipelineComponentLookup(components);
  const nodeById: Map<string, AnyRecord> = new Map(pipelineNodes.map((node: any) => [String(node?.id || ""), node]));
  const monitorNodeIds = pipelineNodes
    .filter((node: any) => pipelineNodeIsMonitor(node, pipelineNodeComponent(node, componentByRef)))
    .map((node: any) => String(node?.id || ""))
    .filter(Boolean);
  const columns = pipelineColumnsWithLeadingMonitors(pipelineGraphColumns(pipeline, pipelineNodes, pipelineEdges), monitorNodeIds);
  const flowNodes: Node[] = [];
  const nodeLayout: Map<string, { column: number; row: number; y: number }> = new Map();
  const columnGap = 330;
  const rowGap = 122;
  columns.forEach((batch, columnIndex) => {
    const columnHeight = batch.length * rowGap;
    batch.forEach((nodeId, rowIndex) => {
      const node = nodeById.get(nodeId) || { id: nodeId };
      const component = pipelineNodeComponent(node, componentByRef);
      const status = pipelineRunNodeStatus(latestRun, nodeId).toLowerCase();
      const kind = String(node.kind || component?.componentClass || "node").toLowerCase();
      const componentRef = pipelineComponentRef(node.componentRef || component);
      const componentVersion = String(component?.config?.version || component?.version || "");
      const componentDescription = String(component?.config?.description || component?.description || "");
      const y = rowIndex * rowGap - Math.floor(columnHeight / 2);
      nodeLayout.set(nodeId, { column: columnIndex, row: rowIndex, y });
      flowNodes.push({
        id: nodeId,
        type: "pipelineNode",
        position: {
          x: columnIndex * columnGap,
          y,
        },
        data: {
          exportLabel: {
            id: nodeId,
            kind,
            componentRef,
            componentVersion,
            componentDescription,
            status,
          },
          label: h("div", { className: "flow-node-label" },
            h("strong", null, nodeId),
            h("span", null, kind),
            h("code", { title: componentDescription || componentRef }, componentVersion ? `${componentRef}@${componentVersion}` : componentRef),
            h(StatusBadge, { status }, status),
          ),
        },
        className: `pipeline-flow-node ${kind} ${status}`,
      });
    });
  });
  const graphEdges = pipelineEdges.flatMap((edge: any, index: number) => {
    const source = String(edge?.from || edge?.source || "");
    const target = String(edge?.to || edge?.target || "");
    if (!nodeById.has(source) || !nodeById.has(target)) return [];
    return [{ source, target, index, condition: edge?.condition, edgeType: edge?.edgeType }];
  });
  const sourceTotals = graphEdges.reduce((memo: Map<string, number>, edge: AnyRecord) => memo.set(edge.source, (memo.get(edge.source) || 0) + 1), new Map<string, number>());
  const targetTotals = graphEdges.reduce((memo: Map<string, number>, edge: AnyRecord) => memo.set(edge.target, (memo.get(edge.target) || 0) + 1), new Map<string, number>());
  const pairTotals = graphEdges.reduce((memo: Map<string, number>, edge: AnyRecord) => {
    const key = `${edge.source}->${edge.target}`;
    return memo.set(key, (memo.get(key) || 0) + 1);
  }, new Map<string, number>());
  const sourceSeen = new Map<string, number>();
  const targetSeen = new Map<string, number>();
  const pairSeen = new Map<string, number>();
  const targetHandleByEdge = new Map<string, AnyRecord>();
  const targetPortLoads: Map<string, Map<string, number>> = new Map();
  const adjacentForwardFanOutByEdge = new Map<string, { slot: number; count: number }>();
  const adjacentForwardClusters = graphEdges.reduce((memo: Map<string, AnyRecord[]>, edge: AnyRecord) => {
    const sourceLayout = nodeLayout.get(edge.source);
    const targetLayout = nodeLayout.get(edge.target);
    const rawSpan = (targetLayout?.column || 0) - (sourceLayout?.column || 0);
    const backward = rawSpan <= 0 || String(edge.edgeType || "").toLowerCase() === "rework";
    if (backward || rawSpan !== 1) return memo;
    const key = `${edge.source}->column:${targetLayout?.column ?? ""}`;
    const list = memo.get(key) || [];
    list.push(edge);
    memo.set(key, list);
    return memo;
  }, new Map<string, AnyRecord[]>());
  for (const cluster of adjacentForwardClusters.values()) {
    if (cluster.length < 2) continue;
    cluster
      .slice()
      .sort((left: AnyRecord, right: AnyRecord) => {
        const leftTarget = nodeLayout.get(left.target);
        const rightTarget = nodeLayout.get(right.target);
        return (leftTarget?.y || 0) - (rightTarget?.y || 0) || left.index - right.index;
      })
      .forEach((edge: AnyRecord, index: number, ordered: AnyRecord[]) => {
        adjacentForwardFanOutByEdge.set(pipelineFlowEdgeKey(edge), { slot: index - (ordered.length - 1) / 2, count: ordered.length });
      });
  }
  const sortedIncomingEdges = [...graphEdges].sort((left: AnyRecord, right: AnyRecord) => {
    const leftSource = nodeLayout.get(left.source);
    const leftTarget = nodeLayout.get(left.target);
    const rightSource = nodeLayout.get(right.source);
    const rightTarget = nodeLayout.get(right.target);
    const leftScore = Math.abs((leftTarget?.column || 0) - (leftSource?.column || 0)) * columnGap + Math.abs((leftTarget?.y || 0) - (leftSource?.y || 0));
    const rightScore = Math.abs((rightTarget?.column || 0) - (rightSource?.column || 0)) * columnGap + Math.abs((rightTarget?.y || 0) - (rightSource?.y || 0));
    return leftScore - rightScore || left.index - right.index;
  });
  sortedIncomingEdges.forEach((edge: AnyRecord) => {
    const sourceLayout = nodeLayout.get(edge.source) || { column: 0, row: 0, y: 0 };
    const targetLayout = nodeLayout.get(edge.target) || { column: 0, row: 0, y: 0 };
    const rawSpan = targetLayout.column - sourceLayout.column;
    const span = Math.max(0, rawSpan);
    const backward = rawSpan <= 0 || String(edge.edgeType || "").toLowerCase() === "rework";
    const verticalDelta = sourceLayout.y - targetLayout.y;
    const targetIncomingCount = targetTotals.get(edge.target) || 1;
    const adjacentFanOut = adjacentForwardFanOutByEdge.has(pipelineFlowEdgeKey(edge));
    const primaryForwardInput = !backward && span <= 1 && (adjacentFanOut || targetIncomingCount === 1);
    const loads = targetPortLoads.get(edge.target) || new Map<string, number>();
    targetPortLoads.set(edge.target, loads);
    const port = pipelineInputPorts.slice().sort((left, right) => {
      const score = (item: AnyRecord): number => {
        const side = String(item.side);
        let value = 0;
        if (backward) {
          if (side === "left") value += 86;
          if (side === "top") value += targetLayout.y <= 0 ? -22 : 12;
          if (side === "bottom") value += targetLayout.y >= 0 ? -22 : 12;
          if (Math.abs(targetLayout.y) < 12 && side !== "left") value += edge.index % 2 === 0 ? (side === "top" ? -6 : 6) : (side === "bottom" ? -6 : 6);
          return value;
        }
        if (primaryForwardInput) {
          if (side === "left") value -= adjacentFanOut ? 72 : 44;
          if (side !== "left") value += adjacentFanOut ? 72 : 44;
          return value + Math.abs(verticalDelta) * 0.02;
        }
        if (side === "left") value += span <= 1 ? 0 : 24;
        if (side === "top") value += verticalDelta < -36 ? -18 : 42;
        if (side === "bottom") value += verticalDelta > 36 ? -18 : 42;
        if (span <= 1 && Math.abs(verticalDelta) <= 82 && side !== "left") value += 38;
        if (span > 1 && side !== "left") value -= 10;
        return value;
      };
      const rawLaneHint = sourceLayout.y - targetLayout.y;
      const laneHint = rawLaneHint !== 0 ? rawLaneHint : (edge.index % 2 === 0 ? -1 : 1);
      const totalScore = (item: AnyRecord): number => {
        const load = loads.get(item.id) || 0;
        return score(item) + load * 64 + pipelineEndpointScore(item, loads, laneHint);
      };
      return totalScore(left) - totalScore(right) || String(left.id).localeCompare(String(right.id));
    })[0];
    loads.set(port.id, (loads.get(port.id) || 0) + 1);
    targetHandleByEdge.set(pipelineFlowEdgeKey(edge), port);
  });
  const edgeDrafts: AnyRecord[] = graphEdges.map((edge: AnyRecord) => {
    const targetStatus = pipelineRunNodeStatus(latestRun, edge.target).toLowerCase();
    const pairKey = `${edge.source}->${edge.target}`;
    const sourceSlot = sourceSeen.get(edge.source) || 0;
    const targetSlot = targetSeen.get(edge.target) || 0;
    const pairSlot = pairSeen.get(pairKey) || 0;
    sourceSeen.set(edge.source, sourceSlot + 1);
    targetSeen.set(edge.target, targetSlot + 1);
    pairSeen.set(pairKey, pairSlot + 1);
    const sourceLane = sourceSlot - ((sourceTotals.get(edge.source) || 1) - 1) / 2;
    const targetLane = targetSlot - ((targetTotals.get(edge.target) || 1) - 1) / 2;
    const pairLane = pairSlot - ((pairTotals.get(pairKey) || 1) - 1) / 2;
    const sourceLayout = nodeLayout.get(edge.source);
    const targetLayout = nodeLayout.get(edge.target);
    const rawSpan = (targetLayout?.column || 0) - (sourceLayout?.column || 0);
    const span = Math.max(1, Math.abs(rawSpan));
    const backward = rawSpan <= 0 || String(edge.edgeType || "").toLowerCase() === "rework";
    const verticalDelta = Math.abs((targetLayout?.y || 0) - (sourceLayout?.y || 0));
    const fanOutMeta = adjacentForwardFanOutByEdge.get(pipelineFlowEdgeKey(edge));
    const adjacentFanIn = !backward && rawSpan === 1 && (targetTotals.get(edge.target) || 0) > 1;
    const lane = fanOutMeta ? fanOutMeta.slot : pairLane * 2 + sourceLane + targetLane * 0.45;
    const railDirection = lane === 0 ? (edge.index % 2 === 0 ? -1 : 1) : Math.sign(lane);
    const targetPort = targetHandleByEdge.get(pipelineFlowEdgeKey(edge)) || pipelineInputPorts[1];
    const portDirection = targetPort.side === "top" ? -1 : targetPort.side === "bottom" ? 1 : railDirection;
    const shouldScatter = backward || span > 1 || verticalDelta > 96 || Math.abs(lane) > 0.2 || targetPort.side !== "left";
    const baseScatter = backward ? 118 + span * 18 : 22 + span * 16;
    const sideScatter = targetPort.side === "left" ? 0 : 28;
    const laneOffset = shouldScatter ? Math.max(-280, Math.min(280, portDirection * Math.min(180, baseScatter + sideScatter + verticalDelta * 0.22) + lane * 28)) : 0;
    const sourceHandleIndex = Math.max(0, Math.min(pipelineOutputPorts.length - 1, Math.round(sourceLane + (pipelineOutputPorts.length - 1) / 2)));
    const sourcePort = pipelineOutputPorts[sourceHandleIndex] || pipelineOutputPorts[1];
    const baseEdgeColor = targetStatus === "succeeded" ? "var(--accent-2)" : targetStatus === "running" ? "var(--accent)" : targetStatus === "failed" ? "var(--danger)" : "rgba(129, 147, 159, 0.78)";
    const sourceColumn = sourceLayout?.column || 0;
    const targetColumn = targetLayout?.column || 0;
    const railSign = laneOffset === 0 ? 0 : Math.sign(laneOffset);
    const overlapGroup = backward
      ? `feedback:${sourceColumn}->${targetColumn}:${railSign}`
      : fanOutMeta
        ? `fanout:${sourceColumn}->${targetColumn}:${edge.source}`
        : adjacentFanIn
          ? `fanin:${sourceColumn}->${targetColumn}:${edge.target}`
          : targetPort.side !== "left" || span > 1
            ? `corridor:${sourceColumn}->${targetColumn}:${targetPort.side}:${railSign}:${Math.round(Math.abs(laneOffset) / 56)}`
            : "";
    return {
      id: `${edge.source}->${edge.target}-${edge.index}`,
      source: edge.source,
      target: edge.target,
      sourceHandle: sourcePort.id,
      targetHandle: targetPort.id,
      type: "pipelineCurve",
      zIndex: 12,
      animated: targetStatus === "running",
      data: { baseEdgeColor, laneOffset, routeMode: fanOutMeta && targetPort.side === "left" ? "direct-forward-left" : "", targetSide: targetPort.side, isFeedback: backward, overlapGroup },
      targetStatus,
    };
  });
  const overlapGroupCounts = edgeDrafts.reduce((memo: Map<string, number>, edge: AnyRecord) => {
    const key = String(edge.data?.overlapGroup || "");
    return key ? memo.set(key, (memo.get(key) || 0) + 1) : memo;
  }, new Map<string, number>());
  const overlapGroupSeen = new Map<string, number>();
  const edges: Edge[] = edgeDrafts.map((edge: AnyRecord) => {
    const draftTargetStatus = String(edge.targetStatus || "pending");
    const flowEdge: AnyRecord = { ...edge };
    delete flowEdge.targetStatus;
    const groupKey = String(edge.data?.overlapGroup || "");
    const groupCount = groupKey ? overlapGroupCounts.get(groupKey) || 0 : 0;
    const overlapSlot = groupCount > 1 ? overlapGroupSeen.get(groupKey) || 0 : -1;
    if (groupCount > 1) overlapGroupSeen.set(groupKey, overlapSlot + 1);
    const edgeColor = overlapSlot >= 0 ? pipelineOverlapEdgePalette[overlapSlot % pipelineOverlapEdgePalette.length] : String(edge.data.baseEdgeColor);
    const style: AnyRecord = { stroke: edgeColor };
    if (edge.data.isFeedback) style.strokeDasharray = "9 7";
    return {
      ...flowEdge,
      data: { ...edge.data, edgeColor, overlapSlot, overlapCount: groupCount },
      style,
      markerEnd: { type: MarkerType.ArrowClosed, color: edgeColor },
      className: `pipeline-flow-edge ${draftTargetStatus} ${edge.data.isFeedback ? "feedback" : ""} ${overlapSlot >= 0 ? "overlap-colored" : ""}`,
    } as Edge;
  });
  return { nodes: flowNodes, edges };
}

function escapeSvg(value: any): string {
  return String(value ?? "")
    .replace(/&/g, "&amp;")
    .replace(/</g, "&lt;")
    .replace(/>/g, "&gt;")
    .replace(/"/g, "&quot;");
}

function exportColor(value: any): string {
  const text = String(value || "");
  if (text.includes("--accent-2")) return "#4eb7a8";
  if (text.includes("--accent")) return "#d7a13a";
  if (text.includes("--danger")) return "#cf6a54";
  return text.startsWith("#") ? text : "#81939f";
}

function exportMarkerId(color: string): string {
  return `arrow-${color.replace(/[^a-zA-Z0-9_-]+/g, "")}`;
}

function safeExportFileTitle(title: string, fallback = "pipeline"): string {
  return String(title || fallback).replace(/[^a-zA-Z0-9_-]+/g, "-").replace(/^-|-$/g, "") || fallback;
}

function targetPortPosition(node: Node, handle: string): { x: number; y: number; position: Position } {
  const x = node.position.x;
  const y = node.position.y;
  const port = pipelineInputPorts.find((item) => item.id === handle);
  if (port?.side === "top") return { x: x + pipelineNodeWidth * pipelinePercent(port.style?.left, 0.5), y, position: Position.Top };
  if (port?.side === "bottom") return { x: x + pipelineNodeWidth * pipelinePercent(port.style?.left, 0.5), y: y + pipelineNodeHeight, position: Position.Bottom };
  return { x, y: y + pipelineNodeHeight / 2, position: Position.Left };
}

function sourcePortPosition(node: Node): { x: number; y: number } {
  return { x: node.position.x + pipelineNodeWidth, y: node.position.y + pipelineNodeHeight / 2 };
}

function pipelineGraphSvg(flow: { nodes: Node[]; edges: Edge[] }, title: string): { svg: string; width: number; height: number } {
  const minX = Math.min(...flow.nodes.map((node) => node.position.x), 0) - 220;
  const minY = Math.min(...flow.nodes.map((node) => node.position.y), 0) - 220;
  const maxX = Math.max(...flow.nodes.map((node) => node.position.x + pipelineNodeWidth), 1) + 220;
  const maxY = Math.max(...flow.nodes.map((node) => node.position.y + pipelineNodeHeight), 1) + 220;
  const width = Math.ceil(maxX - minX);
  const height = Math.ceil(maxY - minY);
  const nodeById = new Map(flow.nodes.map((node) => [node.id, node]));
  const edgeColors = flow.edges.map((edge) => exportColor((edge.data as AnyRecord)?.edgeColor || (edge.style as AnyRecord)?.stroke));
  const markerColors = Array.from(new Set(["#4eb7a8", "#d7a13a", "#cf6a54", "#81939f", ...edgeColors]));
  const markers = markerColors.map((color) =>
    `<marker id="${exportMarkerId(color)}" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M 0 0 L 10 5 L 0 10 z" fill="${color}"/></marker>`,
  ).join("");
  const edgeSvg = flow.edges.flatMap((edge) => {
    const source = nodeById.get(edge.source);
    const target = nodeById.get(edge.target);
    if (!source || !target) return [];
    const sourcePoint = sourcePortPosition(source);
    const targetPoint = targetPortPosition(target, String(edge.targetHandle || "in-left"));
    const path = pipelineCurvePath(sourcePoint.x, sourcePoint.y, targetPoint.x, targetPoint.y, targetPoint.position, Number((edge.data as AnyRecord)?.laneOffset || 0), String((edge.data as AnyRecord)?.routeMode || ""));
    const color = exportColor((edge.data as AnyRecord)?.edgeColor || (edge.style as AnyRecord)?.stroke);
    const dash = (edge.data as AnyRecord)?.isFeedback ? ` stroke-dasharray="9 7"` : "";
    return `<path d="${escapeSvg(path)}" fill="none" stroke="${color}" stroke-width="2.35" stroke-linecap="round" stroke-linejoin="round" opacity="0.82"${dash} marker-end="url(#${exportMarkerId(color)})"/>`;
  }).join("\n");
  const nodeSvg = flow.nodes.map((node) => {
    const label = ((node.data as AnyRecord)?.exportLabel || {}) as AnyRecord;
    const status = String(label.status || "pending").toLowerCase();
    const stroke = status === "succeeded" ? "#4eb7a8" : status === "running" ? "#d7a13a" : status === "failed" ? "#cf6a54" : "#81939f";
    const x = node.position.x;
    const y = node.position.y;
    const inputHandles = pipelineInputPorts.map((port) => {
      const point = targetPortPosition(node, port.id);
      if (port.side === "top" || port.side === "bottom") return `<rect x="${point.x - 9}" y="${point.y - 5}" width="18" height="10" rx="2" fill="#071016" stroke="#4eb7a8"/>`;
      return `<rect x="${point.x - 5}" y="${point.y - 9}" width="10" height="18" rx="2" fill="#071016" stroke="#4eb7a8"/>`;
    }).join("\n");
    return `<g>
      <rect x="${x}" y="${y}" width="${pipelineNodeWidth}" height="${pipelineNodeHeight}" rx="2" fill="#0d171f" stroke="${stroke}" stroke-width="1.2"/>
      ${inputHandles}
      <rect x="${x + pipelineNodeWidth - 5}" y="${y + pipelineNodeHeight / 2 - 9}" width="10" height="18" rx="2" fill="#071016" stroke="#d7a13a"/>
      <text x="${x + 12}" y="${y + 22}" fill="#e5edf2" font-size="12" font-family="monospace" font-weight="700">${escapeSvg(label.id || node.id)}</text>
      <text x="${x + 12}" y="${y + 42}" fill="#81939f" font-size="10" font-family="monospace">${escapeSvg(label.kind || "node")}</text>
      <text x="${x + 12}" y="${y + 60}" fill="#d7a13a" font-size="10" font-family="monospace">${escapeSvg(label.componentRef || "--")}</text>
      <text x="${x + 12}" y="${y + 78}" fill="${stroke}" font-size="10" font-family="monospace">${escapeSvg(status)}</text>
    </g>`;
  }).join("\n");
  const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}">
    <defs>${markers}<pattern id="grid" width="22" height="22" patternUnits="userSpaceOnUse"><path d="M 22 0 L 0 0 0 22" fill="none" stroke="#18303a" stroke-width="0.6"/></pattern></defs>
    <rect width="100%" height="100%" fill="#081118"/>
    <rect width="100%" height="100%" fill="url(#grid)" opacity="0.55"/>
    <text x="24" y="34" fill="#d7a13a" font-size="13" font-family="monospace" letter-spacing="2">${escapeSvg(title)}</text>
    <g transform="translate(${-minX} ${-minY})">${nodeSvg}<g>${edgeSvg}</g></g>
  </svg>`;
  return { svg, width, height };
}

function pipelineGanttExportStatusColor(status: any): string {
  const value = String(status || "").toLowerCase();
  if (value === "succeeded" || value === "completed") return "#4eb7a8";
  if (value === "failed") return "#cf6a54";
  if (runningStatus(value)) return "#69aee8";
  return "#d7a13a";
}

function pipelineGanttExportMarkerColor(marker: AnyRecord): string {
  const kind = String(marker?.kind || "");
  const tone = String(marker?.tone || marker?.status || "").toLowerCase();
  if (kind === "prompt" && tone === "initial") return "#d7a13a";
  if (kind === "prompt" && tone === "monitor") return "#69aee8";
  if (kind === "prompt") return "#4eb7a8";
  if (tone === "modify") return "#e0b95a";
  if (tone === "approve" || tone === "guide" || tone === "monitor") return "#4eb7a8";
  if (tone === "restart" || tone === "redo") return "#d7a13a";
  if (tone === "ignored") return "#81939f";
  if (tone === "webui") return "#69aee8";
  if (tone === "cli") return "#d7a13a";
  return "#a7bac5";
}

function pipelineGanttExportArrowColor(arrow: AnyRecord): string {
  const sourceKind = String(arrow?.sourceKind || "").toLowerCase();
  const action = String(arrow?.action || "").toLowerCase();
  const status = String(arrow?.status || "").toLowerCase();
  if (action === "observe" || status === "observation" || sourceKind === "monitor") return "#4eb7a8";
  if (sourceKind === "webui") return "#69aee8";
  if (sourceKind === "cli") return "#d7a13a";
  if (status.includes("ignored")) return "#81939f";
  return "#8aa0ad";
}

function pipelineGanttExportMarkerSvg(marker: AnyRecord, x: number, y: number): string {
  const color = pipelineGanttExportMarkerColor(marker);
  const kind = String(marker?.kind || "");
  if (kind === "control-source") {
    return `<rect x="${x - 5.5}" y="${y - 5.5}" width="11" height="11" rx="2" transform="rotate(45 ${x} ${y})" fill="${color}" stroke="rgba(255,255,255,0.26)" stroke-width="1"/>`;
  }
  if (kind === "control-target") {
    const fill = String(marker?.tone || "").toLowerCase() === "approve" ? "rgba(78,183,168,0.22)" : "#081118";
    return `<circle cx="${x}" cy="${y}" r="5.5" fill="${fill}" stroke="${color}" stroke-width="2"/>`;
  }
  const radius = kind === "prompt" ? 4.5 : 5.5;
  return `<circle cx="${x}" cy="${y}" r="${radius}" fill="${color}" stroke="rgba(255,255,255,0.24)" stroke-width="1"/>`;
}

function pipelineGanttSvg(input: AnyRecord): { svg: string; width: number; height: number } {
  const nodeIds = asArray(input.visibleNodeIds).map((nodeId) => String(nodeId || "")).filter(Boolean);
  const intervals = asArray(input.intervals).filter(isRecord);
  const markers = asArray(input.markers).filter(isRecord);
  const arrows = asArray(input.arrows).filter(isRecord);
  const ticks = asArray(input.ticks).filter(isRecord);
  const bounds = isRecord(input.bounds) ? input.bounds : {};
  const backendLayout = isRecord(input.backendLayout) ? input.backendLayout : null;
  const chartHeight = Math.max(240, Math.round(Number(input.chartHeight || 360)));
  const exportColumnWidth = Math.max(pipelineGanttNodeColumnWidth, 108);
  const timeAxisWidth = 128;
  const padding = 24;
  const titleHeight = 58;
  const headerHeight = 56;
  const boardWidth = timeAxisWidth + Math.max(1, nodeIds.length) * exportColumnWidth;
  const width = Math.max(760, boardWidth + padding * 2);
  const height = titleHeight + headerHeight + chartHeight + padding;
  const boardX = padding;
  const boardY = titleHeight;
  const chartY = boardY + headerHeight;
  const nodeX = (index: number) => boardX + timeAxisWidth + index * exportColumnWidth;
  const nodeCenterX = (index: number) => nodeX(index) + exportColumnWidth / 2;
  const meta = asArray(input.meta).map((item) => String(item || "")).filter(Boolean).slice(0, 4).join("  ·  ");
  const markerById = new Map(markers.map((marker) => [String(marker.id || ""), marker]));
  const markerColors = Array.from(new Set(["#4eb7a8", "#69aee8", "#d7a13a", "#cf6a54", "#8aa0ad", ...arrows.map(pipelineGanttExportArrowColor)]));
  const defs = markerColors.map((color) =>
    `<marker id="${exportMarkerId(color)}" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse"><path d="M 0 0 L 10 5 L 0 10 z" fill="${color}"/></marker>`,
  ).join("");
  const tickSvg = ticks.map((tick) => {
    const y = chartY + pipelineGanttTickY(tick, bounds, chartHeight, backendLayout);
    return `<g>
      <line x1="${boardX}" y1="${y}" x2="${boardX + boardWidth}" y2="${y}" stroke="rgba(215,161,58,0.18)" stroke-width="1"/>
      <text x="${boardX + 8}" y="${y - 4}" fill="#dce6ec" font-size="10" font-family="monospace">${escapeSvg(fmtDate(tick.ms))}</text>
      <text x="${boardX + 8}" y="${y + 10}" fill="#81939f" font-size="9" font-family="monospace">+${escapeSvg(fmtDurationMs(Number(tick.offsetMs ?? Number(tick.ms) - Number(bounds.startMs))))}</text>
    </g>`;
  }).join("\n");
  const headerSvg = [
    `<rect x="${boardX}" y="${boardY}" width="${timeAxisWidth}" height="${headerHeight}" fill="#101d25" stroke="rgba(255,255,255,0.08)"/>`,
    `<text x="${boardX + 10}" y="${boardY + 20}" fill="#d7a13a" font-size="10" font-family="monospace" letter-spacing="1.2">TIME</text>`,
    ...nodeIds.map((nodeId, index) => {
      const x = nodeX(index);
      const label = nodeId.length > 18 ? `${nodeId.slice(0, 16)}…` : nodeId;
      return `<g>
        <rect x="${x}" y="${boardY}" width="${exportColumnWidth}" height="${headerHeight}" fill="#101d25" stroke="rgba(255,255,255,0.08)"/>
        <text x="${x + exportColumnWidth / 2}" y="${boardY + 21}" fill="#dce6ec" font-size="10" font-family="monospace" text-anchor="middle">${escapeSvg(label)}</text>
        <text x="${x + exportColumnWidth / 2}" y="${boardY + 39}" fill="#81939f" font-size="9" font-family="monospace" text-anchor="middle">node ${index + 1}</text>
      </g>`;
    }),
  ].join("\n");
  const columnSvg = nodeIds.map((_nodeId, index) => {
    const x = nodeX(index);
    return `<rect x="${x}" y="${chartY}" width="${exportColumnWidth}" height="${chartHeight}" fill="${index % 2 === 0 ? "rgba(255,255,255,0.018)" : "rgba(255,255,255,0.008)"}" stroke="rgba(255,255,255,0.045)"/>`;
  }).join("\n");
  const intervalSvg = intervals.map((interval) => {
    const index = nodeIds.indexOf(String(interval.nodeId || ""));
    if (index < 0) return "";
    const top = chartY + pipelineGanttIntervalTop(interval, bounds, chartHeight, backendLayout);
    const barHeight = Math.max(2, pipelineGanttIntervalHeight(interval, bounds, chartHeight, backendLayout));
    const color = pipelineGanttExportStatusColor(interval.status);
    const x = nodeCenterX(index) - 3.5;
    const liveOverlay = interval.live ? `<rect x="${x - 1}" y="${top}" width="9" height="${barHeight}" rx="5" fill="none" stroke="rgba(255,255,255,0.54)" stroke-width="1" stroke-dasharray="4 5"/>` : "";
    const label = barHeight >= 28
      ? `<text x="${x + 12}" y="${top + 12}" fill="${color}" font-size="9" font-family="monospace">${escapeSvg(String(interval.status || "working"))}</text>
      <text x="${x + 12}" y="${top + 24}" fill="#81939f" font-size="8" font-family="monospace">${escapeSvg(fmtDurationMs(interval.durationMs))}</text>`
      : "";
    return `<g>
      <rect x="${x}" y="${top}" width="7" height="${barHeight}" rx="4" fill="${color}" stroke="rgba(0,0,0,0.42)" stroke-width="1"/>
      ${liveOverlay}
      ${label}
    </g>`;
  }).join("\n");
  const markerSvg = markers.map((marker) => {
    const index = nodeIds.indexOf(String(marker.nodeId || ""));
    if (index < 0) return "";
    const y = chartY + pipelineGanttMarkerY(marker, bounds, chartHeight, backendLayout);
    return pipelineGanttExportMarkerSvg(marker, nodeCenterX(index), y);
  }).join("\n");
  const arrowSvg = arrows.map((arrow) => {
    const targetMarker = markerById.get(String(arrow.targetMarkerId || ""));
    if (!targetMarker) return "";
    const sourceMarker = markerById.get(String(arrow.sourceMarkerId || ""));
    const sourceNodeId = String(sourceMarker?.nodeId || arrow.sourceNodeId || "");
    const targetNodeId = String(targetMarker.nodeId || arrow.targetNodeId || "");
    const sourceIndex = nodeIds.indexOf(sourceNodeId);
    const targetIndex = nodeIds.indexOf(targetNodeId);
    if (sourceIndex < 0 || targetIndex < 0) return "";
    const sourceX = nodeCenterX(sourceIndex) - boardX - timeAxisWidth;
    const targetX = nodeCenterX(targetIndex) - boardX - timeAxisWidth;
    const sourceY = pipelineGanttLayoutUsesProvidedY(backendLayout)
      ? (pipelineGanttNumber(arrow.sourceY ?? arrow.y1)
        ?? (sourceMarker ? pipelineGanttMarkerY(sourceMarker, bounds, chartHeight, backendLayout) : pipelineGanttMarkerY(targetMarker, bounds, chartHeight, backendLayout)))
      : (sourceMarker ? pipelineGanttMarkerY(sourceMarker, bounds, chartHeight, backendLayout) : pipelineGanttMarkerY(targetMarker, bounds, chartHeight, backendLayout));
    const targetY = pipelineGanttLayoutUsesProvidedY(backendLayout)
      ? (pipelineGanttNumber(arrow.targetY ?? arrow.y2) ?? pipelineGanttMarkerY(targetMarker, bounds, chartHeight, backendLayout))
      : pipelineGanttMarkerY(targetMarker, bounds, chartHeight, backendLayout);
    const color = pipelineGanttExportArrowColor(arrow);
    const dash = String(arrow.action || "").toLowerCase() === "observe" ? "3 4" : "6 5";
    const path = escapeSvg(pipelineControlArrowPath(sourceX, sourceY, targetX, targetY));
    return `<path d="${path}" fill="none" stroke="rgba(8,17,24,0.92)" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
      <path d="${path}" fill="none" stroke="${color}" stroke-width="2.45" stroke-dasharray="${dash}" stroke-linecap="round" stroke-linejoin="round" opacity="0.96" marker-end="url(#${exportMarkerId(color)})"/>`;
  }).join("\n");
  const emptySvg = nodeIds.length === 0 ? `<text x="${boardX + timeAxisWidth + 24}" y="${chartY + 42}" fill="#81939f" font-size="12" font-family="monospace">No visible Gantt nodes</text>` : "";
  const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}">
    <defs>${defs}<pattern id="ganttGrid" width="32" height="32" patternUnits="userSpaceOnUse"><path d="M 32 0 L 0 0 0 32" fill="none" stroke="#18303a" stroke-width="0.55"/></pattern></defs>
    <rect width="100%" height="100%" fill="#081118"/>
    <rect width="100%" height="100%" fill="url(#ganttGrid)" opacity="0.55"/>
    <rect x="${boardX}" y="${chartY}" width="${boardWidth}" height="${chartHeight}" fill="rgba(255,255,255,0.012)" stroke="rgba(78,183,168,0.24)"/>
    <text x="${padding}" y="26" fill="#d7a13a" font-size="13" font-family="monospace" font-weight="700" letter-spacing="1.4">${escapeSvg(input.title || "Pipeline Epoch Gantt")}</text>
    <text x="${padding}" y="45" fill="#81939f" font-size="10" font-family="monospace">${escapeSvg(meta)}</text>
    ${headerSvg}
    <rect x="${boardX}" y="${chartY}" width="${timeAxisWidth}" height="${chartHeight}" fill="rgba(215,161,58,0.055)" stroke="rgba(215,161,58,0.2)"/>
    ${columnSvg}
    ${tickSvg}
    ${intervalSvg}
    <g transform="translate(${boardX + timeAxisWidth} ${chartY})">${arrowSvg}</g>
    ${markerSvg}
    ${emptySvg}
  </svg>`;
  return { svg, width, height };
}

function downloadBlob(blob: Blob, filename: string): void {
  const url = URL.createObjectURL(blob);
  const link = document.createElement("a");
  link.href = url;
  link.download = filename;
  link.click();
  setTimeout(() => URL.revokeObjectURL(url), 1000);
}

async function exportPipelineGraph(flow: { nodes: Node[]; edges: Edge[] }, title: string): Promise<void> {
  const safeTitle = safeExportFileTitle(title, "pipeline");
  const { svg, width, height } = pipelineGraphSvg(flow, title);
  const svgBlob = new Blob([svg], { type: "image/svg+xml;charset=utf-8" });
  const url = URL.createObjectURL(svgBlob);
  try {
    const image = new Image();
    await new Promise<void>((resolve, reject) => {
      image.onload = () => resolve();
      image.onerror = () => reject(new Error("svg image load failed"));
      image.src = url;
    });
    const canvas = document.createElement("canvas");
    canvas.width = width;
    canvas.height = height;
    const ctx = canvas.getContext("2d");
    if (!ctx) throw new Error("canvas unavailable");
    ctx.drawImage(image, 0, 0);
    const pngBlob = await new Promise<Blob | null>((resolve) => canvas.toBlob(resolve, "image/png"));
    if (!pngBlob) throw new Error("png export failed");
    downloadBlob(pngBlob, `${safeTitle}.png`);
  } catch {
    downloadBlob(svgBlob, `${safeTitle}.svg`);
  } finally {
    URL.revokeObjectURL(url);
  }
}

async function exportPipelineGantt(input: AnyRecord): Promise<void> {
  const safeTitle = safeExportFileTitle(String(input?.title || "pipeline-gantt"), "pipeline-gantt");
  const { svg, width, height } = pipelineGanttSvg(input);
  const svgBlob = new Blob([svg], { type: "image/svg+xml;charset=utf-8" });
  const url = URL.createObjectURL(svgBlob);
  try {
    const image = new Image();
    await new Promise<void>((resolve, reject) => {
      image.onload = () => resolve();
      image.onerror = () => reject(new Error("gantt svg image load failed"));
      image.src = url;
    });
    const canvas = document.createElement("canvas");
    canvas.width = width;
    canvas.height = height;
    const ctx = canvas.getContext("2d");
    if (!ctx) throw new Error("canvas unavailable");
    ctx.drawImage(image, 0, 0);
    const pngBlob = await new Promise<Blob | null>((resolve) => canvas.toBlob(resolve, "image/png"));
    if (!pngBlob) throw new Error("gantt png export failed");
    downloadBlob(pngBlob, `${safeTitle}.png`);
  } catch {
    downloadBlob(svgBlob, `${safeTitle}.svg`);
  } finally {
    URL.revokeObjectURL(url);
  }
}

async function exportPipelineGraphs(items: Array<{ flow: { nodes: Node[]; edges: Edge[] }; title: string }>): Promise<void> {
  for (const item of items) {
    if (item.flow.nodes.length === 0) continue;
    await exportPipelineGraph(item.flow, item.title);
    await new Promise((resolve) => setTimeout(resolve, 750));
  }
}

function pipelineLatestRun(runs: any[], pipelineId: string): any {
  return runs.find((run) => String(run?.pipelineId || "") === pipelineId) || null;
}

function pipelineRunStartMs(run: any): number {
  return timeMs(run?.startedAt) ?? timeMs(run?.artifact?.startedAt) ?? timeMs(run?.request?.createdAt) ?? timeMs(run?.updatedAt) ?? 0;
}

function pipelineEpochRuns(runs: any[], pipelineId: string): any[] {
  return runs
    .filter((run: any) => String(run?.pipelineId || "") === pipelineId)
    .slice()
    .sort((left: any, right: any) => pipelineRunStartMs(left) - pipelineRunStartMs(right) || String(left?.runId || "").localeCompare(String(right?.runId || "")));
}

function pipelineEpochLabel(epochs: any[], run: any): string {
  const runId = String(run?.runId || "");
  const index = epochs.findIndex((item: any) => String(item?.runId || "") === runId);
  const number = index >= 0 ? index + 1 : epochs.length;
  const status = String(run?.status || "--");
  return `Epoch ${number} / ${runId || "--"} / ${status}`;
}

function procedureRunIdOf(procedure: any): string {
  return String(procedure?.procedureRunId || procedure?.runId || "");
}

function inferProcedureNodeId(procedure: any, parentRunId: string): string {
  const direct = String(procedure?.nodeId || procedure?.request?.nodeId || "");
  if (direct) return direct;
  const procedureRunId = procedureRunIdOf(procedure);
  const prefix = `${parentRunId}__`;
  if (procedureRunId.startsWith(prefix)) return procedureRunId.slice(prefix.length).replace(/__\d+$/u, "");
  return "";
}

function procedureStartIso(procedure: any, run: any): string {
  const artifact = isRecord(procedure?.artifact) ? procedure.artifact : {};
  const request = isRecord(procedure?.request) ? procedure.request : {};
  return firstIso(
    procedure?.startedAt,
    artifact.startedAt,
    request.createdAt,
    request.startedAt,
    procedure?.createdAt,
    procedure?.updatedAt,
    run?.startedAt,
    run?.request?.createdAt,
  );
}

function procedureEndIso(procedure: any, run: any): string {
  const status = String(procedure?.status?.status || procedure?.artifact?.status || procedure?.status || "").toLowerCase();
  const artifact = isRecord(procedure?.artifact) ? procedure.artifact : {};
  const terminal = terminalStatus(status);
  return firstIso(
    procedure?.finishedAt,
    artifact.finishedAt,
    procedure?.completedAt,
    terminal ? procedure?.updatedAt : undefined,
    terminal ? artifact.updatedAt : undefined,
    terminal ? run?.updatedAt : undefined,
  );
}

function pipelineRunIntervals(run: any, pipelineNodes: AnyRecord[], nowMs = Date.now()): AnyRecord[] {
  const runId = String(run?.runId || "");
  const knownNodeIds = new Set(pipelineNodes.map((node: any) => String(node?.id || "")).filter(Boolean));
  return asArray(run?.procedureRuns).flatMap((procedure: any) => {
    const nodeId = inferProcedureNodeId(procedure, runId);
    if (!nodeId) return [];
    const status = String(procedure?.status?.status || procedure?.artifact?.status || procedure?.status || "unknown").toLowerCase();
    const startIso = procedureStartIso(procedure, run);
    const startMs = timeMs(startIso);
    if (startMs === null) return [];
    const explicitEndIso = procedureEndIso(procedure, run);
    const endMs = timeMs(explicitEndIso) ?? (terminalStatus(status) ? (timeMs(procedure?.updatedAt) ?? startMs + 1000) : nowMs);
    const safeEndMs = Math.max(startMs + 1000, endMs);
    return [{
      nodeId,
      knownNode: knownNodeIds.has(nodeId),
      procedureRunId: procedureRunIdOf(procedure),
      status,
      startMs,
      endMs: safeEndMs,
      startedAt: isoFromMs(startMs),
      finishedAt: isoFromMs(safeEndMs),
      durationMs: safeEndMs - startMs,
      runId,
      raw: procedure,
    }];
  }).sort((left: AnyRecord, right: AnyRecord) => left.startMs - right.startMs || left.endMs - right.endMs || left.nodeId.localeCompare(right.nodeId));
}

function pipelineRunTimeBounds(run: any, intervals: AnyRecord[], markers: AnyRecord[] = []): AnyRecord {
  const starts = intervals.map((item) => Number(item.startMs)).filter(Number.isFinite);
  const ends = intervals.map((item) => Number(item.endMs)).filter(Number.isFinite);
  for (const marker of markers) {
    const markerMs = pipelineGanttNumber(marker?.eventMs ?? marker?.ms);
    if (markerMs !== null) {
      starts.push(markerMs);
      ends.push(markerMs);
    }
  }
  const runStart = timeMs(run?.startedAt) ?? timeMs(run?.artifact?.startedAt) ?? timeMs(run?.request?.createdAt);
  const runEnd = timeMs(run?.finishedAt) ?? timeMs(run?.artifact?.finishedAt) ?? timeMs(run?.updatedAt);
  if (runStart !== null) starts.push(runStart);
  if (runEnd !== null) ends.push(runEnd);
  const now = Date.now();
  const startMs = starts.length > 0 ? Math.min(...starts) : now - 60_000;
  const endMs = Math.max(startMs + 60_000, ends.length > 0 ? Math.max(...ends) : now);
  return { startMs, endMs, durationMs: endMs - startMs };
}

const pipelineGanttScaleBasePxPerMinute = 12;
const pipelineGanttScaleExponentBase = 20;
const pipelineDefaultGanttPxPerMinute = 100;
const pipelineDefaultGanttAutoHideIdle = false;

function clampPipelineGanttScale(value: any): number {
  const number = Number(value);
  if (!Number.isFinite(number)) return 0;
  return Math.max(0, Math.min(100, Math.round(number * 100) / 100));
}

function pipelineScaleValueForPxPerMinute(pxPerMinute: number): number {
  const safePxPerMinute = Math.max(pipelineGanttScaleBasePxPerMinute, Number(pxPerMinute || pipelineGanttScaleBasePxPerMinute));
  const normalized = Math.log(safePxPerMinute / pipelineGanttScaleBasePxPerMinute) / Math.log(pipelineGanttScaleExponentBase);
  return clampPipelineGanttScale(normalized * 100);
}

const pipelineDefaultGanttScale = pipelineScaleValueForPxPerMinute(pipelineDefaultGanttPxPerMinute);

function pipelineGanttScaleConfig(value: number): AnyRecord {
  const normalized = clampPipelineGanttScale(value) / 100;
  const pxPerMinute = pipelineGanttScaleBasePxPerMinute * Math.pow(pipelineGanttScaleExponentBase, normalized);
  const label = normalized < 0.24 ? "全局" : normalized < 0.64 ? "均衡" : "细节";
  return { value: clampPipelineGanttScale(normalized * 100), pxPerMinute, label };
}

function pipelineDisplayPxPerMinute(value: any): number {
  const rounded = Math.round(Number(value));
  return Math.abs(rounded - pipelineDefaultGanttPxPerMinute) <= 1 ? pipelineDefaultGanttPxPerMinute : rounded;
}

function pipelineGanttHeight(bounds: AnyRecord, scaleValue = pipelineDefaultGanttScale): number {
  const minutes = Math.max(1, Number(bounds.durationMs || 0) / 60_000);
  const scale = pipelineGanttScaleConfig(scaleValue);
  return Math.round(Math.max(360, Math.min(7200, minutes * Number(scale.pxPerMinute || 48))));
}

function pipelineGanttTicks(bounds: AnyRecord, count = 7): AnyRecord[] {
  const duration = Math.max(1, Number(bounds.endMs || 0) - Number(bounds.startMs || 0));
  return Array.from({ length: count }, (_item, index) => {
    const ratio = count === 1 ? 0 : index / (count - 1);
    const ms = Number(bounds.startMs) + duration * ratio;
    return { ms, percent: ratio * 100 };
  });
}

function intervalOverlaps(interval: AnyRecord, range: AnyRecord): boolean {
  return Number(interval.startMs) <= Number(range.endMs) && Number(interval.endMs) >= Number(range.startMs);
}

function markerPercent(ms: number, bounds: AnyRecord): number {
  const duration = Math.max(1, Number(bounds.endMs) - Number(bounds.startMs));
  return Math.max(0, Math.min(100, ((ms - Number(bounds.startMs)) / duration) * 100));
}

function pipelineGanttNumber(value: any): number | null {
  const number = Number(value);
  return Number.isFinite(number) ? number : null;
}

function pipelineGanttIntervalIsRunning(interval: AnyRecord): boolean {
  return runningStatus(interval?.status) && !terminalStatus(interval?.status);
}

function pipelineGanttYForMs(ms: number, startMs: number, endMs: number, chartHeight: number): number {
  const durationMs = Math.max(1, endMs - startMs);
  const ratio = Math.max(0, Math.min(1, (ms - startMs) / durationMs));
  return Number((ratio * chartHeight).toFixed(3));
}

function pipelineGanttMsToY(ms: number, layout: AnyRecord | null): number | null {
  if (!layout) return null;
  const startMs = pipelineGanttNumber(layout?.startMs);
  const endMs = pipelineGanttNumber(layout?.endMs);
  const chartHeight = pipelineGanttNumber(layout?.chartHeight);
  if (startMs === null || endMs === null || chartHeight === null) return null;
  return pipelineGanttYForMs(ms, startMs, endMs, chartHeight);
}

function pipelineGanttLiveEndMs(interval: AnyRecord, nowMs: number): number {
  const startMs = pipelineGanttNumber(interval?.rawStartMs ?? interval?.startMs) ?? pipelineGanttNumber(interval?.startMs) ?? nowMs;
  const currentEndMs = pipelineGanttNumber(interval?.endMs) ?? startMs + 1000;
  if (!pipelineGanttIntervalIsRunning(interval)) return Math.max(startMs + 1000, currentEndMs);
  return Math.max(startMs + 1000, currentEndMs, nowMs);
}

function pipelineGanttFrontendLayout(bounds: AnyRecord, scaleValue: number, intervals: AnyRecord[], nowMs: number): AnyRecord {
  const startMs = pipelineGanttNumber(bounds?.startMs) ?? nowMs - 60_000;
  const baseEndMs = pipelineGanttNumber(bounds?.endMs) ?? nowMs;
  const liveEndMs = intervals.reduce((maxMs, interval) => Math.max(maxMs, pipelineGanttLiveEndMs(interval, nowMs)), baseEndMs);
  const endMs = Math.max(startMs + 60_000, baseEndMs, liveEndMs);
  const durationMs = Math.max(1, endMs - startMs);
  const layoutBounds = { startMs, endMs, durationMs };
  const chartHeight = pipelineGanttHeight(layoutBounds, scaleValue);
  const scaleConfig = pipelineGanttScaleConfig(scaleValue);
  const tickCount = Math.max(5, Math.min(18, Math.round(chartHeight / 150)));
  const ticks = pipelineGanttTicks(layoutBounds, tickCount).map((tick) => {
    const ms = Number(tick.ms);
    const y = pipelineGanttYForMs(ms, startMs, endMs, chartHeight);
    return { ...tick, y, timestamp: isoFromMs(ms), offsetMs: ms - startMs };
  });
  return {
    source: "frontend-y",
    startMs,
    endMs,
    durationMs,
    chartHeight,
    scale: clampPipelineGanttScale(scaleValue),
    normalizedScale: Number((clampPipelineGanttScale(scaleValue) / 100).toFixed(3)),
    pxPerMinute: Number(Number(scaleConfig.pxPerMinute || 0).toFixed(3)),
    ticks,
  };
}

function pipelineGanttNormalizeLiveInterval(interval: AnyRecord, layout: AnyRecord | null, nowMs: number): AnyRecord {
  if (!pipelineGanttIntervalIsRunning(interval)) return interval;
  const startMs = pipelineGanttNumber(interval?.rawStartMs ?? interval?.startMs) ?? pipelineGanttNumber(interval?.startMs) ?? nowMs;
  const endMs = pipelineGanttLiveEndMs(interval, nowMs);
  const y1 = pipelineGanttMsToY(startMs, layout);
  const y2 = pipelineGanttMsToY(endMs, layout);
  const safeY1 = pipelineGanttNumber(y1 ?? interval?.y1 ?? interval?.startY) ?? 0;
  const safeY2 = pipelineGanttNumber(y2 ?? interval?.y2 ?? interval?.endY) ?? safeY1 + 10;
  const height = Math.max(24, safeY2 - safeY1);
  return {
    ...interval,
    live: true,
    startMs,
    endMs,
    durationMs: Math.max(1000, endMs - startMs),
    finishedAt: isoFromMs(endMs),
    y1: safeY1,
    y2: safeY2,
    startY: safeY1,
    endY: safeY2,
    height,
  };
}

function pipelineGanttFallbackY(ms: number, bounds: AnyRecord, chartHeight: number): number {
  return (markerPercent(ms, bounds) / 100) * chartHeight;
}

function pipelineGanttLayoutUsesProvidedY(layout: AnyRecord | null): boolean {
  return Boolean(layout && String(layout?.source || "") !== "frontend-y");
}

function pipelineGanttItemY(item: AnyRecord, bounds: AnyRecord, chartHeight: number, backendLayout: AnyRecord | null, fields: string[]): number {
  if (pipelineGanttLayoutUsesProvidedY(backendLayout)) {
    for (const field of fields) {
      const y = pipelineGanttNumber(item?.[field]);
      if (y !== null) return y;
    }
  }
  const ms = pipelineGanttNumber(item?.ms ?? item?.eventMs ?? item?.startMs);
  return pipelineGanttFallbackY(ms ?? Number(bounds.startMs), bounds, chartHeight);
}

function pipelineGanttIntervalTop(interval: AnyRecord, bounds: AnyRecord, chartHeight: number, backendLayout: AnyRecord | null): number {
  return pipelineGanttItemY(interval, bounds, chartHeight, backendLayout, ["y1", "startY"]);
}

function pipelineGanttIntervalBottom(interval: AnyRecord, bounds: AnyRecord, chartHeight: number, backendLayout: AnyRecord | null): number {
  if (pipelineGanttLayoutUsesProvidedY(backendLayout)) {
    const endY = pipelineGanttNumber(interval?.y2 ?? interval?.endY);
    if (endY !== null) return endY;
  }
  const endMs = pipelineGanttNumber(interval?.endMs) ?? Number(bounds.endMs);
  return pipelineGanttFallbackY(endMs, bounds, chartHeight);
}

function pipelineGanttIntervalHeight(interval: AnyRecord, bounds: AnyRecord, chartHeight: number, backendLayout: AnyRecord | null): number {
  if (pipelineGanttLayoutUsesProvidedY(backendLayout)) {
    const height = pipelineGanttNumber(interval?.height);
    if (height !== null) return Math.max(1, height);
  }
  const minHeight = interval?.live ? 24 : 10;
  return Math.max(minHeight, pipelineGanttIntervalBottom(interval, bounds, chartHeight, backendLayout) - pipelineGanttIntervalTop(interval, bounds, chartHeight, backendLayout));
}

function pipelineGanttMarkerY(marker: AnyRecord, bounds: AnyRecord, chartHeight: number, backendLayout: AnyRecord | null): number {
  return pipelineGanttItemY(marker, bounds, chartHeight, backendLayout, ["y", "timeAxisY"]);
}

function pipelineGanttTickY(tick: AnyRecord, bounds: AnyRecord, chartHeight: number, backendLayout: AnyRecord | null): number {
  if (pipelineGanttLayoutUsesProvidedY(backendLayout) || String(backendLayout?.source || "") === "frontend-y") {
    const y = pipelineGanttNumber(tick?.y);
    if (y !== null) return y;
  }
  const percent = pipelineGanttNumber(tick?.percent);
  if (percent !== null) return (percent / 100) * chartHeight;
  const ms = pipelineGanttNumber(tick?.ms) ?? Number(bounds.startMs);
  return pipelineGanttFallbackY(ms, bounds, chartHeight);
}

function pipelineObservedPromptSourceNodeId(marker: AnyRecord): string {
  const promptEvent = String(marker?.promptEvent || marker?.raw?.promptEvent || marker?.event || "").toLowerCase();
  if (!["node-long-running-observation", "node-finished"].includes(promptEvent)) return "";
  const sourceNodeId = String(marker?.sourceNodeId || marker?.raw?.sourceNodeId || marker?.raw?.detail?.nodeId || "");
  const monitorNodeId = String(marker?.nodeId || marker?.targetNodeId || "");
  return sourceNodeId && sourceNodeId !== monitorNodeId ? sourceNodeId : "";
}

function pipelineGanttAddObservationArrows(markers: AnyRecord[], arrows: AnyRecord[]): AnyRecord {
  const arrowKeys = new Set(arrows.map((arrow) => [
    String(arrow.sourceNodeId || ""),
    String(arrow.targetNodeId || ""),
    String(arrow.targetMarkerId || ""),
    String(arrow.action || ""),
  ].join(":")));
  const nextArrows = [...arrows];
  for (const marker of markers) {
    const observedNodeId = pipelineObservedPromptSourceNodeId(marker);
    const monitorNodeId = String(marker?.nodeId || "");
    const targetMarkerId = String(marker?.id || "");
    if (!observedNodeId || !monitorNodeId || !targetMarkerId) continue;
    const arrowKey = [observedNodeId, monitorNodeId, targetMarkerId, "observe"].join(":");
    if (arrowKeys.has(arrowKey)) continue;
    arrowKeys.add(arrowKey);
    nextArrows.push({
      id: `observation-arrow:${targetMarkerId}:${observedNodeId}:${monitorNodeId}`,
      commandId: String(marker?.commandId || marker?.eventId || targetMarkerId),
      sourceNodeId: observedNodeId,
      targetNodeId: monitorNodeId,
      sourceMarkerId: "",
      targetMarkerId,
      sourceKind: "monitor",
      action: "observe",
      status: "observation",
    });
  }
  return { markers, arrows: nextArrows };
}

function pipelinePromptMarkerTone(record: any): string {
  const kind = eventKind(record);
  const promptEvent = String(record?.promptEvent || "");
  if (kind === "initial-prompt-delivered") return "initial";
  if (promptEvent === "node-finished" || promptEvent === "node-long-running-observation" || promptEvent.startsWith("monitor-")) return "monitor";
  if (kind === "monitor-prompt-delivered" || String(record?.sourceKind || "").toLowerCase() === "monitor") return "monitor";
  return "append";
}

function pipelineRecordTags(record: any): string[] {
  return asArray(record?.tags || record?.raw?.tags).map((tag) => String(tag || "")).filter(Boolean);
}

function pipelinePromptMarkerLabel(record: any): string {
  const kind = eventKind(record);
  const promptEvent = String(record?.promptEvent || "");
  if (kind === "initial-prompt-delivered") return "初始 prompt";
  if (promptEvent === "node-long-running-observation") return "长任务观察";
  if (promptEvent === "node-finished") return pipelineRecordTags(record).includes("monitor.audit") ? "节点完成 / OA 审核" : "节点完成";
  if (promptEvent === "monitor-interval") return "Monitor observation";
  if (promptEvent === "monitor-start") return "Monitor start";
  if (promptEvent === "monitor-stop") return "Monitor stop";
  if (kind === "monitor-prompt-delivered") return "Monitor prompt";
  if (kind === "append-prompt-queued") return "追加 prompt 已排队";
  return "追加 prompt";
}

function controlEventPriority(record: any): number {
  const kind = eventKind(record);
  if (kind === "control-command-applied") return 3;
  if (kind === "control-command-ignored") return 2;
  if (kind === "control-command-queued") return 1;
  return 0;
}

function controlRecordGroupKey(record: any, index: number): string {
  const commandId = String(record?.commandId || "");
  if (commandId) return `command:${commandId}`;
  const timestamp = eventTimestampIso(record) || firstIso(record?.createdAt, record?.timestamp) || `index-${index}`;
  return [
    "control-event",
    timestamp,
    String(record?.sourceKind || ""),
    String(record?.sourceNodeId || ""),
    String(record?.targetNodeId || ""),
    controlActionValue(record),
  ].join(":");
}

function controlTargetNodeIds(record: any): string[] {
  return uniqueStrings([
    record?.targetNodeId,
    ...asArray(record?.resetNodeIds),
  ]);
}

function controlTargetLabel(record: any, targetNodeId: string): string {
  const action = controlActionLabel(record);
  const kind = eventKind(record);
  const primaryTarget = String(record?.targetNodeId || "");
  const isResetSideEffect = Boolean(primaryTarget) && targetNodeId !== primaryTarget;
  if (kind === "control-command-applied") return isResetSideEffect ? `${action} 波及` : `${action} 生效`;
  if (kind === "control-command-ignored") return `${action} 忽略`;
  if (kind === "control-command-queued") return `${action} 已发起`;
  return isResetSideEffect ? `${action} 波及` : action;
}

function controlTargetTone(record: any): string {
  const kind = eventKind(record);
  if (kind === "control-command-ignored") return "ignored";
  const action = controlActionValue(record);
  if (action === "restart" || action === "redo") return "restart";
  if (action === "modify") return "modify";
  if (action === "approve") return "approve";
  if (action === "guide") return "guide";
  return "pending";
}

function controlSourceTone(record: any): string {
  const sourceKind = String(record?.sourceKind || "").toLowerCase();
  if (sourceKind === "monitor") return "monitor";
  if (sourceKind === "webui") return "webui";
  if (sourceKind === "cli") return "cli";
  return "system";
}

function controlTargetMarkerPlacement(intervals: AnyRecord[], nodeId: string, eventMs: number, record: any): AnyRecord {
  const nodeIntervals = intervals
    .filter((interval: AnyRecord) => String(interval.nodeId || "") === nodeId)
    .sort((left: AnyRecord, right: AnyRecord) => Number(left.startMs) - Number(right.startMs));
  const containing = nodeIntervals.find((interval: AnyRecord) => eventMs >= Number(interval.startMs) - 1000 && eventMs <= Number(interval.endMs) + 1000);
  if (containing) {
    return { ms: eventMs, onInterval: true, snapReason: "inside-interval", procedureRunId: String(containing.procedureRunId || "") };
  }
  const action = controlActionValue(record);
  const previous = nodeIntervals.slice().reverse().find((interval: AnyRecord) => Number(interval.endMs) <= eventMs + 1000);
  if (previous && action === "approve") {
    return { ms: Number(previous.endMs), onInterval: true, snapReason: "previous-interval-end", procedureRunId: String(previous.procedureRunId || "") };
  }
  const next = nodeIntervals.find((interval: AnyRecord) => Number(interval.startMs) >= eventMs - 1000);
  if (next && ["guide", "modify", "restart", "redo"].includes(action)) {
    return { ms: Number(next.startMs), onInterval: true, snapReason: "next-interval-start", procedureRunId: String(next.procedureRunId || "") };
  }
  return { ms: eventMs, onInterval: false, snapReason: "event-time", procedureRunId: String(record?.procedureRunId || "") };
}

function pipelineControlArrowPath(sourceX: number, sourceY: number, targetX: number, targetY: number): string {
  const distance = Math.hypot(targetX - sourceX, targetY - sourceY);
  const inset = distance > pipelineGanttArrowTipInsetPx ? pipelineGanttArrowTipInsetPx : 0;
  const endX = inset > 0 ? targetX - ((targetX - sourceX) / distance) * inset : targetX;
  const endY = inset > 0 ? targetY - ((targetY - sourceY) / distance) * inset : targetY;
  const deltaX = endX - sourceX;
  const bend = Math.max(16, Math.min(42, Math.abs(deltaX) * 0.45 + 12));
  const sign = deltaX === 0 ? 1 : Math.sign(deltaX);
  return `M ${sourceX},${sourceY} C ${sourceX + sign * bend},${sourceY} ${endX - sign * bend},${endY} ${endX},${endY}`;
}

function pipelineRunGanttSignals(details: any, activeRun: any): AnyRecord {
  const runId = String(details?.runId || activeRun?.runId || "");
  const intervals = pipelineRunIntervals({ ...(isRecord(activeRun) ? activeRun : {}), ...(isRecord(details) ? details : {}), runId, procedureRuns: asArray(details?.procedureRuns).length > 0 ? details.procedureRuns : activeRun?.procedureRuns }, []);
  const promptMarkers: AnyRecord[] = [];
  const controlMarkers: AnyRecord[] = [];
  const controlArrows: AnyRecord[] = [];
  const markerIds = new Set<string>();
  const sourceMarkerByCommand = new Map<string, string>();

  const addMarker = (marker: AnyRecord, bucket: AnyRecord[]) => {
    if (!marker.nodeId || !Number.isFinite(Number(marker.ms))) return;
    if (markerIds.has(marker.id)) return;
    markerIds.add(marker.id);
    bucket.push(marker);
  };

  for (const procedure of asArray(details?.procedureRuns)) {
    const nodeId = inferProcedureNodeId(procedure, runId);
    const procedureRunId = procedureRunIdOf(procedure);
    if (!nodeId) continue;
    for (const attempt of asArray(procedure?.attempts)) {
      const attemptId = attemptLabel(attempt);
      for (const record of structuredEventRecords(attempt?.controlEventRecords)) {
        const kind = eventKind(record);
        if (!["initial-prompt-delivered", "append-prompt-delivered", "monitor-prompt-delivered"].includes(kind)) continue;
        const timestampIso = eventTimestampIso(record);
        const ms = timeMs(timestampIso);
        if (ms === null) continue;
        const eventId = String(record?.eventId || "");
        addMarker({
          id: `prompt:${eventId || `${procedureRunId}:${attemptId}:${kind}:${ms}`}`,
          runId,
          nodeId,
          procedureRunId,
          attempt: attemptId,
          kind: "prompt",
          tone: pipelinePromptMarkerTone(record),
          status: "delivered",
          label: pipelinePromptMarkerLabel(record),
          ms,
          timestampIso,
          sourceKind: String(record?.sourceKind || ""),
          sourceNodeId: String(record?.sourceNodeId || ""),
          targetNodeId: nodeId,
          action: "",
          eventId,
          commandId: String(record?.commandId || ""),
          raw: record,
        }, promptMarkers);
      }
    }
  }

  const grouped = new Map<string, AnyRecord>();
  structuredEventRecords(details?.controlEvents).forEach((record: AnyRecord, index: number) => {
    const key = controlRecordGroupKey(record, index);
    const group = grouped.get(key) || { key, events: [] };
    group.events.push(record);
    grouped.set(key, group);
  });

  for (const group of grouped.values()) {
    const events = asArray(group.events).slice().sort((left: any, right: any) => controlEventPriority(right) - controlEventPriority(left));
    const queuedRecord = asArray(group.events).find((record: any) => eventKind(record) === "control-command-queued") || null;
    const outcomeRecord = events[0] || queuedRecord;
    if (!queuedRecord && !outcomeRecord) continue;
    const sourceNodeId = String(queuedRecord?.sourceNodeId || outcomeRecord?.sourceNodeId || "");
    const sourceKind = String(queuedRecord?.sourceKind || outcomeRecord?.sourceKind || "");
    const sourceTimestampIso = eventTimestampIso(queuedRecord) || eventTimestampIso(outcomeRecord) || firstIso(queuedRecord?.createdAt, outcomeRecord?.createdAt);
    const sourceMs = timeMs(sourceTimestampIso);
    const commandId = String(outcomeRecord?.commandId || queuedRecord?.commandId || group.key);
    const outcomeStatus = (eventKind(outcomeRecord) || "control-command-queued").replace(/^control-command-/u, "");
    let sourceMarkerId = "";
    if (sourceNodeId && sourceMs !== null) {
      sourceMarkerId = `control-source:${commandId}:${sourceNodeId}`;
      sourceMarkerByCommand.set(commandId, sourceMarkerId);
      addMarker({
        id: sourceMarkerId,
        runId,
        nodeId: sourceNodeId,
        procedureRunId: String(queuedRecord?.procedureRunId || outcomeRecord?.procedureRunId || ""),
        attempt: "",
        kind: "control-source",
        tone: controlSourceTone(queuedRecord || outcomeRecord),
        status: outcomeStatus,
        label: `${controlActionLabel(queuedRecord || outcomeRecord)} 发起`,
        ms: sourceMs,
        timestampIso: sourceTimestampIso,
        action: controlActionValue(queuedRecord || outcomeRecord),
        sourceKind,
        sourceNodeId,
        targetNodeId: String(outcomeRecord?.targetNodeId || queuedRecord?.targetNodeId || ""),
        commandId,
        raw: queuedRecord || outcomeRecord,
      }, controlMarkers);
    }
    const targetRecord = outcomeRecord || queuedRecord;
    const targetTimestampIso = eventTimestampIso(targetRecord) || sourceTimestampIso;
    const targetMs = timeMs(targetTimestampIso);
    if (targetMs === null) continue;
    const targetIds = controlTargetNodeIds(targetRecord);
    for (const targetNodeId of targetIds) {
      const placement = controlTargetMarkerPlacement(intervals, targetNodeId, targetMs, targetRecord);
      const targetMarkerId = `control-target:${commandId}:${targetNodeId}`;
      addMarker({
        id: targetMarkerId,
        runId,
        nodeId: targetNodeId,
        procedureRunId: placement.procedureRunId,
        attempt: "",
        kind: "control-target",
        tone: controlTargetTone(targetRecord),
        status: outcomeStatus,
        label: controlTargetLabel(targetRecord, targetNodeId),
        ms: placement.ms,
        eventMs: targetMs,
        onInterval: placement.onInterval,
        snapReason: placement.snapReason,
        snapped: Number(placement.ms) !== targetMs,
        timestampIso: targetTimestampIso,
        renderedTimestampIso: isoFromMs(Number(placement.ms)),
        action: controlActionValue(targetRecord),
        sourceKind,
        sourceNodeId,
        targetNodeId,
        commandId,
        raw: targetRecord,
      }, controlMarkers);
      if (sourceMarkerId && sourceNodeId && sourceNodeId !== targetNodeId) {
        controlArrows.push({
          id: `control-arrow:${commandId}:${sourceNodeId}:${targetNodeId}`,
          commandId,
          sourceNodeId,
          targetNodeId,
          sourceMarkerId,
          targetMarkerId,
          sourceKind,
          action: controlActionValue(targetRecord),
          status: outcomeStatus,
        });
      }
    }
  }

  const markers = [...promptMarkers, ...controlMarkers].sort((left: AnyRecord, right: AnyRecord) =>
    Number(left.ms) - Number(right.ms)
      || String(left.nodeId).localeCompare(String(right.nodeId))
      || String(left.id).localeCompare(String(right.id)));
  return { ...pipelineGanttAddObservationArrows(markers, controlArrows), sourceMarkerByCommand };
}

function PipelineNodeExecutionIndex({ details, selectedNodeId, selectedNodeRuntime, control, onRaw }: AnyRecord) {
  if (!details) {
    return h("span", { className: "muted" }, "点击“抓取过程”读取 node 运行材料；主界面只显示结构化摘要，完整内容需点开原始 JSON。");
  }
  const procedureRuns = asArray(details.procedureRuns);
  const latestProcedure = procedureRuns.at(-1) || {};
  const attempts = asArray(latestProcedure.attempts);
  const latestAttempt = attempts.at(-1) || {};
  const workerLogTail = asArray(latestProcedure.workerLogTail);
  const controlEventsTail = asArray(latestAttempt.controlEventsTail);
  const controlPromptsTail = asArray(latestAttempt.controlPromptsTail);
  const monitorPromptsTail = asArray(latestAttempt.monitorPromptsTail);
  const eventTail = tailSummary(controlEventsTail);
  const controlPromptTail = tailSummary(controlPromptsTail);
  const monitorPromptTail = tailSummary(monitorPromptsTail);
  const latestMessage = latestAttempt.opencodeMessages || {};
  return h("div", { className: "pipeline-evidence-list compact" },
    h(EvidenceIndexRow, {
      title: "Node runtime",
      subtitle: selectedNodeId || "--",
      facts: [
        `status ${selectedNodeRuntime?.status || "pending"}`,
        `attempts ${selectedNodeRuntime?.attempts ?? attempts.length}`,
        `procedure ${selectedNodeRuntime?.currentProcedureRunId || procedureRunIdOf(latestProcedure) || "--"}`,
        control.fetchedAt ? `fetched ${fmtClock(control.fetchedAt)}` : "not fetched",
      ],
      data: details.node || details,
      onRaw,
      testId: "raw-pipeline-node-runtime",
    }),
    h(EvidenceIndexRow, {
      title: "Procedure runs",
      subtitle: `${procedureRuns.length} groups`,
      facts: [
        `latest ${latestProcedure.status?.status || latestProcedure.status || "--"}`,
        `steps ${asArray(latestProcedure.recentSteps).length}`,
        `duration ${fmtDurationMs(timeMs(latestProcedure.finishedAt) && timeMs(latestProcedure.startedAt) ? Number(timeMs(latestProcedure.finishedAt)) - Number(timeMs(latestProcedure.startedAt)) : latestProcedure.durationMs)}`,
      ],
      data: procedureRuns,
      onRaw,
      testId: "raw-pipeline-node-procedures",
    }),
    h(EvidenceIndexRow, {
      title: "OpenCode messages",
      subtitle: String(latestMessage.exists ? "available" : "not indexed"),
      facts: [
        `messages ${summarizeValue(latestMessage.messageCount)}`,
        `size ${summarizeValue(latestMessage.size)}`,
        `updated ${fmtDate(latestMessage.updatedAt)}`,
      ],
      data: latestMessage,
      onRaw,
      testId: "raw-pipeline-node-messages",
    }),
    h(EvidenceIndexRow, {
      title: "Control prompts",
      subtitle: "manual / monitor append queues",
      facts: [
        `manual tail ${controlPromptTail.total}`,
        `monitor tail ${monitorPromptTail.total}`,
        `last ${fmtDate(latestIso(controlPromptTail.lastAt, monitorPromptTail.lastAt))}`,
      ],
      data: { controlPromptsTail, monitorPromptsTail },
      onRaw,
      testId: "raw-pipeline-node-prompts",
    }),
    h(EvidenceIndexRow, {
      title: "Control events",
      subtitle: eventTail.eventKinds.length > 0 ? eventTail.eventKinds.join(", ") : "event tail",
      facts: [
        `tail ${eventTail.total}`,
        `parsed ${eventTail.parsed}`,
        `last ${fmtDate(eventTail.lastAt)}`,
      ],
      data: controlEventsTail,
      onRaw,
      testId: "raw-pipeline-node-events",
    }),
    h(EvidenceIndexRow, {
      title: "Worker log",
      subtitle: "tail is hidden on main canvas",
      facts: [`tail ${workerLogTail.length} lines`, `raw only via button`, `procedure ${procedureRunIdOf(latestProcedure) || "--"}`],
      data: workerLogTail,
      onRaw,
      testId: "raw-pipeline-node-worker-log",
    }),
  );
}

function PipelineRunMaterialIndex({ activeRun, onRaw }: AnyRecord) {
  if (!activeRun) return h(EmptyState, { title: "暂无运行材料", text: "没有 Pipeline epoch 时不会展示运行材料索引。" });
  const nodes = asArray(activeRun.nodes);
  const procedures = asArray(activeRun.procedureRuns);
  const submissions = asArray(activeRun.submissions);
  const workerLogTail = asArray(activeRun.workerLogTail);
  const nodeCounts = statusCounts(nodes);
  const procedureCounts = statusCounts(procedures);
  const failedProcedures = procedures.filter((procedure: any) => String(procedure?.status || "").toLowerCase() === "failed");
  const latestProcedureAt = latestIso(...procedures.flatMap((procedure: any) => [procedure.updatedAt, procedure.finishedAt, procedure.startedAt]));
  return h("div", { className: "pipeline-evidence-list" },
    h(EvidenceIndexRow, {
      title: "Epoch overview",
      subtitle: activeRun.runId || "--",
      facts: [
        `pipeline ${activeRun.pipelineId || "--"}`,
        `status ${activeRun.status || "--"}`,
        `started ${fmtDate(activeRun.startedAt)}`,
        `updated ${fmtDate(activeRun.updatedAt)}`,
      ],
      data: activeRun,
      onRaw,
      testId: "raw-pipeline-run",
    }),
    h(EvidenceIndexRow, {
      title: "Node states",
      subtitle: `${nodes.length} nodes`,
      facts: [
        `running ${nodeCounts.running || 0}`,
        `succeeded ${nodeCounts.succeeded || 0}`,
        `failed ${nodeCounts.failed || 0}`,
        `pending ${nodeCounts.pending || 0}`,
      ],
      data: nodes,
      onRaw,
      testId: "raw-pipeline-run-nodes",
    }),
    h(EvidenceIndexRow, {
      title: "Procedure run index",
      subtitle: `${procedures.length} procedure records`,
      facts: [
        `succeeded ${procedureCounts.succeeded || 0}`,
        `failed ${procedureCounts.failed || 0}`,
        `latest ${fmtDate(latestProcedureAt)}`,
        `errors ${failedProcedures.length}`,
      ],
      data: procedures,
      onRaw,
      testId: "raw-pipeline-run-procedures",
    }),
    h(EvidenceIndexRow, {
      title: "OA submissions",
      subtitle: `${submissions.length} submission files`,
      facts: [`records ${submissions.length}`, `task ${summarizeValue(activeRun.task)}`, `raw grouped by run`],
      data: submissions,
      onRaw,
      testId: "raw-pipeline-run-submissions",
    }),
    h(EvidenceIndexRow, {
      title: "Worker log tail",
      subtitle: "hidden from main interface",
      facts: [`tail ${workerLogTail.length} lines`, `display raw only after click`, `updated ${fmtDate(activeRun.updatedAt)}`],
      data: workerLogTail,
      onRaw,
      testId: "raw-pipeline-run-worker-log",
    }),
  );
}

function PipelineOaEventFlowPanel({ diagnostics, onRaw }: AnyRecord) {
  const runs = asArray(diagnostics?.runs).filter(isRecord);
  const forbiddenResiduals = asArray(diagnostics?.forbiddenResiduals);
  const guarantees = isRecord(diagnostics?.guarantees) ? diagnostics.guarantees : {};
  const evidenceOk = diagnostics?.hasNeutralNodeFinishedEvidence === true
    && diagnostics?.hasNoAuditPolicyEvidence === true
    && diagnostics?.hasAuditPolicyEvidence === true;
  const ok = diagnostics?.ok === true && evidenceOk && forbiddenResiduals.length === 0;
  const latestRun = runs[0] || null;
  const guaranteeItems = [
    { label: "中性完成事实", ok: guarantees.neutralNodeFinished === true, hint: "node-finished 不携带流程策略" },
    { label: "Config 策略判定", ok: guarantees.auditPolicyFromConfig === true, hint: "OA backend 读取当前 epoch 配置" },
    { label: "控制命令来自 OA", ok: guarantees.runnerConsumesControlCommandsFromOaEvents === true, hint: "runner 只消费 OA control.command" },
    { label: "无独立审核事件", ok: guarantees.noIndependentAuditRequestEvent === true, hint: "审核由 node-finished + policy 派生" },
    { label: "无批次门禁", ok: guarantees.noBatchFinishedControlGate === true, hint: "下游启动由每个 node 完成驱动" },
  ];
  return h("div", { className: "pipeline-oa-panel", "data-testid": "pipeline-oa-event-flow-panel" },
    h("div", { className: "metric-grid compact" },
      h(MetricCard, { label: "OA Flow", value: ok ? "100%" : "--", hint: String(diagnostics?.mode || "waiting diagnostics"), tone: ok ? "ok" : "warn" }),
      h(MetricCard, { label: "禁止残留", value: forbiddenResiduals.length, hint: forbiddenResiduals.length === 0 ? "source scan clean" : "needs cleanup", tone: forbiddenResiduals.length === 0 ? "ok" : "warn" }),
      h(MetricCard, { label: "No-audit", value: diagnostics?.hasNoAuditPolicyEvidence ? "OK" : "--", hint: "OA 下游策略证据", tone: diagnostics?.hasNoAuditPolicyEvidence ? "ok" : "warn" }),
      h(MetricCard, { label: "Monitor 审核", value: diagnostics?.hasAuditPolicyEvidence ? "OK" : "--", hint: "OA 控制事件闭环", tone: diagnostics?.hasAuditPolicyEvidence ? "ok" : "warn" }),
    ),
    h("div", { className: "pipeline-oa-guarantees" },
      guaranteeItems.map((item) => h("article", { key: item.label, className: `pipeline-oa-guarantee ${item.ok ? "ok" : "warn"}` },
        h(StatusBadge, { status: item.ok ? "online" : "warn" }, item.ok ? "OK" : "MISS"),
        h("div", null, h("strong", null, item.label), h("span", null, item.hint)),
      )),
    ),
    h("div", { className: "pipeline-evidence-list compact" },
      runs.slice(0, 6).map((run: AnyRecord) => h(EvidenceIndexRow, {
        key: run.runId,
        title: String(run.runId || "--"),
        subtitle: [
          Number(run.monitorAuditNodeFinishedCount || 0) > 0 ? "monitor audit" : "",
          Number(run.noAuditPolicyCount || 0) > 0 ? "no-audit policy" : "",
        ].filter(Boolean).join(" / ") || "event evidence",
        facts: [
          `events ${run.eventCount || 0}`,
          `node-finished ${run.nodeFinishedCount || 0}`,
          `policy-in-detail ${run.nodeFinishedWithPolicyCount || 0}`,
          `queued ${run.controlQueuedCount || 0}`,
          `applied ${run.controlAppliedCount || 0}`,
        ],
        data: run,
        onRaw,
        testId: `raw-pipeline-oa-run-${String(run.runId || "run").replace(/[^a-zA-Z0-9_.-]+/g, "-")}`,
      })),
    ),
    latestRun ? h("p", { className: "muted paragraph" }, `最新证据 ${latestRun.runId}: ${latestRun.nodeFinishedCount || 0} 个 node-finished，${latestRun.controlAppliedCount || 0} 个控制结果。`) : h(EmptyState, { title: "暂无 OA 事件流证据", text: "等待 Pipeline backend 暴露 diagnostics。" }),
    diagnostics ? h("div", { className: "panel-actions inline-actions" }, h(RawButton, { title: "Pipeline OA Event Flow Diagnostics", data: diagnostics, onOpen: onRaw, testId: "raw-pipeline-oa-event-flow" })) : null,
  );
}

function PipelineMinimaxQuotaPanel({ quota, onRaw }: AnyRecord) {
  const summary = isRecord(quota?.summary) ? quota.summary : {};
  const target = isRecord(quota?.target) ? quota.target : {};
  const cache = isRecord(quota?.cache) ? quota.cache : {};
  const ok = quota?.ok === true;
  const modelId = String(quota?.modelId || summary.modelName || target.modelName || "MiniMax-M2.7");
  const total = summary.totalCount ?? target.currentIntervalTotalCount;
  const used = summary.usageCount ?? target.currentIntervalUsageCount;
  const remaining = summary.remainingCount ?? target.currentIntervalRemainingCount;
  const remainingRatio = summary.remainingRatio ?? (
    Number.isFinite(Number(total)) && Number(total) > 0 && Number.isFinite(Number(remaining)) ? Number(remaining) / Number(total) : undefined
  );
  const usageRatio = summary.usageRatio ?? (
    Number.isFinite(Number(total)) && Number(total) > 0 && Number.isFinite(Number(used)) ? Number(used) / Number(total) : undefined
  );
  const resetAt = summary.resetAt || target.endAt;
  const remainsMs = summary.remainsMs ?? target.remainsMs;
  const remainingNumber = Number(remaining);
  const tone = !ok || (Number.isFinite(remainingNumber) && remainingNumber <= 0) ? "warn" : "ok";
  const facts = [
    ok ? `endpoint ${quota?.endpoint || "--"}` : "quota unavailable",
    `fetched ${fmtClockValue(quota?.fetchedAt)}`,
    cache.hit ? `cache ${fmtDurationMs(cache.ageMs)}` : "live quota",
  ];
  return h("div", { className: "pipeline-minimax-quota-panel", "data-testid": "pipeline-minimax-quota-panel" },
    h("div", { className: "metric-grid compact" },
      h(MetricCard, { label: "MiniMax", value: ok ? modelId : "--", hint: quota?.modelComponent || quota?.error || "model/minimax-m27", tone }),
      h(MetricCard, { label: "当前窗口", value: `${fmtCount(used)}/${fmtCount(total)}`, hint: `已用 ${fmtPercent(usageRatio)}`, tone }),
      h(MetricCard, { label: "剩余额度", value: fmtCount(remaining), hint: `剩余 ${fmtPercent(remainingRatio)}`, tone }),
      h(MetricCard, { label: "重置时间", value: fmtClockValue(resetAt), hint: remainsMs !== undefined ? `约 ${fmtDurationMs(remainsMs)}` : fmtDate(resetAt), tone }),
    ),
    h(PipelineChipRow, { items: facts }),
    ok ? h("p", { className: "muted paragraph" }, `MiniMax 限额来自 D601 Pipeline 后端实时查询；当前模型匹配 ${summary.modelName || target.modelName || modelId}。`)
      : h(UniDeskErrorBanner, { error: quota?.error || "MiniMax 限额查询失败" }),
    quota ? h("div", { className: "panel-actions inline-actions" }, h(RawButton, { title: "Pipeline MiniMax Quota", data: quota, onOpen: onRaw, testId: "raw-pipeline-minimax-quota" })) : null,
  );
}

function PipelineEpochGantt({ epochs, activeRun, activePipeline, pipelineNodes, pipelineEdges, runDetails, nodeDetails, nodeDetailsState, ganttScale = pipelineDefaultGanttScale, onGanttScaleChange, onRunChange, onIntervalSelect, onMarkerSelect, selection, detailOpen, onDetailOpenChange, onRaw }: AnyRecord) {
  const [autoHideIdle, setAutoHideIdle] = useState(pipelineDefaultGanttAutoHideIdle);
  const [visibleRange, setVisibleRange] = useState({ startY: 0, endY: 0, startMs: 0, endMs: 0 });
  const [liveNowMs, setLiveNowMs] = useState(Date.now());
  const viewportRef = useRef(null);
  const activeRunId = String(activeRun?.runId || "");
  const ganttDetailOpen = Boolean(detailOpen);
  const setGanttDetailOpen = (open: boolean) => {
    if (typeof onDetailOpenChange === "function") onDetailOpenChange(open);
  };
  const timeScale = clampPipelineGanttScale(ganttScale ?? pipelineDefaultGanttScale);
  const runDetailPayload = String(runDetails?.runId || "") === activeRunId ? runDetails?.details : null;
  const timelineRun = runDetailPayload
    ? {
      ...(isRecord(activeRun) ? activeRun : {}),
      ...(isRecord(runDetailPayload) ? runDetailPayload : {}),
      runId: activeRunId,
      procedureRuns: asArray(runDetailPayload?.procedureRuns).length > 0 ? runDetailPayload.procedureRuns : activeRun?.procedureRuns,
    }
    : activeRun;
  const rawIntervals = pipelineRunIntervals(timelineRun, pipelineNodes, liveNowMs);
  const ganttSignals = runDetailPayload ? pipelineRunGanttSignals(runDetailPayload, timelineRun) : { markers: [], arrows: [] };
  const allMarkers = asArray(ganttSignals.markers);
  const initialBounds = pipelineRunTimeBounds(timelineRun, rawIntervals, allMarkers);
  const ganttLayout = pipelineGanttFrontendLayout(initialBounds, timeScale, rawIntervals, liveNowMs);
  const layoutSource = String(ganttLayout.source || "frontend-y");
  const intervals = rawIntervals.map((interval: AnyRecord) => pipelineGanttNormalizeLiveInterval(interval, ganttLayout, liveNowMs));
  const bounds = {
    startMs: Number(ganttLayout.startMs),
    endMs: Number(ganttLayout.endMs),
    durationMs: Math.max(1, Number(ganttLayout.durationMs ?? Number(ganttLayout.endMs) - Number(ganttLayout.startMs))),
  };
  const baseTimeScaleConfig = pipelineGanttScaleConfig(timeScale);
  const timeScaleConfig: AnyRecord = {
    ...baseTimeScaleConfig,
    pxPerMinute: Number(ganttLayout.pxPerMinute ?? baseTimeScaleConfig.pxPerMinute),
  };
  const chartHeight = Math.round(Number(ganttLayout.chartHeight || 360));
  const hasLiveRunning = rawIntervals.some(pipelineGanttIntervalIsRunning);
  useEffect(() => {
    if (!activeRunId || !hasLiveRunning) return undefined;
    const timer = window.setInterval(() => setLiveNowMs(Date.now()), 1000);
    return () => window.clearInterval(timer);
  }, [activeRunId, hasLiveRunning]);
  const graphNodeOrder = pipelineGraphNodeOrder(activePipeline, pipelineNodes, Array.isArray(pipelineEdges) ? pipelineEdges : []);
  const configuredNodeIds = pipelineNodes.map((node: any) => String(node?.id || "")).filter(Boolean);
  const intervalNodeIds = intervals.map((interval: AnyRecord) => String(interval.nodeId || "")).filter(Boolean);
  const markerNodeIds = allMarkers.map((marker: AnyRecord) => String(marker.nodeId || "")).filter(Boolean);
  const allNodeIds = Array.from(new Set([
    ...graphNodeOrder,
    ...configuredNodeIds,
    ...intervalNodeIds,
    ...markerNodeIds,
  ]));
  const defaultVisibleRange = { startY: 0, endY: chartHeight, startMs: Number(bounds.startMs), endMs: Number(bounds.endMs) };
  const safeRange = Number(visibleRange?.endY || 0) > 0 ? visibleRange : defaultVisibleRange;
  const intervalIsVisible = (interval: AnyRecord): boolean => {
    return pipelineGanttIntervalTop(interval, bounds, chartHeight, ganttLayout) <= Number(safeRange.endY)
      && pipelineGanttIntervalBottom(interval, bounds, chartHeight, ganttLayout) >= Number(safeRange.startY);
  };
  const markerIsVisible = (marker: AnyRecord): boolean => {
    const y = pipelineGanttMarkerY(marker, bounds, chartHeight, ganttLayout);
    return y >= Number(safeRange.startY) && y <= Number(safeRange.endY);
  };
  const activeNodeIds = new Set(allNodeIds.filter((nodeId) =>
    intervals.some((interval: AnyRecord) => interval.nodeId === nodeId && intervalIsVisible(interval))
    || allMarkers.some((marker: AnyRecord) => marker.nodeId === nodeId && markerIsVisible(marker))));
  const visibleNodeIds = autoHideIdle ? allNodeIds.filter((nodeId) => activeNodeIds.has(nodeId)) : allNodeIds;
  const gridTemplateColumns = `${pipelineGanttTimeAxisWidth}px ${visibleNodeIds.length > 0 ? visibleNodeIds.map(() => `${pipelineGanttNodeColumnWidth}px`).join(" ") : "minmax(160px, 1fr)"}`;
  const ticks = asArray(ganttLayout.ticks).filter(isRecord);
  const selectedIntervalKey = String(selection?.mode === "interval" ? selection?.interval?.procedureRunId || "" : "");
  const selectedMarkerKey = String(selection?.mode === "event" ? selection?.marker?.id || "" : "");
  const updateVisibleRange = () => {
    const element = viewportRef.current as HTMLElement | null;
    if (!element) {
      setVisibleRange(defaultVisibleRange);
      return;
    }
    const topY = Math.max(0, element.scrollTop - pipelineGanttHeaderHeight);
    const visiblePx = Math.max(120, element.clientHeight - pipelineGanttHeaderHeight);
    const bottomY = Math.min(chartHeight, topY + visiblePx);
    const nextRange: AnyRecord = { startY: topY, endY: bottomY, startMs: Number(bounds.startMs), endMs: Number(bounds.endMs) };
    const startRatio = Math.max(0, Math.min(1, topY / chartHeight));
    const endRatio = Math.max(startRatio, Math.min(1, bottomY / chartHeight));
    const duration = Math.max(1, Number(bounds.endMs) - Number(bounds.startMs));
    nextRange.startMs = Number(bounds.startMs) + duration * startRatio;
    nextRange.endMs = Number(bounds.startMs) + duration * endRatio;
    setVisibleRange(nextRange);
  };
  useEffect(() => {
    const element = viewportRef.current as HTMLElement | null;
    const timer = window.setTimeout(updateVisibleRange, 0);
    element?.addEventListener("scroll", updateVisibleRange);
    window.addEventListener("resize", updateVisibleRange);
    return () => {
      window.clearTimeout(timer);
      element?.removeEventListener("scroll", updateVisibleRange);
      window.removeEventListener("resize", updateVisibleRange);
    };
  }, [activeRunId, bounds.startMs, bounds.endMs, chartHeight]);
  const hiddenCount = Math.max(0, allNodeIds.length - visibleNodeIds.length);
  const visibleMarkerIds = new Set(allMarkers.filter((marker: AnyRecord) =>
    visibleNodeIds.includes(String(marker.nodeId || "")) && markerIsVisible(marker)).map((marker: AnyRecord) => String(marker.id)));
  const markerById = new Map(allMarkers.map((marker: AnyRecord) => [String(marker.id), marker]));
  const visibleArrows = asArray(ganttSignals.arrows).filter((arrow: AnyRecord) => {
    const targetVisible = visibleMarkerIds.has(String(arrow.targetMarkerId || ""));
    if (!targetVisible) return false;
    if (String(arrow.action || "") === "observe") return visibleNodeIds.includes(String(arrow.sourceNodeId || ""));
    return visibleMarkerIds.has(String(arrow.sourceMarkerId || ""));
  });
  const boardMinWidth = pipelineGanttTimeAxisWidth + Math.max(1, visibleNodeIds.length) * pipelineGanttNodeColumnWidth;
  const setScaleFromSlider = (event: any) => {
    const nextScale = clampPipelineGanttScale(event.target.value);
    if (typeof onGanttScaleChange === "function") onGanttScaleChange(nextScale);
    window.setTimeout(updateVisibleRange, 0);
  };
  const exportCurrentGantt = () => exportPipelineGantt({
    title: `${activePipeline?.id || "pipeline"}-${activeRunId || "epoch"}-gantt`,
    meta: [
      `run ${activeRunId || "--"}`,
      `${fmtDate(bounds.startMs)} -> ${fmtDate(bounds.endMs)}`,
      `duration ${fmtDurationMs(bounds.durationMs)}`,
      `${timeScaleConfig.label} / ${pipelineDisplayPxPerMinute(timeScaleConfig.pxPerMinute)} px/min`,
      `${visibleNodeIds.length}/${allNodeIds.length} nodes`,
      `${allMarkers.length} markers`,
    ],
    visibleNodeIds,
    intervals,
    markers: allMarkers.filter((marker: AnyRecord) => visibleNodeIds.includes(String(marker.nodeId || ""))),
    arrows: visibleArrows,
    ticks,
    bounds,
    chartHeight,
    backendLayout: ganttLayout,
  });
  const diagnostics = isRecord(runDetailPayload?.gantt?.diagnostics) ? runDetailPayload.gantt.diagnostics : null;
  return h(Panel, {
    title: "Epoch 甘特图",
    eyebrow: `${activePipeline?.id || "pipeline"} / ${epochs.length} epochs`,
    className: "pipeline-wide-panel",
    loading: runDetails?.loading,
    actions: h("div", { className: "pipeline-gantt-actions" },
      h("select", {
        value: activeRunId,
        disabled: epochs.length === 0,
        onChange: (event: any) => onRunChange(event.target.value),
        "data-testid": "pipeline-epoch-select",
      }, epochs.map((run: any) => h("option", { key: run.runId, value: run.runId }, pipelineEpochLabel(epochs, run)))),
      h("label", { className: "pipeline-gantt-toggle" },
        h("input", {
          type: "checkbox",
          "data-testid": "pipeline-gantt-auto-hide-idle",
          checked: autoHideIdle,
          onChange: (event: any) => {
            setAutoHideIdle(Boolean(event.target.checked));
            window.setTimeout(updateVisibleRange, 0);
          },
        }),
        h("span", null, "自动隐藏空闲列"),
      ),
      h("label", { className: "pipeline-gantt-scale" },
        h("span", null,
          h("b", null, "时间尺度"),
          h("em", { "data-testid": "pipeline-gantt-scale-label" }, `${timeScaleConfig.label} · ${pipelineDisplayPxPerMinute(timeScaleConfig.pxPerMinute)} px/min`),
        ),
        h("input", {
          type: "range",
          min: 0,
          max: 100,
          step: 0.01,
          value: timeScale,
          onChange: setScaleFromSlider,
          "aria-label": "调整甘特图时间尺度",
          "data-testid": "pipeline-gantt-time-scale",
        }),
        h("small", null, h("span", null, "全局"), h("span", null, "细节")),
      ),
      activeRun ? h("button", {
        type: "button",
        className: "ghost-btn",
        onClick: exportCurrentGantt,
        disabled: visibleNodeIds.length === 0,
        "data-testid": "pipeline-export-gantt",
      }, "导出甘特图") : null,
      activeRun ? h(RawButton, { title: `Pipeline Epoch ${activeRun.runId}`, data: activeRun, onOpen: onRaw, testId: "raw-pipeline-epoch-gantt" }) : null,
    ),
  },
    !activeRun ? h(EmptyState, { title: "暂无 Epoch", text: "当前 pipeline 还没有完整运行记录。" }) :
      intervals.length === 0 ? h(EmptyState, { title: "暂无时间区间", text: "等待 D601 Pipeline backend 在 procedure summary 中返回 startedAt / finishedAt。" }) :
        h("div", { className: "pipeline-gantt-wrap" },
          h("div", {
            className: `pipeline-gantt-detail-layout ${ganttDetailOpen ? "detail-open" : "detail-collapsed"}`,
            "data-testid": "pipeline-gantt-detail-layout",
            "data-sidebar-open": ganttDetailOpen ? "true" : "false",
          },
            h("div", { className: "pipeline-gantt-main" },
              h("div", { className: "pipeline-gantt-main-head" },
                h("div", { className: "pipeline-gantt-meta" },
                  h("span", null, `time ${fmtDate(bounds.startMs)} -> ${fmtDate(bounds.endMs)}`),
                  h("span", null, `duration ${fmtDurationMs(bounds.durationMs)}`),
                  h("span", null, `scale ${timeScaleConfig.label} / ${pipelineDisplayPxPerMinute(timeScaleConfig.pxPerMinute)} px/min`),
                  h("span", null, `layout ${layoutSource}`),
                  diagnostics ? h("span", null, `align ${diagnostics.timeAxisAlignmentOk === false ? "check" : "ok"}`) : null,
                  h("span", null, `visible ${visibleNodeIds.length}/${allNodeIds.length} nodes`),
                  runDetailPayload ? h("span", null, `markers ${allMarkers.length}`) : null,
                  autoHideIdle && hiddenCount > 0 ? h("span", null, `hidden idle ${hiddenCount}`) : null,
                ),
                !ganttDetailOpen ? h("button", {
                  type: "button",
                  className: "pipeline-sidecar-tab right",
                  disabled: !selection?.mode,
                  onClick: () => setGanttDetailOpen(true),
                  "data-testid": "pipeline-gantt-sidebar-toggle",
                }, selection?.mode ? "展开详情" : "点击甘特图元素展开详情") : null,
              ),
              h("div", {
                className: "pipeline-gantt-viewport",
                ref: viewportRef,
                "data-testid": "pipeline-epoch-gantt",
                "data-pipeline-id": activePipeline?.id || "",
                "data-run-id": activeRunId,
                "data-layout-source": layoutSource,
                "data-start-ms": String(bounds.startMs),
                "data-end-ms": String(bounds.endMs),
                "data-chart-height": String(chartHeight),
              },
                h("div", { className: "pipeline-gantt-board", style: { gridTemplateColumns, minWidth: `${boardMinWidth}px` } },
                  h("div", { className: "pipeline-gantt-head time" }, "Time"),
                  visibleNodeIds.length === 0 ? h("div", { className: "pipeline-gantt-head empty" }, "当前时间窗无工作节点") :
                    visibleNodeIds.map((nodeId) => h("div", { key: `head-${nodeId}`, className: "pipeline-gantt-head node", title: nodeId, "data-testid": "pipeline-gantt-head-node", "data-node-id": nodeId }, h(GanttHeaderLabel, { value: nodeId }))),
                  h("div", { className: "pipeline-gantt-time-axis", style: { height: `${chartHeight}px` } },
                    ticks.map((tick: AnyRecord) => {
                      const tickY = pipelineGanttTickY(tick, bounds, chartHeight, ganttLayout);
                      return h("div", {
                        key: `tick-${tick.ms}-${tickY}`,
                        className: "pipeline-gantt-tick",
                        style: { top: `${tickY}px` },
                        "data-testid": "pipeline-gantt-tick",
                        "data-ms": String(tick.ms),
                        "data-y": String(tickY),
                      },
                        h("b", null, fmtDate(tick.ms)),
                        h("span", null, `+${fmtDurationMs(Number(tick.offsetMs ?? Number(tick.ms) - Number(bounds.startMs)))}`),
                      );
                    }),
                  ),
                  visibleNodeIds.length > 0 ? h("svg", {
                    className: "pipeline-gantt-arrow-layer",
                    width: visibleNodeIds.length * pipelineGanttNodeColumnWidth,
                    height: chartHeight,
                    viewBox: `0 0 ${visibleNodeIds.length * pipelineGanttNodeColumnWidth} ${chartHeight}`,
                    style: {
                      left: `${pipelineGanttTimeAxisWidth}px`,
                      top: `${pipelineGanttHeaderHeight}px`,
                      width: `${visibleNodeIds.length * pipelineGanttNodeColumnWidth}px`,
                      height: `${chartHeight}px`,
                    },
                    "aria-hidden": "true",
                  },
                    h("defs", null,
                      h("marker", {
                        id: "pipeline-gantt-arrowhead",
                        viewBox: "0 0 10 10",
                        refX: 9,
                        refY: 5,
                        markerWidth: 6,
                        markerHeight: 6,
                        orient: "auto-start-reverse",
                      },
                        h("path", { d: "M 0 0 L 10 5 L 0 10 z", fill: "context-stroke" }),
                      ),
                    ),
                    visibleArrows.map((arrow: AnyRecord) => {
                      const targetMarker = markerById.get(String(arrow.targetMarkerId || ""));
                      if (!targetMarker) return null;
                      const sourceMarker = markerById.get(String(arrow.sourceMarkerId || ""));
                      const sourceNodeId = String(sourceMarker?.nodeId || arrow.sourceNodeId || "");
                      const sourceIndex = visibleNodeIds.indexOf(sourceNodeId);
                      const targetIndex = visibleNodeIds.indexOf(String(targetMarker.nodeId || ""));
                      if (sourceIndex < 0 || targetIndex < 0) return null;
                      const sourceX = sourceIndex * pipelineGanttNodeColumnWidth + pipelineGanttNodeColumnWidth / 2;
                      const targetX = targetIndex * pipelineGanttNodeColumnWidth + pipelineGanttNodeColumnWidth / 2;
                      const sourceY = sourceMarker ? pipelineGanttMarkerY(sourceMarker, bounds, chartHeight, ganttLayout) : pipelineGanttMarkerY(targetMarker, bounds, chartHeight, ganttLayout);
                      const targetY = pipelineGanttMarkerY(targetMarker, bounds, chartHeight, ganttLayout);
                      return h("path", {
                        key: arrow.id,
                        className: `pipeline-gantt-arrow ${String(arrow.sourceKind || "").toLowerCase()} ${String(arrow.status || "").toLowerCase()} ${String(arrow.action || "").toLowerCase()}`,
                        d: pipelineControlArrowPath(sourceX, sourceY, targetX, targetY),
                        markerEnd: "url(#pipeline-gantt-arrowhead)",
                        "data-testid": String(arrow.action || "") === "observe" ? "pipeline-gantt-observation-arrow" : "pipeline-gantt-arrow",
                        "data-source-node-id": String(arrow.sourceNodeId || ""),
                        "data-target-node-id": String(arrow.targetNodeId || ""),
                        "data-target-marker-id": String(arrow.targetMarkerId || ""),
                        "data-action": String(arrow.action || ""),
                        "data-source-y": String(sourceY),
                        "data-target-y": String(targetY),
                      });
                    }),
                  ) : null,
                  visibleNodeIds.length === 0 ? h("div", { className: "pipeline-gantt-empty-col", style: { height: `${chartHeight}px` } }, "滚动到有活动的时间段后，相关 node 列会自动出现。") :
                    visibleNodeIds.map((nodeId) => {
                      const nodeIntervals = intervals.filter((interval: AnyRecord) => interval.nodeId === nodeId);
                      const nodeMarkers = allMarkers.filter((marker: AnyRecord) => String(marker.nodeId || "") === nodeId);
                      return h("div", { key: `col-${nodeId}`, className: "pipeline-gantt-node-col", style: { height: `${chartHeight}px` } },
                        nodeIntervals.map((interval: AnyRecord) => {
                          const top = pipelineGanttIntervalTop(interval, bounds, chartHeight, ganttLayout);
                          const bottom = pipelineGanttIntervalBottom(interval, bounds, chartHeight, ganttLayout);
                          const height = pipelineGanttIntervalHeight(interval, bounds, chartHeight, ganttLayout);
                          const intervalKey = String(interval.procedureRunId || `${nodeId}-${interval.startMs}`);
                          return h("button", {
                            key: intervalKey,
                            type: "button",
                            className: `pipeline-gantt-bar ${interval.status} ${interval.live ? "live" : ""} ${selectedIntervalKey === intervalKey ? "selected" : ""}`,
                            style: { top: `${top}px`, height: `${height}px` },
                            title: `${nodeId} ${interval.status} ${fmtDate(interval.startedAt || interval.startMs)} -> ${fmtDate(interval.finishedAt || interval.endMs)}`,
                            onClick: () => onIntervalSelect(interval),
                            "data-testid": "pipeline-gantt-line",
                            "data-node-id": nodeId,
                            "data-procedure-run-id": String(interval.procedureRunId || ""),
                            "data-status": String(interval.status || ""),
                            "data-live": interval.live ? "true" : "false",
                            "data-start-ms": String(interval.startMs || ""),
                            "data-end-ms": String(interval.endMs || ""),
                            "data-y1": String(top),
                            "data-y2": String(bottom),
                            "data-natural-height": String(Math.max(0, bottom - top)),
                          },
                            h("strong", null, interval.status || "working"),
                            h("span", null, fmtDurationMs(interval.durationMs)),
                          );
                        }),
                        nodeMarkers.map((marker: AnyRecord) => h("button", {
                          key: marker.id,
                          type: "button",
                          className: `pipeline-gantt-marker ${marker.kind} ${marker.tone || ""} ${marker.status || ""} ${selectedMarkerKey === String(marker.id) ? "selected" : ""}`,
                          style: { top: `${pipelineGanttMarkerY(marker, bounds, chartHeight, ganttLayout)}px` },
                          title: `${marker.label || "event"} / ${fmtDate(marker.timestampIso || marker.timestamp || marker.ms)}`,
                          onClick: () => onMarkerSelect(marker),
                          "data-testid": marker.kind === "prompt" ? "pipeline-gantt-prompt-marker" : "pipeline-gantt-control-marker",
                          "data-marker-id": String(marker.id || ""),
                          "data-ms": String(marker.ms ?? marker.eventMs ?? ""),
                          "data-y": String(pipelineGanttMarkerY(marker, bounds, chartHeight, ganttLayout)),
                        })),
                      );
                    }),
                ),
              ),
            ),
            ganttDetailOpen ? h(PipelineGanttDetailPanel, { selection, runDetails, nodeDetails, nodeDetailsState, onRaw, onCollapse: () => setGanttDetailOpen(false) }) : null,
          ),
        ),
  );
}

function pipelineNodeControlState(): AnyRecord {
  return {
    loading: false,
    actionLoading: "",
    error: "",
    message: "",
    details: null,
    fetchedAt: null,
    appendPrompt: "",
    guidePrompt: "",
    modifyPrompt: "",
    approveReason: "",
    redoReason: "",
  };
}

function pipelineGanttSelectionState(): AnyRecord {
  return {
    mode: "",
    runId: "",
    interval: null,
    marker: null,
  };
}

function pipelineRunDetailsState(): AnyRecord {
  return {
    runId: "",
    loading: false,
    error: "",
    details: null,
    fetchedAt: null,
  };
}

function pipelineProxyPath(apiBaseUrl: string, path: string): string {
  return `${apiBaseUrl}/microservices/pipeline/proxy${path}`;
}

function PipelineNodeControlPanel({ activeRun, pipelineRuns, selectedRunId, onRunChange, selectedNodeId, selectedNodeConfig, selectedNodeRuntime, control, onControlChange, onFetch, onAction, onRaw, onCollapse }: AnyRecord) {
  const runId = String(activeRun?.runId || "");
  const status = String(selectedNodeRuntime?.status || "pending");
  const disabled = !runId || !selectedNodeId || control.loading || Boolean(control.actionLoading);
  const updateText = (field: string) => (event: any) => onControlChange({ [field]: event.target.value, error: "", message: "" });
  const runOptions = pipelineRuns.length > 0 ? pipelineRuns : (activeRun ? [activeRun] : []);
  return h("aside", { className: "pipeline-node-control", "data-testid": "pipeline-node-control" },
    h("div", { className: "pipeline-node-control-head" },
      h("div", null,
        h("p", { className: "panel-eyebrow" }, "Manual Node Control"),
        h(LoadingTitle, { title: selectedNodeId || "点击控制图中的 node", level: 3, loading: control.loading || Boolean(control.actionLoading) }),
      ),
      h("div", { className: "pipeline-node-control-head-actions" },
        selectedNodeId ? h(StatusBadge, { status }, status) : h(StatusBadge, { status: "pending" }, "idle"),
        h("button", { type: "button", className: "ghost-btn mini", onClick: onCollapse, "data-testid": "pipeline-node-sidebar-collapse" }, "收起"),
      ),
    ),
    h("div", { className: "pipeline-control-runbar" },
      h("label", null,
        h("span", null, "目标 run"),
        h("select", {
          value: runId || selectedRunId,
          disabled: runOptions.length === 0,
          onChange: (event: any) => onRunChange(event.target.value),
          "data-testid": "pipeline-node-run-select",
        }, runOptions.map((run: any) => h("option", { key: run.runId, value: run.runId }, `${run.runId || "--"} / ${run.status || "--"}`))),
      ),
      h("button", {
        type: "button",
        className: "ghost-btn",
        disabled,
        onClick: onFetch,
        "data-testid": "pipeline-node-fetch",
      }, control.loading ? "抓取中" : "抓取过程"),
      control.details ? h(RawButton, { title: `Pipeline Node ${selectedNodeId}`, data: control.details, onOpen: onRaw, testId: "raw-pipeline-node-control" }) : null,
    ),
    h("div", { className: "pipeline-control-meta" },
      h("span", null, h("b", null, "kind"), String(selectedNodeConfig?.kind || "--")),
      h("span", null, h("b", null, "procedure"), String(selectedNodeRuntime?.currentProcedureRunId || "--")),
      h("span", null, h("b", null, "attempts"), String(selectedNodeRuntime?.attempts ?? "--")),
      h("span", null, h("b", null, "updated"), fmtDate(activeRun?.updatedAt)),
    ),
    !selectedNodeId ? h(EmptyState, { title: "未选择 node", text: "点击 React Flow 控制图中的任意 node 后，可抓取执行过程、追加 prompt、下发引导、增量修改、审核通过或重做。" }) : null,
    h(UniDeskErrorBanner, { error: control.error, wide: true }),
    h("div", { className: "pipeline-control-actions" },
      h("label", null,
        h("span", null, "实时追加 prompt（仅 running node）"),
        h("textarea", { value: control.appendPrompt, onChange: updateText("appendPrompt"), placeholder: "让当前执行中的 agent 继续、补充检查或调整当前步骤...", rows: 4, disabled: !selectedNodeId, "data-testid": "pipeline-node-append-input" }),
        h("button", {
          type: "button",
          className: "primary-btn compact",
          disabled: disabled || !String(control.appendPrompt || "").trim(),
          onClick: () => onAction("append"),
          "data-testid": "pipeline-node-append-button",
        }, control.actionLoading === "append" ? "追加中" : "追加到运行中 node"),
      ),
      h("label", null,
        h("span", null, "下次尝试引导 prompt"),
        h("textarea", { value: control.guidePrompt, onChange: updateText("guidePrompt"), placeholder: "给该 node 下一次 attempt 的执行提示；不会立即打断当前 session。", rows: 4, disabled: !selectedNodeId, "data-testid": "pipeline-node-guide-input" }),
        h("button", {
          type: "button",
          className: "ghost-btn compact",
          disabled: disabled || !String(control.guidePrompt || "").trim(),
          onClick: () => onAction("guide"),
          "data-testid": "pipeline-node-guide-button",
        }, control.actionLoading === "guide" ? "下发中" : "下发 guide"),
      ),
      h("label", null,
        h("span", null, "完成后增量修改 prompt"),
        h("textarea", { value: control.modifyPrompt, onChange: updateText("modifyPrompt"), placeholder: "在该 node 已完成结果基础上追加修改要求；runner 会重跑目标 node，并保留同 node 既有 OA 输出作为上下文。", rows: 4, disabled: !selectedNodeId, "data-testid": "pipeline-node-modify-input" }),
        h("button", {
          type: "button",
          className: "ghost-btn compact",
          disabled: disabled || !String(control.modifyPrompt || "").trim(),
          onClick: () => onAction("modify"),
          "data-testid": "pipeline-node-modify-button",
        }, control.actionLoading === "modify" ? "排队中" : "增量修改 node"),
      ),
      h("label", null,
        h("span", null, "Monitor 审核通过原因"),
        h("textarea", { value: control.approveReason, onChange: updateText("approveReason"), placeholder: "当流程配置开启 monitor 审核时，记录审核通过原因并释放后续 node。", rows: 3, disabled: !selectedNodeId, "data-testid": "pipeline-node-approve-input" }),
        h("button", {
          type: "button",
          className: "primary-btn compact",
          disabled: disabled || !String(control.approveReason || "").trim(),
          onClick: () => onAction("approve"),
          "data-testid": "pipeline-node-approve-button",
        }, control.actionLoading === "approve" ? "提交中" : "审核通过"),
      ),
      h("label", null,
        h("span", null, "重做 / restart 原因"),
        h("textarea", { value: control.redoReason, onChange: updateText("redoReason"), placeholder: "说明为什么需要重做；runner 会重置目标 node 以及非 rework 下游 node。", rows: 4, disabled: !selectedNodeId, "data-testid": "pipeline-node-redo-input" }),
        h("button", {
          type: "button",
          className: "danger-btn compact",
          disabled: disabled || !String(control.redoReason || "").trim(),
          onClick: () => onAction("redo"),
          "data-testid": "pipeline-node-redo-button",
        }, control.actionLoading === "redo" ? "排队中" : "重做 node"),
      ),
    ),
    h("div", { className: "pipeline-control-evidence" },
      h("strong", null, "Node 过程索引"),
      h(PipelineNodeExecutionIndex, {
        details: control.details,
        selectedNodeId,
        selectedNodeRuntime,
        control,
        onRaw,
      }),
    ),
  );
}

export function PipelinePage({ microservices, onRaw, apiBaseUrl = "/api" }: AnyRecord) {
  const service = microservices.find((item: any) => item.id === "pipeline") || null;
  const [state, setState] = useState({ loading: false, error: "", health: null, snapshot: null, oaDiagnostics: null, minimaxQuota: null, refreshedAt: null });
  const [selectedPipelineId, setSelectedPipelineId] = useState("");
  const [selectedRunId, setSelectedRunId] = useState("");
  const [selectedNodeId, setSelectedNodeId] = useState("");
  const [nodeControl, setNodeControl] = useState(pipelineNodeControlState());
  const [ganttNodeDetails, setGanttNodeDetails] = useState({});
  const [ganttSelection, setGanttSelection] = useState(pipelineGanttSelectionState());
  const [runDetails, setRunDetails] = useState(pipelineRunDetailsState());
  const [ganttScale, setGanttScale] = useState(pipelineDefaultGanttScale);
  const [nodeControlOpen, setNodeControlOpen] = useState(false);
  const [ganttDetailOpen, setGanttDetailOpen] = useState(false);
  const loadRequestRef = useRef(0);
  const { addNotification } = useNotification();
  const loadInFlightRef = useRef(false);
  const runDetailsRequestRef = useRef(0);
  const runDetailsInFlightRef = useRef("");
  const ganttNodeDetailsRequestRef = useRef({});
  const selectedNodeIdRef = useRef("");
  const activeRunIdRef = useRef("");

  async function load(options: AnyRecord = {}): Promise<void> {
    const silent = options.silent === true;
    if (!service) return;
    if (loadInFlightRef.current) return;
    loadInFlightRef.current = true;
    const requestId = loadRequestRef.current + 1;
    loadRequestRef.current = requestId;
    if (!silent) setState((prev: any) => ({ ...prev, loading: true, error: "" }));
    try {
      const snapshotQuery = `__unideskArrayLimit=registry.components:80,runs:${pipelineSnapshotRunLimit}`;
      const [snapshot, oaDiagnostics, minimaxQuota] = await Promise.all([
        requestJson(`${apiBaseUrl}/microservices/pipeline/proxy/api/snapshot?${snapshotQuery}`, { cache: "no-store" }),
        requestJson(`${apiBaseUrl}/microservices/pipeline/proxy/api/oa-event-flow/diagnostics`, { cache: "no-store" })
          .catch((error: unknown) => ({ ok: false, error: errorMessage(error, "OA event flow diagnostics failed") })),
        requestJson(`${apiBaseUrl}/microservices/pipeline/proxy/api/model-quota/minimax`, { cache: "no-store" })
          .catch((error: unknown) => ({ ok: false, error: errorMessage(error, "MiniMax quota failed") })),
      ]);
      if (requestId !== loadRequestRef.current) return;
      const health = { ok: snapshot?.ok !== false, service: "pipeline-v2-control snapshot" };
      setState({ loading: false, error: "", health, snapshot, oaDiagnostics, minimaxQuota, refreshedAt: new Date() });
    } catch (err) {
      if (requestId !== loadRequestRef.current) return;
      setState((prev: any) => ({ ...prev, loading: false, error: errorMessage(err, "Pipeline 加载失败") }));
    } finally {
      loadInFlightRef.current = false;
    }
  }

  useEffect(() => {
    load();
    if (!service) return undefined;
    const tick = (): void => {
      if (isDocumentVisible()) load({ silent: true });
    };
    const timer = window.setInterval(() => {
      tick();
    }, pipelineAutoRefreshMs);
    const onVisible = (): void => {
      if (isDocumentVisible()) tick();
    };
    document.addEventListener("visibilitychange", onVisible);
    return () => {
      window.clearInterval(timer);
      document.removeEventListener("visibilitychange", onVisible);
    };
  }, [service?.id, service?.runtime?.providerStatus, apiBaseUrl]);

  const runtime = microserviceRuntime(service);
  const repository = microserviceRepository(service);
  const backend = microserviceBackend(service);
  const snapshot = state.snapshot || {};
  const oaDiagnostics = state.oaDiagnostics || null;
  const minimaxQuota = state.minimaxQuota || null;
  const { components, pipelines, runs } = pipelineSnapshotArrays(snapshot);
  const latestPipelineId = String(runs[0]?.pipelineId || "");
  const defaultPipeline = (latestPipelineId ? pipelines.find((pipeline: any) => String(pipeline.id || "") === latestPipelineId) : null) || pipelines[0] || {};
  const activePipeline = pipelines.find((pipeline: any) => String(pipeline.id || "") === selectedPipelineId) || defaultPipeline;
  const activePipelineId = String(activePipeline.id || "");
  const pipelineNodes = pipelineConfigNodes(activePipeline);
  const pipelineEdges = pipelineConfigEdges(activePipeline);
  const latestRun = pipelineLatestRun(runs, activePipelineId);
  const pipelineRuns = pipelineEpochRuns(runs, activePipelineId);
  const activeRunBase = pipelineRuns.find((run: any) => String(run?.runId || "") === selectedRunId) || latestRun;
  const activeRunDetail = String(runDetails.runId || "") === String(activeRunBase?.runId || "") ? pipelineDetailedRun(runDetails.details) : null;
  const activeRun = mergePipelineRunSummary(activeRunBase, activeRunDetail);
  const activeRunId = String(activeRun?.runId || "");
  const selectedNodeConfig = pipelineNodes.find((node: any) => String(node?.id || "") === selectedNodeId) || null;
  const selectedNodeRuntime = selectedNodeId ? pipelineRunNodeRecord(activeRun, selectedNodeId) : null;
  const statusCounts = pipelineStatusCounts(runs);
  const componentClasses = pipelineComponentClassCounts(components);
  const componentCount = Number(state.health?.components) || pipelineArrayCount(snapshot, "registry.components", components.length);
  const runCount = pipelineArrayCount(snapshot, "runs", runs.length);
  const baseFlow = pipelineFlowElements(activePipeline, activeRun, components);
  const flow = {
    nodes: baseFlow.nodes.map((node) => node.id === selectedNodeId ? { ...node, selected: true, className: `${node.className || ""} selected-control-node` } : node),
    edges: baseFlow.edges,
  };
  const exportItems = pipelines.map((pipeline: any) => {
    const pipelineId = String(pipeline.id || "pipeline");
    const run = pipelineLatestRun(runs, pipelineId);
    return {
      title: `${pipelineId}-${run?.runId || "snapshot"}`,
      flow: pipelineFlowElements(pipeline, run, components),
    };
  });
  const ganttSelectionRunId = String(ganttSelection?.runId || activeRunId || "");
  const ganttSelectionNodeId = String(ganttSelection?.interval?.nodeId || ganttSelection?.marker?.nodeId || "");
  const ganttNodeDetailState = ganttSelectionRunId && ganttSelectionNodeId
    ? (ganttNodeDetails as AnyRecord)[pipelineNodeDetailKey(ganttSelectionRunId, ganttSelectionNodeId)] || null
    : null;
  const exactNodeControlDetails = pipelineFocusedNodeDetails(nodeControl.details, ganttSelectionRunId, ganttSelectionNodeId);
  const focusedGanttNodeDetails = pipelineFocusedNodeDetails(ganttNodeDetailState?.details, ganttSelectionRunId, ganttSelectionNodeId) || exactNodeControlDetails;
  const focusedGanttNodeState = ganttSelectionRunId && ganttSelectionNodeId
    ? {
      ...(isRecord(ganttNodeDetailState) ? ganttNodeDetailState : {}),
      runId: ganttSelectionRunId,
      nodeId: ganttSelectionNodeId,
      details: focusedGanttNodeDetails,
      loading: Boolean(ganttNodeDetailState?.loading) || (!focusedGanttNodeDetails && Boolean(nodeControl.loading) && selectedNodeId === ganttSelectionNodeId),
      error: String(ganttNodeDetailState?.error || ""),
      fetchedAt: ganttNodeDetailState?.fetchedAt || (exactNodeControlDetails ? nodeControl.fetchedAt : null),
    }
    : null;
  const pipelineRunIds = pipelineRuns.map((run: any) => String(run?.runId || "")).filter(Boolean).join("|");
  const pipelineNodeIds = pipelineNodes.map((node: any) => String(node?.id || "")).filter(Boolean).join("|");

  useEffect(() => {
    selectedNodeIdRef.current = selectedNodeId;
  }, [selectedNodeId]);

  useEffect(() => {
    activeRunIdRef.current = activeRunId;
  }, [activeRunId]);

  useEffect(() => {
    if (!selectedRunId || pipelineRunIds.split("|").includes(selectedRunId)) return;
    setSelectedRunId("");
  }, [selectedRunId, pipelineRunIds]);

  useEffect(() => {
    if (!selectedNodeId || pipelineNodeIds.split("|").includes(selectedNodeId)) return;
    setSelectedNodeId("");
    setNodeControl(pipelineNodeControlState());
    setGanttSelection(pipelineGanttSelectionState());
    setNodeControlOpen(false);
    setGanttDetailOpen(false);
  }, [selectedNodeId, pipelineNodeIds]);

  useEffect(() => {
    if (!selectedNodeId) setNodeControlOpen(false);
  }, [selectedNodeId]);

  useEffect(() => {
    if (!ganttSelection.mode) setGanttDetailOpen(false);
  }, [ganttSelection.mode]);

  async function fetchRunDetails(runId = activeRunId, options: AnyRecord = {}): Promise<void> {
    if (!runId) {
      setRunDetails(pipelineRunDetailsState());
      return;
    }
    const scale = clampPipelineGanttScale(options.scale ?? ganttScale ?? pipelineDefaultGanttScale);
    const requestKey = `${runId}:timeline`;
    if (runDetailsInFlightRef.current === requestKey) return;
    runDetailsInFlightRef.current = requestKey;
    const silent = options.silent === true;
    const requestId = runDetailsRequestRef.current + 1;
    runDetailsRequestRef.current = requestId;
    setRunDetails((prev: AnyRecord) => ({
      runId,
      scale,
      loading: !silent || String(prev.runId || "") !== runId || !prev.details,
      error: "",
      details: silent && prev.runId === runId ? prev.details : prev.runId === runId ? prev.details : null,
      fetchedAt: prev.runId === runId ? prev.fetchedAt : null,
    }));
    try {
      const [details, runSummary] = await Promise.all([
        requestJson(pipelineProxyPath(apiBaseUrl, `/api/node-control/runs/${encodeURIComponent(runId)}?tail=160&view=timeline`), { cache: "no-store", strictJson: true }),
        requestJson(pipelineProxyPath(apiBaseUrl, `/api/runs/${encodeURIComponent(runId)}`), { cache: "no-store" }).catch((error: unknown) => ({ ok: false, runSummaryError: errorMessage(error, "抓取评分失败") })),
      ]);
      if (requestId !== runDetailsRequestRef.current) return;
      setRunDetails({ runId, scale, loading: false, error: "", details: { ...details, run: isRecord(runSummary?.run) ? runSummary.run : undefined, runSummaryError: runSummary?.runSummaryError }, fetchedAt: new Date() });
    } catch (err) {
      if (requestId !== runDetailsRequestRef.current) return;
      setRunDetails((prev: AnyRecord) => ({
        runId,
        scale,
        loading: false,
        error: errorMessage(err, "抓取 epoch 执行过程失败"),
        details: prev.runId === runId ? prev.details : null,
        fetchedAt: prev.runId === runId ? prev.fetchedAt : null,
      }));
    } finally {
      if (runDetailsInFlightRef.current === requestKey) runDetailsInFlightRef.current = "";
    }
  }

  function setGanttNodeDetailEntry(runId: string, nodeId: string, patch: AnyRecord): void {
    const key = pipelineNodeDetailKey(runId, nodeId);
    setGanttNodeDetails((prev: AnyRecord) => {
      const next: AnyRecord = {
        ...prev,
        [key]: {
          ...(isRecord(prev?.[key]) ? prev[key] : {}),
          runId,
          nodeId,
          ...patch,
        },
      };
      const keys = Object.keys(next);
      if (keys.length > 32) {
        for (const staleKey of keys.slice(0, keys.length - 32)) delete next[staleKey];
      }
      return next;
    });
  }

  async function fetchGanttNodeDetails(runId: string, nodeId: string): Promise<void> {
    if (!runId || !nodeId) return;
    const key = pipelineNodeDetailKey(runId, nodeId);
    const requestId = Number(ganttNodeDetailsRequestRef.current?.[key] || 0) + 1;
    ganttNodeDetailsRequestRef.current = { ...ganttNodeDetailsRequestRef.current, [key]: requestId };
    setGanttNodeDetailEntry(runId, nodeId, { loading: true, error: "" });
    try {
      const details = await requestJson(pipelineProxyPath(apiBaseUrl, `/api/node-control/runs/${encodeURIComponent(runId)}/nodes/${encodeURIComponent(nodeId)}?tail=160`), { cache: "no-store", strictJson: true });
      if (Number(ganttNodeDetailsRequestRef.current?.[key] || 0) !== requestId) return;
      const fetchedAt = new Date();
      setGanttNodeDetailEntry(runId, nodeId, { loading: false, details, fetchedAt, error: "" });
      if (selectedNodeIdRef.current === nodeId && activeRunIdRef.current === runId) {
        setNodeControl((prev: AnyRecord) => ({ ...prev, loading: false, details, fetchedAt, error: "" }));
      }
    } catch (err) {
      if (Number(ganttNodeDetailsRequestRef.current?.[key] || 0) !== requestId) return;
      setGanttNodeDetailEntry(runId, nodeId, { loading: false, error: errorMessage(err, "抓取 Gantt node 详情失败") });
    }
  }

  useEffect(() => {
    if (!activeRunId) {
      setRunDetails(pipelineRunDetailsState());
      return undefined;
    }
    void fetchRunDetails(activeRunId);
    const tick = (): void => {
      if (isDocumentVisible()) void fetchRunDetails(activeRunId, { silent: true });
    };
    const timer = window.setInterval(() => {
      tick();
    }, pipelineAutoRefreshMs);
    const onVisible = (): void => {
      if (isDocumentVisible()) tick();
    };
    document.addEventListener("visibilitychange", onVisible);
    return () => {
      window.clearInterval(timer);
      document.removeEventListener("visibilitychange", onVisible);
    };
  }, [activeRunId, apiBaseUrl]);

  async function fetchNodeDetails(runId = activeRunId, nodeId = selectedNodeId): Promise<void> {
    if (!runId || !nodeId) {
      setNodeControl((prev: AnyRecord) => ({ ...prev, error: "请先选择 run 和 node", message: "" }));
      return;
    }
    setNodeControl((prev: AnyRecord) => ({ ...prev, loading: true, error: "", message: "" }));
    try {
      const details = await requestJson(pipelineProxyPath(apiBaseUrl, `/api/node-control/runs/${encodeURIComponent(runId)}/nodes/${encodeURIComponent(nodeId)}?tail=160`), { cache: "no-store", strictJson: true });
      const fetchedAt = new Date();
      setNodeControl((prev: AnyRecord) => ({ ...prev, loading: false, details, fetchedAt, error: "" }));
      setGanttNodeDetailEntry(runId, nodeId, { loading: false, details, fetchedAt, error: "" });
    } catch (err) {
      setNodeControl((prev: AnyRecord) => ({ ...prev, loading: false, error: errorMessage(err, "抓取 node 执行过程失败") }));
    }
  }

  async function selectGanttInterval(interval: AnyRecord): Promise<void> {
    const runId = String(interval?.runId || activeRunId || "");
    const nodeId = String(interval?.nodeId || "");
    setGanttSelection({ mode: "interval", runId, interval, marker: null });
    setGanttDetailOpen(true);
    if (!runId || !nodeId) return;
    if (runId !== activeRunId) setSelectedRunId(runId);
    setSelectedNodeId(nodeId);
    setNodeControl(pipelineNodeControlState());
    void fetchRunDetails(runId, { silent: true });
    void fetchGanttNodeDetails(runId, nodeId);
  }

  async function selectGanttMarker(marker: AnyRecord): Promise<void> {
    const runId = String(marker?.runId || activeRunId || "");
    const nodeId = String(marker?.nodeId || "");
    setGanttSelection({ mode: "event", runId, interval: null, marker });
    setGanttDetailOpen(true);
    if (!runId) return;
    if (runId !== activeRunId) setSelectedRunId(runId);
    void fetchRunDetails(runId, { silent: true });
    if (!nodeId) return;
    setSelectedNodeId(nodeId);
    setNodeControl(pipelineNodeControlState());
    void fetchGanttNodeDetails(runId, nodeId);
  }

  async function postNodeAction(action: "append" | "guide" | "modify" | "approve" | "redo"): Promise<void> {
    if (!activeRunId || !selectedNodeId) {
      setNodeControl((prev: AnyRecord) => ({ ...prev, error: "请先选择 run 和 node", message: "" }));
      return;
    }
    const endpoint = action === "append" ? "prompts" : action;
    const text =
      action === "append" ? nodeControl.appendPrompt :
      action === "guide" ? nodeControl.guidePrompt :
      action === "modify" ? nodeControl.modifyPrompt :
      action === "approve" ? nodeControl.approveReason :
      nodeControl.redoReason;
    if (!String(text || "").trim()) {
      setNodeControl((prev: AnyRecord) => ({ ...prev, error: "操作内容不能为空", message: "" }));
      return;
    }
    setNodeControl((prev: AnyRecord) => ({ ...prev, actionLoading: action, error: "", message: "" }));
    try {
      const body = action === "redo" || action === "approve"
        ? { reason: text, source: "unidesk-frontend", sourceKind: "webui" }
        : { prompt: text, source: "unidesk-frontend", sourceKind: "webui" };
      const result = await requestJson(pipelineProxyPath(apiBaseUrl, `/api/node-control/runs/${encodeURIComponent(activeRunId)}/nodes/${encodeURIComponent(selectedNodeId)}/${endpoint}`), {
        method: "POST",
        body: JSON.stringify(body),
      });
      setNodeControl((prev: AnyRecord) => ({
        ...prev,
        actionLoading: "",
        details: result,
        fetchedAt: new Date(),
        appendPrompt: action === "append" ? "" : prev.appendPrompt,
        guidePrompt: action === "guide" ? "" : prev.guidePrompt,
        modifyPrompt: action === "modify" ? "" : prev.modifyPrompt,
        approveReason: action === "approve" ? "" : prev.approveReason,
        redoReason: action === "redo" ? "" : prev.redoReason,
        message:
          action === "append" ? "已追加到运行中 node" :
          action === "guide" ? "已下发 guide，等待 runner 处理" :
          action === "modify" ? "已排队增量修改命令" :
          action === "approve" ? "已提交审核通过决策" :
          "已排队重做命令",
      }));
      const msg = action === "append" ? "已追加到运行中 node" : action === "guide" ? "已下发 guide，等待 runner 处理" : action === "modify" ? "已排队增量修改命令" : action === "approve" ? "已提交审核通过决策" : "已排队重做命令";
      addNotification("success", msg);
      await fetchNodeDetails(activeRunId, selectedNodeId);
      await fetchRunDetails(activeRunId, { silent: true });
      if (action !== "append") await load();
    } catch (err) {
      setNodeControl((prev: AnyRecord) => ({ ...prev, actionLoading: "", error: errorMessage(err, "node 控制操作失败") }));
    }
  }

  if (!service) return h(EmptyState, { title: "Pipeline 未登记", text: "请在 config.json 的 microservices 中登记用户服务 id=pipeline" });

  return h("div", { className: "pipeline-page", "data-testid": "pipeline-page" },
    h(Panel, {
      title: "Pipeline v2 工作台",
      eyebrow: "D601 Snapshot 用户服务",
      loading: state.loading,
      actions: h("div", { className: "panel-actions" },
        h("button", { type: "button", className: "ghost-btn", onClick: load, disabled: state.loading, "data-testid": "pipeline-refresh-button" }, state.loading ? "刷新中" : "刷新"),
        h(RawButton, { title: "Pipeline 用户服务", data: service, onOpen: onRaw, testId: "raw-pipeline-service" }),
      ),
    },
      h("div", { className: "pipeline-hero" },
        h("div", null,
          h("div", { className: "node-version-line" },
            h(StatusBadge, { status: runtime.providerStatus === "online" ? "online" : "warn" }, runtime.providerStatus || "unknown"),
            h("span", null, service.providerId),
            h("span", null, backend.public ? "公网暴露" : "仅 UniDesk frontend 代理访问"),
          ),
          h("p", { className: "muted paragraph" }, service.description),
        ),
        h("div", { className: "microservice-ref-card" },
          h("span", null, "Repo"),
          h("strong", null, repository.url || "--"),
          h("code", null, repository.commitId || "--"),
        ),
        h("div", { className: "microservice-ref-card" },
          h("span", null, "D601 Docker"),
          h("strong", null, `${backend.nodeBindHost || "--"}:${backend.nodePort || "--"}`),
          h("code", null, `${repository.composeFile || "--"} / ${repository.composeService || "--"}`),
        ),
      ),
      h(UniDeskErrorBanner, { error: state.error, wide: true }),
    ),
    h("div", { className: "pipeline-grid" },
      h(Panel, {
        title: "控制图",
        eyebrow: `${activePipeline.id || "pipeline"} / run ${activeRun?.status || "--"}`,
        className: "pipeline-wide-panel",
        loading: state.loading,
        actions: h("div", { className: "pipeline-toolbar" },
          h("select", {
            value: activePipelineId,
            disabled: pipelines.length === 0,
            onChange: (event: any) => {
              setSelectedPipelineId(event.target.value);
              setSelectedRunId("");
              setSelectedNodeId("");
              setNodeControl(pipelineNodeControlState());
              setGanttSelection(pipelineGanttSelectionState());
              setNodeControlOpen(false);
              setGanttDetailOpen(false);
            },
            "data-testid": "pipeline-select",
          }, pipelines.map((pipeline: any) => h("option", { key: pipeline.id, value: pipeline.id }, pipeline.id || pipeline.key))),
          h("select", {
            value: activeRunId,
            disabled: pipelineRuns.length === 0,
            onChange: (event: any) => {
              setSelectedRunId(event.target.value);
              setNodeControl(pipelineNodeControlState());
              setGanttSelection(pipelineGanttSelectionState());
              setNodeControlOpen(false);
              setGanttDetailOpen(false);
              if (selectedNodeId) void fetchNodeDetails(event.target.value, selectedNodeId);
            },
            "data-testid": "pipeline-run-select",
          }, pipelineRuns.map((run: any) => h("option", { key: run.runId, value: run.runId }, pipelineEpochLabel(pipelineRuns, run)))),
          h("button", {
            type: "button",
            className: "ghost-btn",
            disabled: flow.nodes.length === 0,
            onClick: () => exportPipelineGraph(flow, `${activePipeline.id || "pipeline"}-${activeRun?.runId || "snapshot"}`),
            "data-testid": "pipeline-export-graph",
          }, "导出渲染图"),
          h("button", {
            type: "button",
            className: "ghost-btn",
            disabled: exportItems.every((item) => item.flow.nodes.length === 0),
            onClick: () => exportPipelineGraphs(exportItems),
            "data-testid": "pipeline-export-all-graphs",
          }, "批量导出"),
        ),
      },
        pipelineNodes.length === 0 ? h(EmptyState, { title: "暂无控制图", text: "等待 D601 pipeline backend 返回 config.nodes / config.edges" }) :
          h("div", {
            className: `pipeline-control-shell ${nodeControlOpen ? "detail-open" : "detail-collapsed"}`,
            "data-testid": "pipeline-control-shell",
            "data-sidebar-open": nodeControlOpen ? "true" : "false",
          },
            h("div", { className: "pipeline-flow-frame", "data-testid": "pipeline-react-flow" },
              h(ReactFlow, {
                nodes: flow.nodes,
                edges: flow.edges,
                nodeTypes: pipelineNodeTypes,
                edgeTypes: pipelineEdgeTypes,
                fitView: true,
                fitViewOptions: { padding: 0.18 },
                nodesDraggable: false,
                nodesConnectable: false,
                elementsSelectable: true,
                minZoom: 0.25,
                maxZoom: 1.4,
                proOptions: { hideAttribution: true },
                onNodeClick: (_event: any, node: Node) => {
                  const nodeId = String(node.id);
                  setSelectedNodeId(nodeId);
                  setNodeControl(pipelineNodeControlState());
                  setNodeControlOpen(true);
                  if (activeRunId) void fetchNodeDetails(activeRunId, nodeId);
                },
              },
                h(Background, { gap: 22, size: 1, color: "rgba(215, 161, 58, 0.24)" }),
                h(Controls, { showInteractive: false }),
              ),
              !nodeControlOpen ? h("button", {
                type: "button",
                className: "pipeline-sidecar-tab right",
                disabled: !selectedNodeId,
                onClick: () => setNodeControlOpen(true),
                "data-testid": "pipeline-node-sidebar-toggle",
              }, selectedNodeId ? "展开 node 控制" : "点击 node 展开控制") : null,
            ),
            nodeControlOpen ? h(PipelineNodeControlPanel, {
              activeRun,
              pipelineRuns,
              selectedRunId,
              onRunChange: (runId: string) => {
                setSelectedRunId(runId);
                setNodeControl(pipelineNodeControlState());
                setGanttSelection(pipelineGanttSelectionState());
                if (selectedNodeId) void fetchNodeDetails(runId, selectedNodeId);
              },
              selectedNodeId,
              selectedNodeConfig,
              selectedNodeRuntime,
              control: nodeControl,
              onControlChange: (patch: AnyRecord) => setNodeControl((prev: AnyRecord) => ({ ...prev, ...patch })),
              onFetch: () => fetchNodeDetails(),
              onAction: postNodeAction,
              onRaw,
              onCollapse: () => setNodeControlOpen(false),
            }) : null,
          ),
        h("div", { className: "pipeline-flow-summary" },
          h("span", null, `${flow.nodes.length} nodes`),
          h("span", null, `${flow.edges.length} edges`),
          h("span", null, `${pipelines.length} pipelines`),
          h("span", null, `source config+components(${components.length})`),
          h("span", null, `run ${activeRun?.runId || "--"}`),
          h("span", null, `score ${pipelineScoreText(activeRun)}`),
          h("span", null, selectedNodeId ? `selected ${selectedNodeId}` : "click node to control"),
        ),
      ),
      h(PipelineEpochGantt, {
        epochs: pipelineRuns,
        activeRun,
        activePipeline,
        pipelineNodes,
        pipelineEdges,
        selection: ganttSelection,
        detailOpen: ganttDetailOpen,
        onDetailOpenChange: setGanttDetailOpen,
        runDetails,
        nodeDetails: focusedGanttNodeDetails,
        nodeDetailsState: focusedGanttNodeState,
        ganttScale,
        onGanttScaleChange: setGanttScale,
        onIntervalSelect: selectGanttInterval,
        onMarkerSelect: selectGanttMarker,
        onRunChange: (runId: string) => {
          setSelectedRunId(runId);
          setNodeControl(pipelineNodeControlState());
          setGanttSelection(pipelineGanttSelectionState());
          setGanttDetailOpen(false);
          if (selectedNodeId) void fetchNodeDetails(runId, selectedNodeId);
        },
        onRaw,
      }),
      h(Panel, { title: "观测指标", eyebrow: state.refreshedAt ? `Updated ${fmtClock(state.refreshedAt)}` : "Snapshot", loading: state.loading },
        h("div", { className: "metric-grid" },
          h(MetricCard, { label: "Health", value: state.health?.ok ? "OK" : "--", hint: state.health?.service || "D601 /health", tone: state.health?.ok ? "ok" : "warn" }),
          h(MetricCard, { label: "组件", value: componentCount, hint: "components registry", tone: snapshot?.registry?.ok === false ? "warn" : "ok" }),
          h(MetricCard, { label: "Pipeline", value: pipelines.length, hint: `${pipelineNodes.length} nodes / ${pipelineEdges.length} edges` }),
          h(MetricCard, { label: "运行记录", value: runCount, hint: `${statusCounts.succeeded || 0} succeeded / ${statusCounts.running || 0} running` }),
          h(MetricCard, { label: "OA 记录", value: Array.isArray(latestRun?.submissions) ? latestRun.submissions.length : 0, hint: latestRun?.runId || "latest run" }),
          h(MetricCard, { label: "Procedure", value: Array.isArray(latestRun?.procedureRuns) ? latestRun.procedureRuns.length : 0, hint: latestRun?.status || "no run" }),
          h(MetricCard, { label: "Score", value: pipelineScoreText(activeRun), hint: activeRun?.runId || "selected epoch", tone: pipelineScoreTone(activeRun) }),
        ),
        h("div", { className: "panel-actions inline-actions" }, h(RawButton, { title: "Pipeline Snapshot", data: snapshot, onOpen: onRaw, testId: "raw-pipeline-snapshot" })),
      ),
      h(Panel, { title: "评分器", eyebrow: activeRun?.runId || "selected epoch", loading: state.loading },
        h(PipelineScoreBoard, { run: activeRun, onRaw }),
      ),
      h(Panel, { title: "MiniMax 限额", eyebrow: "model/minimax-m27 quota", loading: state.loading },
        h(PipelineMinimaxQuotaPanel, { quota: minimaxQuota, onRaw }),
      ),
      h(Panel, { title: "OA 事件流", eyebrow: "100% event-driven diagnostics", className: "pipeline-wide-panel", loading: state.loading },
        h(PipelineOaEventFlowPanel, { diagnostics: oaDiagnostics, onRaw }),
      ),
      h(Panel, { title: "组件矩阵", eyebrow: `${componentClasses.length} classes`, loading: state.loading },
        componentClasses.length === 0 ? h(EmptyState, { title: "暂无组件", text: "等待 D601 pipeline backend 返回 registry.components" }) :
          h("div", { className: "component-strata" }, componentClasses.map((item) => h("article", { key: item.name, className: "component-stratum" },
            h("span", null, item.name),
            h("strong", null, item.count),
          ))),
        h("div", { className: "pipeline-component-list" },
          components.slice(0, 12).map((component: any) => h("span", { key: component.key, className: "data-chip" }, h("b", null, component.componentClass || "--"), h("span", null, component.id || component.key || "--"))),
        ),
      ),
      h(Panel, { title: "Epoch 列表", eyebrow: `${pipelineRuns.length}/${runCount} preview`, loading: state.loading },
        pipelineRuns.length === 0 ? h(EmptyState, { title: "暂无运行记录", text: "当前 pipeline 在 .state/pipeline-runs 中还没有 epoch。" }) :
          h("div", { className: "pipeline-run-list" }, pipelineRuns.map((run: any) => {
            const cardRun = String(run?.runId || "") === activeRunId ? activeRun : run;
            return h("article", {
            key: run.runId,
            className: `pipeline-run-card ${String(run.runId || "") === activeRunId ? "active" : ""}`,
            role: "button",
            tabIndex: 0,
            onClick: () => {
              setSelectedRunId(String(run.runId || ""));
              setGanttSelection(pipelineGanttSelectionState());
            },
            onKeyDown: (event: any) => {
              if (event.key === "Enter" || event.key === " ") {
                setSelectedRunId(String(run.runId || ""));
                setGanttSelection(pipelineGanttSelectionState());
              }
            },
          },
            h("div", { className: "node-card-head" }, h("strong", null, pipelineEpochLabel(pipelineRuns, run)), h(StatusBadge, { status: run.status }, run.status || "--")),
            h("div", { className: "docker-meta compact" },
              h("span", null, cardRun?.pipelineId || "--"),
              h("span", null, `nodes ${Array.isArray(cardRun?.nodes) ? cardRun.nodes.length : 0}`),
              h("span", null, `oa ${Array.isArray(cardRun?.submissions) ? cardRun.submissions.length : 0}`),
              h("span", null, `procedures ${Array.isArray(cardRun?.procedureRuns) ? cardRun.procedureRuns.length : 0}`),
              h(PipelineScoreBadge, { run: cardRun }),
            ),
            h("p", { className: "muted paragraph" }, summarizeValue(cardRun?.task)),
            h("span", { className: "pipeline-run-time" }, fmtDate(cardRun?.updatedAt)),
          );
          })),
      ),
      h(Panel, { title: "运行材料索引", eyebrow: activeRun?.runId || "selected epoch", className: "pipeline-wide-panel", loading: state.loading },
        h(PipelineRunMaterialIndex, { activeRun, onRaw }),
      ),
    ),
  );
}
