import { get } from "./api.js"; import { errorText, h, replace } from "./dom.js"; import { markdown } from "./markdown.js"; import { diagram } from "./mermaid.js"; import { findTask, focusTask, onEach, state } from "./state.js"; function graph(run) { const id = new Map(run.stages.map((s, i) => [s.name, `s${i}`])); const lines = ["flowchart TD"]; for (const s of run.stages) { lines.push(` ${id.get(s.name)}["${s.name.replaceAll('"', "#quot;")}"]`); } for (const s of run.stages) { for (const a of s.after) { lines.push(` ${id.get(a)} --> ${id.get(s.name)}`); } } return lines.join("\n"); } function attemptLabel(a) { switch (a.state) { case "running": return "running"; case "idle": return "idle, not finished"; case "finished": return "finished"; case "merged": return "merged"; case "discarded": return `discarded before finishing${a.discarded_by ? ` by ${a.discarded_by}` : ""}`; case "cleaned_up": return a.discarded_by === "byre" ? "finished, then discarded by the run's cleanup" : `finished, then discarded${a.discarded_by ? ` by ${a.discarded_by}` : ""}`; } } function stageLabel(s) { if (s.state !== "waiting") { return attemptLabel(s.attempts.findLast((a) => a.output !== undefined) ?? s.attempts.at(-1)); } if (s.waiting_on.length > 0) { return `waiting on ${s.waiting_on.map((n) => `“${n}”`).join(", ")}`; } return "waiting to start"; } function taskLink(taskId, label) { const a = h("a", { href: `#/task/${taskId}`, class: "workflow-task" }, label); a.onclick = (e) => { e.preventDefault(); focusTask(taskId); }; return a; } function attemptName(run, taskId) { for (const s of run.stages) { const i = s.attempts.findIndex((a) => a.task_id === taskId); if (i >= 0) { return s.attempts.length > 1 ? `“${s.name}” attempt ${i + 1}` : `“${s.name}”`; } } return findTask(taskId)?.title ?? taskId; } function attemptRow(run, stage, a, index) { const fed = run.stages.flatMap((s) => s.attempts.filter((d) => d.inputs.some((i) => i.task_id === a.task_id)).map((d) => d.task_id)); return h("li", { class: `workflow-attempt ${a.state}${a.task_id === state.selectedTaskId ? " current" : ""}` }, h("div", { class: "workflow-attempt-head" }, stage.attempts.length > 1 ? h("span", { class: "workflow-attempt-n" }, `#${index + 1}`) : null, taskLink(a.task_id, a.title), h("span", { class: `workflow-state ${a.state}` }, attemptLabel(a)), a.discarding ? h("span", { class: "workflow-state discarding" }, "discarding…") : null), a.forked_from ? h("div", { class: "workflow-line" }, "fork of ", taskLink(a.forked_from, attemptName(run, a.forked_from))) : null, a.inputs.length > 0 ? h("div", { class: "workflow-line" }, "started from ", ...a.inputs.flatMap((i, n) => [n > 0 ? ", " : null, taskLink(i.task_id, attemptName(run, i.task_id))])) : null, fed.length > 0 ? h("div", { class: "workflow-line fed" }, "fed ", ...fed.flatMap((id, n) => [n > 0 ? ", " : null, taskLink(id, attemptName(run, id))])) : null, a.output !== undefined ? h("details", { class: "workflow-output" }, h("summary", {}, "finish_stage output", a.commit ? h("code", {}, ` ${a.commit.slice(0, 10)}`) : null), markdown(a.output, "md", { taskId: a.task_id })) : null); } function stageCard(run, s) { return h("li", { class: `workflow-stage ${s.state}` }, h("header", {}, h("span", { class: "workflow-stage-name" }, s.name), h("span", { class: `workflow-state ${s.state}` }, stageLabel(s))), s.after.length > 0 ? h("div", { class: "workflow-line" }, `after ${s.after.map((n) => `“${n}”`).join(", ")}`) : h("div", { class: "workflow-line" }, "starts from the brief"), s.attempts.length > 0 ? h("ol", { class: "workflow-attempts" }, ...s.attempts.map((a, i) => attemptRow(run, s, a, i))) : null); } export function mountWorkflow(root) { const head = h("header", { class: "workflow-header" }); const graphBox = h("div", { class: "workflow-graph" }); const list = h("div", { class: "workflow-body" }); let run; let drawnRun = ""; let error = ""; let target = 0; let loading = false; let again = false; let disposed = false; const load = async () => { const taskId = state.selectedTaskId; if (!taskId || !findTask(taskId)?.workflow_run) { render(); return; } if (loading) { again = true; return; } loading = true; const started = ++target; try { const fresh = await get("/api/tasks/{taskId}/workflow/run", { path: { taskId } }); if (started === target) { run = fresh; error = ""; } } catch (err) { if (started === target) { error = errorText(err); } } loading = false; if (disposed) { return; } if (started === target) { render(); } if (again) { again = false; void load(); } }; const render = () => { const task = state.selectedTaskId ? findTask(state.selectedTaskId) : undefined; if (!task?.workflow_run) { replace(head); replace(graphBox); drawnRun = ""; replace(list, h("div", { class: "empty" }, task ? "This task isn't part of a workflow run." : "No task selected.")); return; } if (error) { replace(list, h("div", { class: "empty error-text" }, error)); return; } if (!run || run.run !== task.workflow_run) { replace(list, h("div", { class: "empty" }, "Loading…")); return; } replace(head, h("span", { class: "workflow-name" }, run.name), h("span", { class: "workflow-title" }, run.title), h("span", { class: `workflow-state ${run.finished ? "finished" : "running"}` }, run.finished ? "workflow finished" : "in progress")); if (drawnRun !== run.run) { drawnRun = run.run; replace(graphBox, diagram(graph(run))); } replace(list, run.brief.trim() ? h("details", { class: "workflow-brief" }, h("summary", {}, "Brief"), markdown(run.brief, "md", { taskId: task.id })) : null, h("ol", { class: "workflow-stages" }, ...run.stages.map((s) => stageCard(run, s)))); }; const relevant = (ev) => { if (ev.seq === 0 || !run) { return false; } if (ev.kind === "task.created") { return ev.payload["workflow_run"] === run.run; } const ids = new Set(run.stages.flatMap((s) => s.attempts.map((a) => a.task_id))); return ev.task_id !== undefined && ids.has(ev.task_id) && (ev.kind.startsWith("task.") || ev.kind.startsWith("session.")); }; const offs = [ onEach(["selection"], () => { target++; run = undefined; error = ""; render(); void load(); }), onEach(["tasks"], () => { if (!run && !loading && !error) { void load(); } }), onEach(["event"], (ev) => { if (relevant(ev)) { void load(); } }), ]; replace(root, h("div", { class: "workflow-view" }, head, h("div", { class: "workflow-scroll" }, graphBox, list))); render(); void load(); return () => { disposed = true; for (const off of offs) { off(); } }; } //# sourceMappingURL=workflow.js.map