RL-Explorer / web /js /run-mimo.js
AdithyaSK's picture
AdithyaSK HF Staff
Deploy HF RL Explorer
6abb7be verified
Raw History Blame Contribute Delete
26.2 kB
// 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`; }
};
}