"use client"; import { useEffect, useRef, useState } from "react"; // The apply chain, offered as ONE action. // // The order is the trap. `apply-hook-trims.mjs` closes by telling you to use // its output with HOOKDIR=, which is right for a plain build and WRONG // for these two: the shipped overlays point at hooks-best/, where hook-01/02/03 // are copies of the winning take, and HOOKDIR overrides BY BASENAME -- so that // route silently restores each occurrence's own take and throws the take // selection away. // // So the UI never exposes HOOKDIR. It offers the chain, shows the exact argv // before running it, and the re-mix step is asserted to carry no HOOKDIR at all // (server-side, in the build route). type Step = { label: string; argv: string[]; cwd: string; env: Record }; type JobView = { id: string; state: "running" | "done" | "failed"; stepIndex: number; steps: Step[]; error: string | null; log: string[]; next: number; }; type MediaFile = { path: string; label: string }; export default function BuildChain({ setId }: { setId: string }) { const [body, setBody] = useState(""); const [files, setFiles] = useState([]); const [dry, setDry] = useState<{ steps: Step[]; body: string } | null>(null); const [job, setJob] = useState(null); const [error, setError] = useState(null); const [busy, setBusy] = useState(false); const since = useRef(0); useEffect(() => { fetch("/api/mix/files", { cache: "no-store" }) .then((r) => (r.ok ? r.json() : null)) .then((j) => { const list = (j?.files ?? j?.media ?? []) as MediaFile[]; if (Array.isArray(list)) setFiles(list.filter((f) => /\.(wav|mp4|mp3|flac)$/i.test(f.label))); }) .catch(() => {}); }, []); // Poll while a job is alive. Same shape as MixBench's render poll. useEffect(() => { if (!job || job.state !== "running") return; const t = setInterval(async () => { try { const r = await fetch(`/api/browse/build?job=${job.id}&since=${since.current}`, { cache: "no-store", }); if (!r.ok) return; const j = (await r.json()) as JobView; since.current = j.next; setJob((prev) => (prev ? { ...j, log: [...prev.log, ...j.log] } : j)); } catch { /* keep polling; a dropped tick is not a failure */ } }, 1000); return () => clearInterval(t); }, [job]); const post = async (asDry: boolean) => { setBusy(true); setError(null); try { const res = await fetch(`/api/browse/build${asDry ? "?dry=1" : ""}`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ recipe: "mk-hooks-remix", set: setId, body }), cache: "no-store", }); const j = await res.json(); if (!res.ok) throw new Error(j.error || `HTTP ${res.status}`); if (asDry) { setDry({ steps: j.steps, body: j.body }); } else { setDry({ steps: j.steps, body: j.body }); since.current = j.job?.next ?? 0; setJob(j.job); } } catch (e) { setError(e instanceof Error ? e.message : String(e)); } finally { setBusy(false); } }; return (
apply the trims

Three steps in one action:{" "} apply-hook-trims →{" "} pick-take --apply →{" "} mk-mix-hooks. Running the first one alone and pointing the build at its output with{" "} HOOKDIR= would put each occurrence's own take back and lose the take selection, so that route is not offered.

{/* The whole-song .sh builds were never runnable here: they hardcoded their own paths, ignored SONG_DIR, and took 20+ minutes. They were deleted in release 12 (lib/jobs.ts). */}

The whole-song rebuild scripts were one-off shell run logs that hardcoded their paths and ignored SONG_DIR; they are not in the tree, and not runnable from here.

{error &&

{error}

} {dry && (
{dry.steps.map((s, i) => (
step {i + 1} · {s.label} {job && job.stepIndex === i && job.state === "running" && ( running )}
                {Object.entries(s.env)
                  .map(([k, v]) => `${k}=${v}`)
                  .join(" ")}
                {"\n"}
                {s.argv.join(" ")}
              
))}
)} {job && (
{job.id} · {job.state} {job.error ? ` · ${job.error}` : ""}
            {job.log.join("\n") || "…"}
          
)}
); }