// Right column: observability — runs, tool timing, AG-UI JSON

function WorkspaceSnapshotDownload({ workspace }) {
  const [busy, setBusy] = React.useState(false);
  const assetId = workspace?.snapshot_asset_id;
  if (!assetId) return null;
  const download = async (e) => {
    e.preventDefault();
    if (busy) return;
    setBusy(true);
    try {
      const { url } = await window.api.createAssetDownloadUrl(assetId);
      window.open(url, "_blank", "noopener,noreferrer");
    } catch (err) {
      alert("Download workspace snapshot failed: " + (err.message || err));
    } finally {
      setBusy(false);
    }
  };
  return (
    <button
      className="btn ghost sm"
      style={{ height: 22, padding: "0 8px", fontSize: 11 }}
      onClick={download}
      disabled={busy}
      title="下载完整工作区"
    >
      <Icon name="upload" size={11} className="rotate-180" /> {busy ? "准备中…" : "下载全部"}
    </button>
  );
}

function WorkspaceTab({ runs, workspace }) {
  const workspaceRuns = [...runs]
    .filter((run) => run.workspace)
    .reverse();
  const latestRunId = workspaceRuns[0]?.runId || null;
  const [selectedRunId, setSelectedRunId] = React.useState(latestRunId);
  React.useEffect(() => {
    if (!workspaceRuns.some((run) => run.runId === selectedRunId)) {
      setSelectedRunId(latestRunId);
    }
  }, [latestRunId, selectedRunId, workspaceRuns.length]);
  const currentRun = workspaceRuns.find((run) => run.runId === selectedRunId)
    || workspaceRuns[0]
    || null;
  const current = currentRun?.workspace || workspace || null;
  const runId = currentRun?.runId || null;
  const [files, setFiles] = React.useState([]);
  const [loading, setLoading] = React.useState(false);
  const [error, setError] = React.useState("");
  const [syncWarnings, setSyncWarnings] = React.useState([]);
  const [preview, setPreview] = React.useState(null);
  const [downloading, setDownloading] = React.useState("");
  const refreshSequence = React.useRef(0);
  const previewSequence = React.useRef(0);

  React.useEffect(() => {
    refreshSequence.current += 1;
    previewSequence.current += 1;
    setFiles([]);
    setPreview(null);
    setError("");
  }, [runId]);

  React.useEffect(() => {
    if (!runId) {
      return undefined;
    }
    let cancelled = false;
    let timer = null;
    const refresh = async () => {
      const requestSequence = ++refreshSequence.current;
      try {
        setLoading(true);
        const result = await window.api.listWorkspaceFiles(runId);
        if (!cancelled && requestSequence === refreshSequence.current) {
          setFiles(result.items || []);
          setSyncWarnings([
            ...(result.truncated ? [{ path: "workspace", error: "文件超过 1,000 个，当前清单已截断" }] : []),
            ...(result.failures || []),
          ]);
          setError("");
        }
      } catch (err) {
        if (!cancelled && requestSequence === refreshSequence.current) {
          setError(err.message || String(err));
        }
      } finally {
        if (!cancelled && requestSequence === refreshSequence.current) {
          setLoading(false);
          if (currentRun?.running) timer = setTimeout(refresh, 10_000);
        }
      }
    };
    refresh();
    return () => {
      cancelled = true;
      refreshSequence.current += 1;
      if (timer) clearTimeout(timer);
    };
  }, [runId, !!currentRun?.running, current?.manifest_revision]);

  const openMarkdown = async (item) => {
    const requestSequence = ++previewSequence.current;
    try {
      setPreview({ path: item.path, loading: true, content: "" });
      const result = await window.api.readWorkspaceMarkdown(runId, item.path);
      if (requestSequence === previewSequence.current) {
        setPreview({ path: item.path, loading: false, content: result.content || "" });
      }
    } catch (err) {
      if (requestSequence === previewSequence.current) {
        setPreview({ path: item.path, loading: false, error: err.message || String(err), content: "" });
      }
    }
  };
  const downloadFile = async (item) => {
    if (downloading) return;
    setDownloading(item.path);
    try {
      await window.api.downloadWorkspaceFile(runId, item.path);
    } catch (err) {
      alert("下载工作区文件失败：" + (err.message || err));
    } finally {
      setDownloading("");
    }
  };
  if (!current && workspaceRuns.length === 0) {
    const hydrationError = runs.find((run) => run.workspaceLoadError)?.workspaceLoadError;
    return (
      <div className="empty">
        <div className="icon"><Icon name="grid" size={20} /></div>
        <h3>{hydrationError ? "工作区加载失败" : "暂无工作区"}</h3>
        <p>{hydrationError ? "请刷新页面重试。" : "沙箱生成文件后，会自动同步到这里。"}</p>
      </div>
    );
  }
  return (
    <div className="workspace-browser">
      <div className="workspace-files-card">
        <div className="workspace-files-head">
          <div>
            <strong>工作区文件</strong>
            <span>{files.length} 个文件 · {(error || current?.workspace_sync_error || current?.workspace_sync_failed) ? "同步失败" : currentRun?.running ? "正在同步" : "已同步"}</span>
          </div>
          <div className="workspace-files-actions">
            {workspaceRuns.length > 1 && (
              <select
                value={currentRun?.runId || ""}
                onChange={(event) => setSelectedRunId(event.target.value)}
                aria-label="选择运行记录"
              >
                {workspaceRuns.map((run, index) => (
                  <option key={run.runId} value={run.runId}>
                    {index === 0 ? "最新 · " : ""}{String(run.runId).slice(-8)}
                  </option>
                ))}
              </select>
            )}
            <WorkspaceSnapshotDownload workspace={current} />
            {currentRun?.running && <span className="workspace-sync-live" title="正在同步"><span></span></span>}
          </div>
        </div>
        {(current?.snapshot_error || current?.snapshot_failed) && (
          <div className="workspace-files-error">完整工作区快照生成失败，单个文件仍可下载。</div>
        )}
        {loading && files.length === 0 ? (
          <div className="workspace-files-empty">正在读取对象存储清单…</div>
        ) : (error || current?.workspace_sync_error || current?.workspace_sync_failed) && files.length === 0 ? (
          <div className="workspace-files-error">工作区文件同步失败，请稍后重试。</div>
        ) : files.length === 0 ? (
          <div className="workspace-files-empty">沙箱生成文件后会实时同步到这里。</div>
        ) : (
          <React.Fragment>
          {(error || current?.workspace_sync_error || current?.workspace_sync_failed) && (
            <div className="workspace-files-error">部分文件同步失败，请稍后重试。</div>
          )}
          {syncWarnings.length > 0 && (
            <div className="workspace-files-error">
              {syncWarnings.slice(0, 3).map((item) => `${item.path}: ${item.error}`).join("；")}
            </div>
          )}
          <div className="workspace-file-list">
            {files.map((item) => (
              <div className="workspace-file-row" key={item.path}>
                <button
                  className={"workspace-file-name" + (item.markdown ? " markdown" : "")}
                  onClick={() => item.markdown && openMarkdown(item)}
                  title={item.markdown ? `打开 ${item.path}` : item.path}
                >
                  <Icon name={item.markdown ? "list" : "code"} size={13} />
                  <span>{item.path}</span>
                </button>
                <span className="workspace-file-size">{formatWorkspaceBytes(item.size)}</span>
                <button className="workspace-file-download" onClick={() => downloadFile(item)} title="下载文件">
                  <Icon name="upload" size={12} className="rotate-180" />
                  {downloading === item.path ? "…" : ""}
                </button>
              </div>
            ))}
          </div>
          </React.Fragment>
        )}
      </div>
      {preview && (
        <div className="workspace-preview">
          <div className="workspace-preview-head">
            <span title={preview.path}>{preview.path}</span>
            <div>
              <button onClick={() => downloadFile({ path: preview.path })} title="下载"><Icon name="upload" size={13} className="rotate-180" /></button>
              <button onClick={() => { previewSequence.current += 1; setPreview(null); }} title="关闭"><Icon name="x" size={13} /></button>
            </div>
          </div>
          <div className="workspace-preview-body">
            {preview.loading ? <div className="workspace-files-empty">加载 Markdown…</div>
              : preview.error ? <div className="workspace-files-error">{preview.error}</div>
              : <MarkdownPreview content={preview.content} />}
          </div>
        </div>
      )}
    </div>
  );
}

function formatWorkspaceBytes(size) {
  const value = Number(size || 0);
  if (value < 1024) return `${value} B`;
  if (value < 1024 * 1024) return `${(value / 1024).toFixed(1)} KB`;
  return `${(value / 1024 / 1024).toFixed(1)} MB`;
}

function TimelineRow({ step }) {
  const kind = step.kind; // "tool" | "llm" | "final"
  return (
    <div className="tl-item">
      <div className={"gut " + kind}>
        <div className="node"></div>
      </div>
      <div className="label">
        <span className={"kind " + kind}>{kind}</span>
        <span className="name">{step.label}</span>
      </div>
      <div className="dur">{step.durationLabel || fmtMs(step.durMs)}</div>
    </div>
  );
}

function summarizeToolBatches(steps) {
  const summarized = [];
  for (let index = 0; index < (steps || []).length;) {
    const step = steps[index];
    if (step.kind !== "tool" || !step.batchId || Number(step.batchSize) <= 1) {
      summarized.push(step);
      index += 1;
      continue;
    }

    const batch = [];
    while (index < steps.length && steps[index].kind === "tool" && steps[index].batchId === step.batchId) {
      batch.push(steps[index]);
      index += 1;
    }
    const names = [...new Set(batch.map((item) => item.label))];
    const name = names.length === 1
      ? `${names[0]} × ${batch.length}`
      : `${names.join(", ")} · ${batch.length} calls`;
    summarized.push({
      ...step,
      label: name,
      durationLabel: `${fmtMs(step.durMs)} batch`,
      batchCallCount: batch.length,
    });
  }
  return summarized;
}

function formatTurnTokens(value) {
  if (!Number.isFinite(value)) return "—";
  if (value >= 1000) return `${(value / 1000).toFixed(value >= 10000 ? 0 : 1)}k`;
  return String(value);
}

function formatTurnCost(value) {
  if (!Number.isFinite(value)) return "cost —";
  return `$${value.toFixed(value > 0 && value < 0.0001 ? 8 : 4)}`;
}

function turnUsage(run) {
  const output = run?.output || {};
  const valueOrNull = (value) => (
    value === null || value === undefined || value === "" || !Number.isFinite(Number(value))
      ? null
      : Number(value)
  );
  return {
    inputTokens: valueOrNull(output.inputTokens),
    outputTokens: valueOrNull(output.outputTokens),
    costUsd: valueOrNull(output.costUsd),
  };
}

function JsonBlock({ title, data, defaultOpen = false }) {
  const [open, setOpen] = React.useState(defaultOpen);
  const [copied, setCopied] = React.useState(false);
  const txt = React.useMemo(() => JSON.stringify(data, null, 2), [data]);
  const copy = (e) => {
    e.stopPropagation();
    window.copyText?.(txt);
    setCopied(true);
    setTimeout(() => setCopied(false), 1200);
  };
  return (
    <div className="json-block">
      <div className="jb-head" style={{ cursor: "pointer" }} onClick={() => setOpen(!open)}>
        <Icon name="chev" size={10} className="" />
        <span style={{ transform: open ? "rotate(0deg)" : "rotate(0deg)" }}>{title}</span>
        <span className="copy" onClick={copy}>{copied ? "copied" : "copy"}</span>
      </div>
      {open && (
        <pre dangerouslySetInnerHTML={{ __html: highlightJSON(txt) }}></pre>
      )}
    </div>
  );
}

function safeStringify(value, space = 2) {
  try {
    return JSON.stringify(value, null, space);
  } catch (err) {
    return String(value ?? "");
  }
}

function fullText(value) {
  return typeof value === "string" ? value : safeStringify(value);
}

function modelPartText(part) {
  if (!part) return "";
  if (typeof part.text === "string") return part.text;
  if (part.text != null) return fullText(part.text);
  if (part.function_call) return fullText(part.function_call);
  if (part.function_response) return fullText(part.function_response);
  return fullText(part);
}

function flattenModelTools(tools) {
  if (!Array.isArray(tools)) return [];
  const out = [];
  tools.forEach((tool, toolIndex) => {
    const decls = tool?.function_declarations;
    if (Array.isArray(decls) && decls.length) {
      decls.forEach((decl, declIndex) => out.push({ ...decl, _toolIndex: toolIndex, _declIndex: declIndex }));
    } else {
      out.push({
        name: tool?.name || `tool_${toolIndex + 1}`,
        description: tool?.description,
        parameters: tool?.parameters || tool,
        _toolIndex: toolIndex,
        _declIndex: 0,
      });
    }
  });
  return out;
}

function MiniJsonBlock({ value }) {
  return <pre>{fullText(value)}</pre>;
}

function ModelMessagesList({ messages }) {
  if (!Array.isArray(messages) || !messages.length) {
    return <div className="mi-empty">No input messages captured yet.</div>;
  }
  return (
    <div className="mi-items">
      {messages.map((msg, index) => {
        const parts = Array.isArray(msg?.parts) ? msg.parts : [];
        return (
          <details className="mi-item" key={index} open={index < 2}>
            <summary>
              <span>Message {index + 1}</span>
              <span className="mono">{msg?.role || "unknown"}</span>
            </summary>
            <button className="mi-copy" onClick={() => {
              window.copyText?.(safeStringify(msg));
            }}>copy</button>
            {parts.length ? parts.map((part, partIndex) => (
              <pre className="text" key={partIndex}>{modelPartText(part)}</pre>
            )) : (
              <MiniJsonBlock value={msg} />
            )}
          </details>
        );
      })}
    </div>
  );
}

function modelSystemInstruction(input) {
  return input?.system_instruction ?? input?.config?.system_instruction ?? "";
}

function ModelSystemInstruction({ value }) {
  if (!value) return <div className="mi-empty">No system instruction captured yet.</div>;
  return <pre className="text">{fullText(value)}</pre>;
}

function ModelToolsList({ tools }) {
  const flat = flattenModelTools(tools);
  if (!flat.length) return <div className="mi-empty">No tools advertised yet.</div>;
  return (
    <div className="mi-items">
      {flat.map((tool, index) => (
        <details className="mi-item" key={`${tool.name || "tool"}-${index}`} open={index < 3}>
          <summary>
            <span>{index + 1}. {tool.name || "tool"}</span>
          </summary>
          <button className="mi-copy" onClick={() => {
            window.copyText?.(safeStringify(tool));
          }}>copy</button>
          {tool.description && <pre className="text">{fullText(tool.description)}</pre>}
          <MiniJsonBlock value={tool.parameters || tool} />
        </details>
      ))}
    </div>
  );
}

function ModelIOBlock({ run, defaultOpen = false }) {
  const [open, setOpen] = React.useState(defaultOpen);
  const displayModelIO = run.modelIO || [];

  const copyAll = (e) => {
    e.stopPropagation();
    window.copyText?.(JSON.stringify(displayModelIO || [], null, 2));
  };

  return (
    <div className="json-block model-io-block">
      <div className="jb-head" style={{ cursor: "pointer" }} onClick={() => setOpen(!open)}>
        <Icon name="chev" size={10} />
        <span>Model IO</span>
        {displayModelIO.length > 0 && <span className="mi-count">{displayModelIO.length}</span>}
        {displayModelIO.length > 0 && <span className="copy" onClick={copyAll}>copy</span>}
      </div>
      {open && (
        <div className="model-io-body">
          {displayModelIO.length === 0 && (
            <div className="mi-empty">
              {run.running ? "Waiting for model IO…" : "No model IO captured for this run."}
            </div>
          )}
          {displayModelIO.map((call, index) => (
            <details
              key={call.call_id}
              className="mi-call"
              open={!call.completed_at_ms || index === displayModelIO.length - 1}
            >
              <summary className="mi-head">
                <span>Round {call.round_index} · {call.model}</span>
                <span>{call.completed_at_ms ? `${call.latency_ms ?? "—"}ms` : "running"}</span>
              </summary>
              <div className="mi-meta">
                <span>prompt {call.usage?.prompt_token_count ?? call.usage?.input_tokens ?? "—"}</span>
                <span>output {call.usage?.candidates_token_count ?? call.usage?.output_tokens ?? "—"}</span>
                <span>tools {countModelTools(call.input?.tools)}</span>
              </div>
              <details className="mi-section" open>
                <summary className="mi-section-title">System instruction</summary>
                <ModelSystemInstruction value={modelSystemInstruction(call.input)} />
              </details>
              <details className="mi-section">
                <summary className="mi-section-title">Input messages</summary>
                <ModelMessagesList messages={call.input?.messages || []} />
              </details>
              <details className="mi-section">
                <summary className="mi-section-title">Tools</summary>
                <ModelToolsList tools={call.input?.tools || []} />
              </details>
              <details className="mi-section" open={!!call.completed_at_ms}>
                <summary className="mi-section-title">Output</summary>
                {call.completed_at_ms
                  ? <MiniJsonBlock value={call.output || {}} />
                  : <div className="mi-empty">Waiting for model output…</div>}
              </details>
            </details>
          ))}
        </div>
      )}
    </div>
  );
}

function countModelTools(tools) {
  let count = 0;
  (tools || []).forEach((tool) => {
    count += (tool.function_declarations || []).length;
    if (tool.name && !tool.function_declarations) count += 1;
  });
  return count;
}

function RunCard({ run, index, defaultOpen, onLoadDetail, onLoadModelIO }) {
  const canLoadDetail = run.historical && !run.running && !run.detailLoaded && !run.pendingBackendRunId;
  const canLoadModelIO = run.historical && !run.running && run.detailLoaded && !run.modelIOLoaded && !run.pendingBackendRunId;
  const usage = turnUsage(run);
  const hasUsage = usage.inputTokens !== null || usage.outputTokens !== null || usage.costUsd !== null;
  const timelineSteps = summarizeToolBatches(run.steps || []);
  return (
    <div className="run-card">
      <div className="head">
        <div>
          <div className="turn">Turn {index + 1} {run.running && <span className="dim" style={{ fontWeight: 400 }}>· running…</span>}</div>
          <div className="ids" style={{ marginTop: 3 }}>
            <span><span className="k">thread</span> {run.threadId}</span>
            <span><span className="k">run</span> {run.runId}</span>
          </div>
          {hasUsage && (
            <div className="turn-usage">
              in {formatTurnTokens(usage.inputTokens)} · out {formatTurnTokens(usage.outputTokens)} · {formatTurnCost(usage.costUsd)}
            </div>
          )}
        </div>
        <div className="right">
          {run.running
            ? <span className="dur" style={{ color: "var(--accent)" }}>● live</span>
            : <span className="dur">{fmtMs(run.totalMs)}</span>}
        </div>
      </div>
      {canLoadDetail && (
        <div style={{ padding: "8px 12px", borderBottom: "1px solid var(--border)", display: "flex", alignItems: "center", gap: 8 }}>
          <span style={{ color: "var(--text-muted)", fontSize: 12, flex: 1 }}>
            Historical run details are loaded on demand.
          </span>
          <button
            className="btn sm"
            disabled={run.detailLoading}
            onClick={() => onLoadDetail?.(run.runId)}
          >
            <Icon name="list" size={12} /> {run.detailLoading ? "Loading…" : "展开过程"}
          </button>
        </div>
      )}
      {run.detailError && (
        <div style={{ padding: "8px 12px", color: "#b91c1c", fontSize: 12, borderBottom: "1px solid var(--border)" }}>
          Failed to load details: {run.detailError}
        </div>
      )}
      <div className="timeline">
        {timelineSteps.map((s, i) => <TimelineRow key={`${s.batchId || s.label}-${i}`} step={s} />)}
      </div>
      {(run.detailLoaded || run.running) && (
        <JsonBlock title="AG-UI events" data={run.events || []} defaultOpen={defaultOpen} />
      )}
      {canLoadModelIO && (
        <div style={{ padding: "8px 12px", borderTop: "1px solid var(--border)", display: "flex", alignItems: "center", gap: 8 }}>
          <span style={{ color: "var(--text-muted)", fontSize: 12, flex: 1 }}>
            Model IO can be large and is loaded separately.
          </span>
          <button
            className="btn sm"
            disabled={run.modelIOLoading}
            onClick={() => onLoadModelIO?.(run.runId)}
          >
            <Icon name="cpu" size={12} /> {run.modelIOLoading ? "Loading…" : "加载 Model IO"}
          </button>
        </div>
      )}
      {run.modelIOError && (
        <div style={{ padding: "8px 12px", color: "#b91c1c", fontSize: 12, borderTop: "1px solid var(--border)" }}>
          Failed to load Model IO: {run.modelIOError}
        </div>
      )}
      {run.runId && !run.pendingBackendRunId && (run.running || run.modelIOLoaded) && (
        <ModelIOBlock run={run} defaultOpen={run.running} />
      )}
      {run.output && (
        <JsonBlock title="Final output" data={run.output} defaultOpen={false} />
      )}
    </div>
  );
}

// PR pg-system-prompt-preview: collapsible block showing the prompt the
// LLM sees for the current agent + session. Fetches block 1 (SYSTEM_PRELUDE)
// from /v1/system-prompt-prelude on first expand, caches on window so
// subsequent renders + agent switches don't refetch. Block 2 (activated
// skills) is rebuilt every render from sessionSkills + window.SKILLS —
// stays in sync when the user toggles a skill chip in the composer.
function SystemPromptSection({ agent, sessionSkills }) {
  const [open, setOpen] = React.useState(false);
  const [prelude, setPrelude] = React.useState(window.__SYSTEM_PRELUDE__ || null);
  const [activatedHeader, setActivatedHeader] = React.useState(
    window.__SYSTEM_PROMPT_HEADER__
    || "--- Activated skills (call `load_skill` for full instructions) ---"
  );
  const [loading, setLoading] = React.useState(false);
  const [error, setError] = React.useState(null);

  // Fetch on first expand — cheap (single constant + tiny string) so we
  // don't bother prefetching on mount; that way an agent that's never
  // opened the panel costs zero.
  React.useEffect(() => {
    if (!open || prelude || loading) return;
    setLoading(true);
    window.api.getSystemPromptPrelude()
      .then((r) => {
        window.__SYSTEM_PRELUDE__ = r.system_prelude;
        window.__SYSTEM_PROMPT_HEADER__ = r.activated_skills_header;
        setPrelude(r.system_prelude);
        if (r.activated_skills_header) setActivatedHeader(r.activated_skills_header);
      })
      .catch((err) => setError(err.message || String(err)))
      .finally(() => setLoading(false));
  }, [open, prelude, loading]);

  // Block 2: rebuild every render from session state. window.SKILLS is
  // [{id, name, description, ...}] with `.id === .name` (api-client.jsx),
  // so we can match sessionSkills (names) directly.
  const activeSkillEntries = React.useMemo(() => {
    const all = window.SKILLS || [];
    return (sessionSkills || [])
      .map((n) => all.find((s) => s.name === n || s.id === n))
      .filter(Boolean);
  }, [sessionSkills, agent && agent.id]);

  const block2 = activeSkillEntries.length
    ? activatedHeader + "\n" +
      activeSkillEntries.map((s) => `- ${s.name}: ${s.description || ""}`).join("\n")
    : activatedHeader + "\n(no skills active)";

  const fullPrompt = prelude
    ? (prelude + "\n\n" + block2)
    : "";

  return (
    <div className="system-prompt-section">
      <button
        type="button"
        className="system-prompt-head"
        onClick={() => setOpen((v) => !v)}
        aria-expanded={open}
      >
        <Icon name={open ? "chevdown" : "chev"} size={12} />
        <span className="label">System prompt</span>
        <span className="hint">
          {open ? "(click to collapse)" : "what the LLM literally sees"}
        </span>
      </button>
      {open && (
        <div className="system-prompt-body">
          {loading && <div className="loading">Loading…</div>}
          {error && <div className="err">Failed to load: {error}</div>}
          {!loading && !error && fullPrompt && (
            <pre className="prompt-text">{fullPrompt}</pre>
          )}
        </div>
      )}
    </div>
  );
}

function RightPanel({ runs, threadId, agent, workspace, sessionSkills, onLoadRunDetail, onLoadRunModelIO }) {
  const [tab, setTab] = React.useState("runs"); // runs | workspace | thread
  const totalRunMs = runs.reduce((s, r) => s + (r.totalMs || 0), 0);
  const totalTools = runs.reduce((s, r) => s + (r.steps || []).filter((x) => x.kind === "tool").length, 0);
  const live = runs.some((r) => r.running);

  return (
    <div className="col right">
      <div className="logs-head">
        <div className="title">Observability</div>
        {live && (
          <span className="live"><span className="dotpulse"></span> LIVE</span>
        )}
        <div style={{ flex: 1 }} />
        </div>

      {/* PR pg-system-prompt-preview: collapsible "what the LLM literally
          sees" panel at the very top of Observability. Default collapsed
          so it doesn't dominate; click the header to expand. Shows block 1
          (SYSTEM_PRELUDE from honesty.py, fetched once and cached on
          window.__SYSTEM_PRELUDE__) + block 2 (activated skills block
          composed from sessionSkills + window.SKILLS descriptions). Block
          3 (runtime facts) and block 4 (file attachments) are per-run /
          per-message → not shown in this stable preview. */}
      <SystemPromptSection agent={agent} sessionSkills={sessionSkills} />

      <div style={{
        padding: "10px 14px", borderBottom: "1px solid var(--border)",
        display: "grid", gridTemplateColumns: "1fr 1fr 1fr", gap: 8
      }}>
        <Stat label="Runs" value={runs.length} />
        <Stat label="Tool calls" value={totalTools} />
        <Stat label="Run time (sum)" value={fmtMs(totalRunMs)} mono />
      </div>

      <div style={{
        display: "flex", gap: 4, padding: "8px 12px 0",
      }}>
        <TabBtn active={tab === "runs"} onClick={() => setTab("runs")}>Runs</TabBtn>
        <TabBtn active={tab === "workspace"} onClick={() => setTab("workspace")}>Workspace</TabBtn>
        <TabBtn active={tab === "thread"} onClick={() => setTab("thread")}>Thread</TabBtn>
      </div>

      <div className="scroll right-scroll" style={{ paddingTop: 0 }}>
        {tab === "runs" ? (
          runs.length === 0 ? (
            <div className="empty">
              <div className="icon"><Icon name="search" size={20} /></div>
              <h3>暂无运行</h3>
              <p>发送消息后，每一轮的 thread / run id、工具调用、AG-UI 事件都会出现在这里。</p>
            </div>
          ) : (
            [...runs].reverse().map((run, ri) => (
              <RunCard
                key={run.runId}
                run={run}
                index={runs.length - 1 - ri}
                defaultOpen={ri === 0}
                onLoadDetail={onLoadRunDetail}
                onLoadModelIO={onLoadRunModelIO}
              />
            ))
          )
        ) : tab === "workspace" ? (
          <WorkspaceTab runs={runs} workspace={workspace} />
        ) : (
          <div className="json-block" style={{ margin: 12 }}>
            <div className="jb-head">Thread metadata</div>
            <pre dangerouslySetInnerHTML={{
              __html: highlightJSON(JSON.stringify({
                threadId,
                createdAt: new Date().toISOString(),
                runs: runs.length,
                totalDurationMs: totalRunMs,
                lastRunId: runs[runs.length - 1]?.runId || null,
                workspace,
              }, null, 2))
            }}></pre>
          </div>
        )}
      </div>
    </div>
  );
}

function Stat({ label, value, mono }) {
  return (
    <div style={{
      background: "white", border: "1px solid var(--border)",
      borderRadius: 8, padding: "8px 10px",
    }}>
      <div style={{ fontSize: 10.5, color: "var(--text-muted)", textTransform: "uppercase", letterSpacing: "0.06em", fontWeight: 600 }}>{label}</div>
      <div style={{
        fontSize: 16, fontWeight: 600, marginTop: 2,
        fontFamily: mono ? "var(--font-mono)" : "inherit"
      }}>{value}</div>
    </div>
  );
}

function TabBtn({ children, active, onClick }) {
  return (
    <button onClick={onClick} style={{
      background: active ? "white" : "transparent",
      border: active ? "1px solid var(--border)" : "1px solid transparent",
      borderBottom: active ? "1px solid white" : "1px solid transparent",
      borderRadius: "6px 6px 0 0",
      padding: "5px 12px",
      marginBottom: -1,
      fontSize: 12, color: active ? "var(--text)" : "var(--text-muted)",
      fontWeight: active ? 600 : 500,
      cursor: "pointer", position: "relative", zIndex: 1,
    }}>
      {children}
    </button>
  );
}

window.RightPanel = RightPanel;
