File size: 26,206 Bytes
da5cba1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6abb7be
da5cba1
 
 
 
 
 
 
6abb7be
da5cba1
 
 
 
 
 
 
 
 
 
6abb7be
da5cba1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6abb7be
 
da5cba1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6abb7be
 
da5cba1
 
 
 
 
 
6abb7be
da5cba1
 
 
 
 
 
 
 
 
 
 
6abb7be
da5cba1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6abb7be
 
 
da5cba1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
// A rollout on the MiMo release's own harness (app/mimo/runner), live: phases across the top, the agent's steps as a
// timeline, and the grade when it lands. The MiMo RL Environment Explorer's run page; run.js hands MiMo rollouts here.
import { $, api, esc, checkMsg, md, money, dur, ago, tokensShort, statusPill, toast, DOMAIN_NAME, LIVE_STATUSES, rewardClass, rewardText, sk, spinner, emptyState,
  confetti, openDialog, closeModal, visibilityBadge, PRIVATE_NOTE, reportUrl } from "./util.js";
import { icon, DOMAIN_ICON } from "./icons.js";
import { refreshActive } from "./session.js";
import { taskHref, compareHref } from "./task.js";

const taskOf = (r) => taskHref(r.dataset || "XiaomiMiMo/MiMo-V2.6-RL-oss", r.path || r.task_id);

const PHASES = [["sandbox", "Sandbox"], ["setup", "Setup"], ["agent", "Agent"], ["verify", "Grading"], ["done", "Done"]];
const TOOL_ICON = { bash: "terminal", read: "file", write: "pencil", edit: "pencil", grep: "search", glob: "search", todowrite: "check", list: "list" };
let timer = null, tick = null, alive = true;

export function unmount() { alive = false; clearTimeout(timer); clearInterval(tick); }
const isLive = (r) => r && LIVE_STATUSES.includes(r.status);

function skeleton() {
  return `<div class="wrap page"><div class="crumbs">${sk.line(18)}</div>
    <div class="rp-head">${sk.line(22, 22)}${sk.line(55, 24)}${sk.line(45)}</div>
    <div class="stepper">${PHASES.map(() => `<div class="step">${sk.box(20, "width:20px;border-radius:50%")}${sk.line(60)}</div>`).join("")}</div>
    <div class="rp-grid"><div style="display:grid;gap:10px">${[70, 90, 55, 85, 60].map((w) => `<div style="display:grid;grid-template-columns:28px 1fr;gap:10px">${sk.box(28, "border-radius:8px")}${sk.box(30, `width:${w}%;border-radius:10px`)}</div>`).join("")}</div>
      <div style="display:grid;gap:12px">${sk.card(sk.line(30, 14) + sk.line(25, 40) + sk.lines(90, 70))}${sk.card(sk.line(30, 14) + sk.line(35, 28) + sk.lines(80, 60))}</div></div></div>`;
}

export async function mount(el, id, first) {
  alive = true;
  const st = { id, after: 0, events: [], run: null, follow: true, steps: 0 };
  if (!first) {
    el.innerHTML = skeleton();
    try { first = await api(`/api/runs/${encodeURIComponent(id)}`); }
    catch (e) {
      if (!alive) return;
      el.innerHTML = `<div class="wrap page">${emptyState("search", "Rollout not found", e.status === 404 ? "It may be private, or the link is wrong." : esc(e.message),
        `<a class="btn" href="/runs">${icon("arrowLeft")}Your rollouts</a>`)}</div>`;
      return;
    }
    if (!alive) return;
  }
  el.innerHTML = `<div class="wrap page rp fade-in">
    <nav class="crumbs" aria-label="Breadcrumb"><a href="/runs">Rollouts</a>${icon("chevronRight", 13)}<span>${esc(id)}</span></nav>
    <header class="rp-head" id="rp-head"></header>
    <div class="stepper" id="rp-phases"></div>
    <div class="rp-grid">
      <div class="rp-main"><div class="tl-bar"><h2>What the agent did</h2><span class="n" id="rp-n"></span>
        <div class="tools-right"><button class="btn sm ghost" id="rp-expand" type="button">Expand all</button>
        <label class="switch" id="rp-follow-wrap"><input type="checkbox" id="rp-follow" checked> Follow live</label></div></div>
        <ol class="timeline" id="rp-tl"></ol><div id="rp-tail"></div></div>
      <aside class="rp-side"><section class="panel grade" id="rp-grade"></section><section class="panel costcard" id="rp-cost"></section>
        <section class="panel meta-card" id="rp-meta"></section></aside>
    </div></div>`;
  $("#rp-follow", el).addEventListener("change", (e) => (st.follow = e.target.checked));
  $("#rp-expand", el).addEventListener("click", (e) => {
    const open = e.target.textContent === "Expand all";
    el.querySelectorAll("#rp-tl details").forEach((d) => (d.open = open));
    e.target.textContent = open ? "Collapse all" : "Expand all";
  });
  el.addEventListener("click", onClick(st));
  apply(el, st, first);
  const poll = async () => {
    if (!alive) return;
    try { apply(el, st, await api(`/api/runs/${encodeURIComponent(id)}?after=${st.after}`)); } catch { /* retry next tick */ }
    if (alive && isLive(st.run)) timer = setTimeout(poll, 1500);
    else refreshActive();
  };
  if (isLive(st.run)) timer = setTimeout(poll, 1500);
  tick = setInterval(() => { if (isLive(st.run)) { renderHead(el, st); renderTail(el, st); } }, 1000);
}

function apply(el, st, d) {
  st.run = d.run;
  st.stream = d.stream;
  if (!st.navSet) {   // someone else's public rollout belongs to Community, not to My rollouts
    st.navSet = true;
    const mine = !!d.run.is_owner;
    document.querySelectorAll("[data-nav]").forEach((a) => a.classList.toggle("on", a.dataset.nav === (mine ? "runs" : "community")));
    const c = el.querySelector(".crumbs a");
    if (c && !mine) { c.href = "/community"; c.textContent = "Community"; }
  }
  const fresh = d.events.filter((e) => e.i >= st.after);
  st.events.push(...fresh);
  st.after = st.events.length ? st.events[st.events.length - 1].i + 1 : st.after;
  renderHead(el, st);
  renderPhases(el, st);
  appendTimeline(el, st, fresh);
  // history appears at once; only steps that arrive while you watch slide in
  if (!st.primed) { el.querySelectorAll("#rp-tl .ev").forEach((li) => li.classList.add("still")); st.primed = true; }
  renderGrade(el, st);
  renderCost(el, st);
  renderMeta(el, st);
  renderTail(el, st);
  $("#rp-follow-wrap", el).hidden = !isLive(st.run);
  if (!st.run.is_owner) $("#rp-cost .fine", el)?.remove();
  if (st.follow && fresh.length && isLive(st.run)) $("#rp-tail", el).scrollIntoView({ behavior: "smooth", block: "end" });
}

// Built once and then only its text changes: re-rendering every second restarted the spinner and made it flicker.
function renderTail(el, st) {
  const t = $("#rp-tail", el);
  if (!t) return;
  if (!isLive(st.run)) { t.innerHTML = ""; return; }
  let msg = $(".live-msg", t);
  if (!msg) {
    t.innerHTML = `<ol class="timeline"><li class="ev ev-live still"><span class="ei">${spinner()}</span><div class="eb"><span class="live-msg"></span></div><span class="ts"></span></li></ol>`;
    msg = $(".live-msg", t);
  }
  const text = liveHint(st);
  if (msg.textContent !== text) msg.textContent = text;
}

function liveHint(st) {
  const s = st.run.status;
  if (s === "queued") return "Queued…";
  if (s === "starting") return "Starting a sandbox from the task's image. Big images take a minute or two.";
  if (s === "setup") return "Preparing the environment…";
  if (s === "verifying") return "Grading…";
  const last = st.events[st.events.length - 1];
  if (last?.kind === "partial") return `Writing… ${last.chars.toLocaleString()} characters${last.thinking_chars ? `, ${last.thinking_chars.toLocaleString()} thinking` : ""}`;
  const sm = st.stream;
  if (sm) {
    const n = sm.text + sm.thinking + sm.tool;
    if (!n) return `Waiting for the model's reply · ${dur(sm.age)}`;
    const parts = [sm.thinking && `${sm.thinking.toLocaleString()} thinking`, sm.text && `${sm.text.toLocaleString()} message`,
      sm.tool && `${sm.tool.toLocaleString()} in a tool call`].filter(Boolean);
    return `The model is writing · ${n.toLocaleString()} characters so far (${parts.join(", ")}) · ${dur(sm.age)}`;
  }
  const since = last ? Math.max(0, (Date.now() / 1000 - (st.run.started_at || st.run.created_at)) - last.t) : 0;
  return since > 45 ? `The model is working · ${dur(since)} since the last step. Long files take a while to write.` : "The model is working…";
}

function renderHead(el, st) {
  const r = st.run;
  if (!r) return;
  const start = r.started_at || r.created_at, end = r.finished_at || Date.now() / 1000;
  const owner = !!r.is_owner;
  const action = !owner ? `<a class="btn sm primary" href="${taskOf(r)}">${icon("play", 13)}Run it yourself</a>`
    : isLive(r)
    ? (st.stopping ? `<button class="btn danger sm" disabled>${spinner()}Stopping</button>` : `<button class="btn danger sm" id="rp-cancel">${icon("stop", 13)}Stop</button>`)
    : `<a class="btn sm primary" href="${taskOf(r)}">${icon("refresh", 13)}Run again</a>`;
  const vis = owner
    ? `<button class="btn sm" id="rp-vis" type="button" title="Change who can see this rollout">${icon(r.visibility === "public" ? "shield" : "globe", 13)}Make ${r.visibility === "public" ? "private" : "public"}</button>`
    : "";
  $("#rp-head", el).innerHTML = `
    <div class="kick"><span class="badge" style="--dc:var(--c-${r.domain},var(--primary))">${icon(DOMAIN_ICON[r.domain] || "cpu", 13)}${esc(DOMAIN_NAME[r.domain] || (r.runner === "nemo-gym" ? "NeMo Gym" : r.domain))}</span>${statusPill(r.status)}${visibilityBadge(r.visibility)}
      ${owner ? "" : '<span class="when">shared anonymously</span>'}
      <span class="when">${icon("clock", 13)}${dur(end - start)} · started ${ago(r.created_at)}</span></div>
    <h1><a href="${taskOf(r)}">${esc(r.title)}</a></h1>
    <div class="rp-bar"><div class="facts">
      <span>${icon("cpu", 14)}<b>${esc(r.endpoint ? r.model : r.model.split("/")[1] || r.model)}</b> via ${r.endpoint ? `your endpoint <code>${esc(r.endpoint.host)}</code>` : esc(r.provider || "auto")}</span>
      ${paramsText(r.params) ? `<span>${icon("gauge", 14)}${esc(paramsText(r.params))}</span>` : ""}
      ${r.judge ? `<span>${icon("scale", 14)}judge <b>${esc(r.judge.split("/")[1] || r.judge)}</b></span>` : ""}
      ${r.runner === "nemo-gym" ? `<span>${icon("check", 14)}One prediction · NeMo Gym verifier</span>` : r.domain === "music" ? `<span>${icon("message", 14)}single model call</span>` : `<span>${icon("terminal", 14)}OpenCode on the MiMo harness</span>`}${r.flavor ? `<span>${icon("box", 14)}${esc(r.flavor)}</span>` : ""}</div>
      <div class="rp-actions"><a class="btn sm ghost" href="${reportUrl({ run: r })}" target="_blank" rel="noopener" title="Report an issue with this rollout">${icon("flag", 13)}Report</a>${vis}
        <a class="btn sm" href="${compareHref(r.dataset || "XiaomiMiMo/MiMo-V2.6-RL-oss", r.path || r.task_id, [r.id])}" title="Compare with other rollouts of this task">${icon("columns", 13)}Compare</a>
        <a class="btn sm" href="${taskOf(r)}">${icon("file", 13)}View task</a>${action}</div></div>
    ${r.error ? `<div class="note-box err">${icon("alert")}<span>${esc(r.error)}</span></div>` : ""}`;
}

function renderPhases(el, st) {
  const seen = {};
  st.events.filter((e) => e.kind === "phase").forEach((e) => { (seen[e.name] ||= {})[e.status] = e; });
  const failed = ["failed", "cancelled", "interrupted"].includes(st.run.status);
  const noSandbox = st.run.domain === "music" || st.run.runner === "nemo-gym";
  const list = PHASES.filter(([k]) => !(noSandbox && (k === "sandbox" || k === "setup")));
  const box = $("#rp-phases", el);
  box.style.setProperty("--n", list.length);
  box.innerHTML = list.map(([k, label]) => {
    const p = seen[k] || {};
    const state = p.done ? (p.done.status === "error" ? "bad" : "ok") : p.start ? (failed ? "bad" : "on") : "off";
    const t = p.start && p.done ? dur(p.done.t - p.start.t) : "";
    const detail = (p.done || p.start)?.detail || "";
    const mk = state === "ok" ? icon("check", 12) : state === "bad" ? icon("x", 12) : "";
    return `<div class="step ${state}" title="${esc(detail)}"><span class="mk">${mk}</span><b>${label}</b><span class="t">${esc(t)}</span><em>${esc(detail)}</em></div>`;
  }).join("");
}

function appendTimeline(el, st, events) {
  const tl = $("#rp-tl", el);
  for (const e of events) {
    if (e.kind === "phase" && e.status === "start") st.phase = e.name;
    const html = item(e, st);
    if (html) { tl.insertAdjacentHTML("beforeend", html); if (e.kind === "tool" || e.kind === "text") st.steps++; }
  }
  $("#rp-n", el).textContent = st.steps ? `${st.steps} step${st.steps === 1 ? "" : "s"}` : "";
  const empty = $(".tl-empty", el);
  if (!tl.children.length && !isLive(st.run)) { if (!empty) tl.insertAdjacentHTML("afterend", `<p class="tl-empty">No agent steps were recorded.</p>`); }
  else if (empty) empty.remove();
}

// A whole trace as timeline rows, for pages that show several at once (the comparison). `skip` leaves out kinds
// the page shows elsewhere, such as the prompt when every rollout got the same one.
export function traceHtml(id, events, { skip = [] } = {}) {
  const st = { id, phase: null };
  let steps = 0;
  const html = events.map((e) => {
    if (e.kind === "phase" && e.status === "start") st.phase = e.name;
    if (skip.includes(e.kind)) return "";
    const h = item(e, st);
    if (h && (e.kind === "tool" || e.kind === "text")) steps++;
    return h;
  }).join("");
  return { html, steps };
}

const row = (cls, ic, t, body) => `<li class="ev ${cls}"><span class="ei">${icon(ic, 14)}</span><div class="eb">${body}</div><span class="ts">${t}</span></li>`;

function item(e, st) {
  const t = dur(e.t);
  const summary = (inner) => `<summary>${icon("chevronRight", 13, "chev")}${inner}</summary>`;
  switch (e.kind) {
    case "prompt": return row("ev-prompt", "doc", t, `<details ${e.harness === "nemo-gym" || st.run.runner === "nemo-gym" ? "" : "open"}>${summary(`<b>Instructions to the model</b><span class="peek">${(e.text || "").length.toLocaleString()} characters</span>`)}
      <div class="prompt-text">${esc(e.text)}</div><p class="prompt-note">${e.harness === "nemo-gym" || st.run.runner === "nemo-gym" ? "The native Responses request: conversation history and function definitions. The reference action is withheld." : e.harness === "opencode"
        ? "The task's instructions, sent as the first user message. OpenCode adds its own system prompt and tool definitions."
        : "The whole request: one user message, no system prompt."}</p></details>`);
    case "text": return row("ev-text", "message", t, `<div class="msg prose">${md(e.text)}</div>`);
    case "thinking": return row("ev-think", "brain", t, `<details>${summary(`<b>Thinking</b><span class="peek">${esc(e.text.replace(/\s+/g, " ").slice(0, 220))}</span>`)}<div class="thought">${esc(e.text)}</div></details>`);
    case "tool": {
      const inp = e.input || {};
      const head = inp.command || inp.filePath || inp.pattern || inp.path || e.title || Object.values(inp).map(String).join(" ").slice(0, 160);
      const mcp = isMcpTool(e.tool);
      const cls = `ev-tool${mcp ? " mcp" : ""}${e.status === "error" ? " bad" : ""}`;
      return row(cls, mcp ? "plug" : TOOL_ICON[e.tool] || "terminal", t, `<details>${summary(`<b>${esc(mcp ? prettyTool(e.tool) : e.tool)}</b>
        <code>${esc(String(head).split("\n")[0].slice(0, 200))}</code>${e.ms >= 1000 ? `<span class="ms">${dur(e.ms / 1000)}</span>` : ""}`)}
        <div class="io">${Object.keys(inp).length ? `<h5>Input</h5><pre>${esc(JSON.stringify(inp, null, 2))}</pre>` : ""}
          <h5>Output${e.truncated ? " (truncated)" : ""}</h5><pre>${esc(e.output || "(empty)")}</pre></div></details>`);
    }
    case "phase": return e.status === "error" && e.name !== "done" ? row("ev-err", "alert", t, `${esc(e.name)} failed: ${esc(e.detail)}`) : "";
    case "error": return row("ev-err", "alert", t, `<b>Error</b> ${esc(e.text)}`);
    case "log": return row("ev-log", "terminal", t, `<details>${summary(`<b>${({ sandbox: "Sandbox", setup: "Setup output", agent: "Agent output", verify: "Grader output" })[st.phase] || "Output"}</b><code>${esc((e.text || "").split("\n")[0].slice(0, 160))}</code>`)}<pre class="code-block">${esc(e.text)}</pre></details>`);
    case "diff": return row("ev-diff", "diff", t, `<details open>${summary(`<b>The agent's change</b><code>${e.files.length} file${e.files.length === 1 ? "" : "s"}</code>`)}<pre class="code-block diff">${diffHtml(e.text)}</pre></details>`);
    case "files": return row("ev-log", "folder", t, `<details>${summary(`<b>${esc(e.title)}</b><code>${e.files.length} files</code>`)}<pre class="code-block">${esc(e.files.join("\n"))}</pre></details>`);
    case "image": {
      const src = `/api/runs/${encodeURIComponent(st.id)}/artifacts/${encodeURIComponent(e.name)}`;
      return row("ev-img", "image", t, `<b>Rendered page</b> <span class="muted sm">${e.width}×${e.height}</span>
        <a href="${src}" target="_blank" rel="noopener"><img loading="lazy" src="${src}" alt="Full-page screenshot of what the agent built"></a>`);
    }
    default: return "";
  }
}

const paramsText = (p) => !p ? "" : [p.thinking && p.thinking !== "default" && `thinking ${p.thinking === "none" ? "off" : p.thinking}`,
  p.temperature != null && `temperature ${p.temperature}`, p.steps && `${p.steps} steps max`, p.timeout_min && `${p.timeout_min} min limit`,
  p.max_tokens && `${p.max_tokens.toLocaleString()} max tokens`].filter(Boolean).join(" · ");
const humanize = (id) => { const t = String(id || "").replace(/[_-]+/g, " ").trim(); return t.charAt(0).toUpperCase() + t.slice(1); };
export const prettyTool = (name) => { const parts = name.split("_"); return parts.length > 3 ? parts.slice(-3).join("_") : name; };
export const isMcpTool = (name) => !(name in TOOL_ICON) && /_/.test(name || "");

export function diffHtml(text) {
  return (text || "").split("\n").slice(0, 3000).map((l) => {
    const c = l.startsWith("+++") || l.startsWith("---") || l.startsWith("diff ") ? "dh" : l.startsWith("+") ? "da" : l.startsWith("-") ? "dd" : l.startsWith("@@") ? "dc" : "";
    return `<span class="${c}">${esc(l)}</span>`;
  }).join("\n");
}

function renderGrade(el, st) {
  const g = [...st.events].reverse().find((e) => e.kind === "checks") ||
    (st.run.status === "done" && Number.isFinite(st.run.reward) ? { reward: st.run.reward, summary: "Recorded reward. Detailed grading events are unavailable." } : null);
  const box = $("#rp-grade", el);
  const h = `<div class="panel-h"><h3>${icon("scale", 14)}Grade</h3></div>`;
  if (!g) {
    box.innerHTML = `${h}<div class="panel-b"><p class="muted sm">${isLive(st.run) ? "Appears here once the agent finishes and the task's grader has run." : "This rollout was not graded."}</p></div>`;
    return;
  }
  const rw = g.reward ?? st.run.reward;
  const img = [...st.events].reverse().find((e) => e.kind === "image");
  const src = img && `/api/runs/${encodeURIComponent(st.id)}/artifacts/${encodeURIComponent(img.name)}`;
  const long = (g.summary || "").length > 180;
  const zh = /[一-鿿]/.test(g.summary || "");
  const cls = rewardClass(rw);
  // General rubric checks: each one's share of the reward (verify.py counts a missing weight as 1)
  const rubric = (g.checks || []).some((c) => c.method === "llm" || c.method === "rule");
  const wt = (c) => (c.weight == null ? 1 : Number(c.weight));
  const wsum = rubric ? g.checks.reduce((n, c) => n + wt(c), 0) || 1 : 1;
  box.innerHTML = `${h}<div class="panel-b">
    ${src ? `<a class="shot" href="${src}" target="_blank" rel="noopener" title="Open the full-page render"><img src="${src}" alt="What the judge saw"></a>` : ""}
    <div class="score"><span class="big ${cls}">${rewardText(rw)}</span><span class="of">${esc(st.run.reward_key || "reward")}</span></div>
    ${g.formula ? `<p class="muted xs">${esc(g.formula)}</p>` : ""}
    ${long || !g.summary ? "" : `<p class="gsum">${esc(g.summary)}</p>`}
    ${(g.checks || []).length ? `<ul class="gchecks">${g.checks.map((c) => `<li class="${c.passed === true ? "ok" : c.passed === false ? "bad" : "na"}">
      <span class="mk">${icon(c.passed === true ? "check" : c.passed === false ? "x" : "more", 14)}</span>
      <div><b>${esc(c.question || humanize(c.id))}</b>
      ${c.tier || c.method === "llm" ? `<span class="meta">${esc([c.tier, c.method === "llm" && "model-judged", rubric && `${Math.round((wt(c) / wsum) * 100)}% of the reward`].filter(Boolean).join(" · "))}</span>` : ""}
      ${c.message ? `<p>${esc(checkMsg(c.message).slice(0, 300))}</p>` : ""}</div>
      ${c.score != null && c.passed == null ? `<span class="sc">${Number(c.score).toFixed(2)}</span>` : "<span></span>"}</li>`).join("")}</ul>` : ""}
    ${(g.checks || []).some((c) => c.method === "llm") ? `<p class="xs faint" style="margin-top:8px">The task's grader keeps each check's verdict, not the judge's reasoning.</p>` : ""}
    ${long ? `<details class="notes"><summary class="disclose">${icon("chevronRight", 14, "chev")}Judge's notes</summary><p>${esc(g.summary)}</p>
      ${zh ? `<p class="xs faint">In Chinese because Xiaomi's grading rubric is written in Chinese; it is used unchanged so scores stay comparable with theirs.</p>` : ""}</details>` : ""}
  </div>`;
}

function renderCost(el, st) {
  const r = st.run, c = r.cost || {}, t = r.tokens || {};
  const tot = (c.model || 0) + (c.sandbox || 0) || 1;
  $("#rp-cost", el).innerHTML = `<div class="panel-h"><h3>${icon("coins", 14)}Cost</h3>${isLive(r) ? `<span class="aside"><i class="pulse" style="color:var(--live)"></i> live</span>` : ""}</div>
    <div class="panel-b">
    <div class="big-cost">${money(c.total)}</div>
    <div class="cost-split"><i class="m" style="width:${((c.model || 0) / tot) * 100}%"></i><i class="s" style="width:${((c.sandbox || 0) / tot) * 100}%"></i></div>
    <div class="legend">
      <div><span class="sw" style="background:var(--c-code)"></span><span>Model tokens</span><b>${r.endpoint && r.endpoint.price_in == null && r.endpoint.price_out == null ? '<span class="faint">not metered</span>' : money(c.model)}</b></div>
      <div><span class="sw" style="background:var(--c-cyber)"></span><span>Sandbox time</span><b>${money(c.sandbox)}</b></div>
      ${r.judge ? `<div><span class="sw" style="background:var(--surface-3)"></span><span>Judge calls</span><b class="faint">not metered</b></div>` : ""}
    </div>
    <dl class="kv sm"><dt>Tokens in</dt><dd class="num">${tokensShort((t.input || 0) + (t.cache_read || 0))}${t.cache_read ? ` <span class="faint">(${tokensShort(t.cache_read)} cached)</span>` : ""}</dd>
      <dt>Tokens out</dt><dd class="num">${tokensShort(t.output)}${t.reasoning ? ` <span class="faint">+ ${tokensShort(t.reasoning)} thinking</span>` : ""}</dd></dl>
    <p class="fine">${r.endpoint ? `Tokens are billed by your endpoint (${esc(r.endpoint.host)}); the sandbox by Hugging Face.` : `Billed to your Hugging Face account at ${esc(r.provider || "the provider")}'s price.`}</p></div>`;
}

async function setVisibility(el, st, to, btn) {
  try {
    await api(`/api/runs/${encodeURIComponent(st.id)}/visibility`, { method: "POST", body: { visibility: to } });
    st.run.visibility = to;
    renderHead(el, st);
    if (to === "public") {
      const r = (btn || $("#rp-vis", el))?.getBoundingClientRect();
      confetti(r ? r.left + r.width / 2 : innerWidth / 2, r ? r.top : innerHeight / 3);
      toast("Public: thank you for sharing with the community");
    } else toast("Private: only you can see this rollout");
  } catch (e) { toast(e.message); }
}

function renderMeta(el, st) {
  const r = st.run, p = r.params || {}, pv = r.provenance || {};
  const row = (k, v) => (v == null || v === "" ? "" : `<dt>${k}</dt><dd>${v}</dd>`);
  const code = (v) => `<code>${esc(v)}</code>`;
  const think = { none: "off", low: "low", medium: "medium", high: "high" }[p.thinking] || "model default";
  $("#rp-meta", el).innerHTML = `<div class="panel-h"><h3>${icon("info", 14)}Run details</h3></div><div class="panel-b"><dl class="kv sm">
    ${row("Model", code(r.model))}
    ${row("Served by", r.endpoint ? "an OpenAI-compatible endpoint the runner brought" + (r.endpoint.host && r.is_owner ? ` (${code(r.endpoint.host)})` : "") : esc(r.provider || "HF router auto"))}
    ${row("Judge", r.judge ? code(r.judge) : "")}
    ${row("Thinking", esc(think))}
    ${row("Temperature", p.temperature != null ? esc(p.temperature) : "model default")}
    ${r.domain === "music" || r.runner === "nemo-gym" ? row("Max tokens", esc(p.max_tokens || p.max_tokens_used || 100000)) : row("Step cap", esc(p.steps || "task default")) + row("Time limit", p.timeout_min ? esc(p.timeout_min + " min") : "task default") + row("Max tokens / step", esc(p.max_tokens || "default"))}
    ${row("Harness", r.runner === "nemo-gym" ? "Single prediction; tool calls are not executed" : r.domain === "music" ? "single model call" : `OpenCode ${esc(pv.harness?.installed || pv.harness?.version || "")}`)}
    ${row("Verifier revision", r.runner === "nemo-gym" ? code(pv.commit || "unknown") : "")}
    ${row("Sandbox", r.flavor ? esc(r.flavor) : "")}
    ${row("Image", r.image ? code(r.image.replace("docker.io/", "")) : "")}
    ${row("Explorer", pv.app ? `v${esc(pv.app.version)} · ${code(pv.app.source)}` : "")}
    ${row("Dataset", pv.dataset ? `${esc(pv.dataset.repo)}${pv.dataset.revision ? " @ " + code(pv.dataset.revision.slice(0, 10)) : ""}` : "")}
    ${row("Reference", pv.reference ? Object.entries(pv.reference).map(([k, v]) => `${esc(k.split("/")[1])} ${code(v)}`).join(" · ") : "")}
    ${row("Rollout id", code(r.id))}
  </dl></div>`;
}

function onClick(st) {
  return async (ev) => {
    const vb = ev.target.closest("#rp-vis");
    if (vb) {
      const el = vb.closest(".rp");
      if (st.run.visibility === "public") {
        const dlg = openDialog(`<div class="dialog-h"><div><h2>Make this rollout private?</h2><p>${esc(PRIVATE_NOTE)}</p></div>
          <button class="icon-btn" type="button" data-close aria-label="Close">${icon("x", 18)}</button></div>
          <div class="dialog-b"><div style="display:flex;gap:8px;justify-content:flex-end"><button class="btn" data-close>Keep it public</button>
          <button class="btn primary" id="vis-go">Make private</button></div></div>`);
        dlg.addEventListener("click", async (e) => {
          if (e.target.closest("[data-close]")) closeModal();
          if (e.target.closest("#vis-go")) { closeModal(); await setVisibility(el, st, "private"); }
        });
      } else await setVisibility(el, st, "public", vb);
      return;
    }
    const b = ev.target.closest("#rp-cancel");
    if (!b) return;
    st.stopping = true;
    b.disabled = true;
    b.innerHTML = `${spinner()}Stopping`;
    try { await api(`/api/runs/${encodeURIComponent(st.id)}/cancel`, { method: "POST" }); toast("Stopping the rollout…"); }
    catch (e) { st.stopping = false; toast(e.message); b.disabled = false; b.innerHTML = `${icon("stop", 13)}Stop`; }
  };
}