apps: restore declared media and timeline capabilities

This commit is contained in:
mvschwarz committed 2026-08-02 13:01:47 -07:00
1 parent 78c41495e5
commit edc530be1c
7 files changed
+235 -74

No files matched your search

+9
View File
@@ -44,6 +44,15 @@ say this. **If there is a field for it, nobody inlines it.**
A manifest authored on a machine that already has everything installed will
always under-declare. Test one by installing it somewhere that has nothing.
The video provider's app-contract and media-root regressions run after its
dependencies are installed:
```sh
cd providers/studio-video
npm ci
npm test
```
## Provenance
Ported from the frozen `studio-box` spike, then de-bespoked: the original
+19 -1
View File
@@ -59,9 +59,27 @@
"/api/slot-history",
"/api/history-since",
"/api/project-assets",
"/api/project-assets/rebuild",
"/api/project-info",
"/api/probe-duration",
"/api/annotation",
"/api/focus"
"/api/approve",
"/api/export-final",
"/api/export-review",
"/api/export-status/",
"/api/focus",
"/api/health",
"/api/library",
"/api/library/media",
"/api/library/route",
"/api/media",
"/api/reconform",
"/api/tags",
"/api/templates",
"/api/thumb",
"/api/timeline/doc",
"/api/validate-audio",
"/api/validate-export"
],
"vendor": [],
"requires": {
+18 -71
View File
@@ -2104,18 +2104,19 @@
<div id="nleNoteOverlay" class="nle-overlay" hidden>
<div class="nle-note-panel">
<h3>Review note <i id="nleNoteContext"></i></h3>
<p class="nle-safe-note">Review handoff lives in AGENTS. MINI-NLE does not inject work into a rig.</p>
<div class="nle-note-row">
kind <select id="nleNoteKind"><option>general</option><option>regen</option><option>timing</option><option>audio</option><option>other</option></select>
to <select id="nleNoteReceiver"></select>
<input id="nleNoteNewReceiver" class="grow" type="text" placeholder="or type a session@rig (persists)">
kind <select id="nleNoteKind" disabled><option>general</option><option>regen</option><option>timing</option><option>audio</option><option>other</option></select>
to <select id="nleNoteReceiver" disabled><option>Use AGENTS</option></select>
<input id="nleNoteNewReceiver" class="grow" type="text" placeholder="Use AGENTS for review handoff" disabled>
</div>
<textarea id="nleNoteText" placeholder="What should change, and why it matters at this moment"></textarea>
<textarea id="nleNoteText" placeholder="Use the AGENTS surface to hand review work to a seat" disabled></textarea>
<div class="nle-note-row">
<button id="nleNoteAnnotate" type="button">Annotate frame...</button>
<button id="nleNoteAnnotate" type="button" disabled>Annotate frame...</button>
<span id="nleNoteAnnotChip" class="nle-annot-chip"></span>
<span class="grow"></span>
<button id="nleNoteSave" type="button" class="m-ib primary">Save draft</button>
<button id="nleNoteSend" type="button">Send all</button>
<button id="nleNoteSave" type="button" class="m-ib primary" disabled>Use AGENTS</button>
<button id="nleNoteSend" type="button" disabled>Use AGENTS</button>
<button id="nleNoteCancel" type="button">Cancel</button>
</div>
</div>
@@ -5203,16 +5204,9 @@
async function nleLoadReceivers() {
const note = nleNoteState();
try {
const payload = await callExportApi("/api/receivers");
nleEls.nleNoteReceiver.innerHTML = (payload.receivers || []).map((receiver) =>
`<option value="${escapeHtml(receiver)}">${escapeHtml(receiver)}</option>`).join("");
note.lanes = payload.lanes || null;
note.receiversLoaded = true;
} catch {
nleEls.nleNoteReceiver.innerHTML = '<option value="">(no receiver configured)</option>';
}
nleApplyNoteLaneDefault();
nleEls.nleNoteReceiver.innerHTML = '<option value="">Use AGENTS for review handoff</option>';
note.lanes = null;
note.receiversLoaded = true;
}
function nleOpenNoteForm({ slot, timestamp, lane }) {
@@ -5306,16 +5300,10 @@
}
async function nleLoadFeedbackDrafts({ silent = true, rerenderStrip = true } = {}) {
try {
const payload = await callExportApi("/api/note");
nle.feedbackDrafts = (payload.drafts || []).filter((note) => note.status === "draft");
nleUpdateFeedbackSurfaces({ rerenderStrip });
return nle.feedbackDrafts;
} catch (error) {
if (!silent) nlePatchStatusShow(`Feedback drafts unavailable: ${error.message}`, "warn");
window.__nleFeedbackDrafts = nle.feedbackDrafts || [];
return nle.feedbackDrafts || [];
}
nle.feedbackDrafts = [];
nleUpdateFeedbackSurfaces({ rerenderStrip });
if (!silent) nlePatchStatusShow("Review handoff lives in AGENTS.", "warn");
return nle.feedbackDrafts;
}
function nleSelectedFeedbackReceiver() {
@@ -5323,23 +5311,7 @@
}
async function nleSaveFeedbackDraft({ slot, timestamp, kind, text, receiver, annotation }) {
const cleanText = String(text || "").trim();
if (!cleanText) throw new Error("Feedback text is required.");
const payload = await callExportApi("/api/note", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({
slot,
timestamp,
kind: kind || "general",
text: cleanText,
receiver,
annotation,
status: "draft",
}),
});
await nleLoadFeedbackDrafts({ silent: true });
return payload;
throw new Error("Review handoff lives in AGENTS; MINI-NLE does not inject rig work.");
}
async function nleSaveNoteDraft() {
@@ -5364,16 +5336,7 @@
}
async function nleFlushFeedbackDrafts(receiver = "") {
const useReceiver = String(receiver || "").trim() || nleSelectedFeedbackReceiver();
const payload = await callExportApi("/api/note/flush", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ receiver: useReceiver }),
});
window.__nleLastFeedbackFlush = payload;
await nleLoadFeedbackDrafts({ silent: true });
nlePatchStatusShow(`Sent ${payload.draftCount} feedback draft(s) to ${payload.receiver}${payload.qitemId ? ` (${payload.qitemId})` : ""}.`, "ok");
return payload;
throw new Error("Review handoff lives in AGENTS; MINI-NLE does not inject rig work.");
}
async function nleSendNote() {
@@ -5763,14 +5726,7 @@
try { window.parent?.postMessage({ t: "open-agent" }, "*"); } catch {}
});
body.querySelector("[data-cu-dismiss]").addEventListener("click", async () => {
for (const note of notes) {
try {
await callExportApi("/api/note/done", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ id: note.id, done: true }) });
} catch (error) { nleEditStatusShow(`Dismiss failed: ${error.message}`, "error"); return; }
}
await nleLoadFeedbackDrafts({ silent: true, rerenderStrip: false });
nle.catchup.asked = (nle.feedbackDrafts || []).filter((note) => note.slot);
nleRenderCatchup();
nleEditStatusShow("Review handoff lives in AGENTS; MINI-NLE does not close rig work.", "warn");
});
wrap.appendChild(node);
}
@@ -6235,7 +6191,6 @@
<div class="p">${escapeHtml(asset.path)}</div>
<div class="nle-fields">
<button type="button" id="nleCopyPath">Copy path</button>
<button type="button" id="nleReveal">Reveal in Finder</button>
${nle.swapTarget && (asset.media_type === "video" || asset.media_type === "image") ? `<button type="button" id="nleUseForSwap" class="primary">Use for ${escapeHtml(nle.swapTarget)}</button>` : ""}
</div>`;
const useForSwap = document.getElementById("nleUseForSwap");
@@ -6284,14 +6239,6 @@
await navigator.clipboard.writeText(asset.path);
nlePatchStatusShow(`Copied path: ${asset.path}`, "ok");
});
document.getElementById("nleReveal").addEventListener("click", async () => {
try {
await callExportApi("/api/reveal-file", { method: "POST", body: JSON.stringify({ path: asset.path }), headers: { "content-type": "application/json" } });
nlePatchStatusShow(`Revealed in Finder: ${asset.path}`, "ok");
} catch (error) {
nlePatchStatusShow(error.message, "error");
}
});
}
function nleRelativeRef(asset) {
+2 -2
View File
@@ -760,8 +760,8 @@ async function handleNleApi(req, res, url) {
assets.push({ name, path: full, root: rootLabel, kind, size: stat.size, mtime: stat.mtimeMs, meta });
}
};
for (const root of roots) walk(path.join(root, "library"), path.basename(root), 0);
return sendJson(res, 200, { ok: true, assets, roots, note: "library/ subtrees of the configured media roots; <file>.meta.json sidecars are informational (license/credit/source) - never gates" });
for (const root of roots) walk(root, path.basename(root), 0);
return sendJson(res, 200, { ok: true, assets, roots, note: "configured media roots are scanned directly; <file>.meta.json sidecars are informational (license/credit/source) - never gates" });
}
if (url.pathname === "/api/library/media" && req.method === "GET") {
// roots-only gate for library previews (library files are not in the
+3
View File
@@ -4,6 +4,9 @@
"type": "module",
"description": "The video/timeline provider for OpenRig Studio surfaces. Serves the verbs mini-nle, media-manager and canvas call. Ported from studio-box; not extracted, not rewritten.",
"license": "Apache-2.0",
"scripts": {
"test": "node --test ../../test/mini-nle-contract.test.mjs ../../test/media-library.test.mjs"
},
"engines": {
"node": ">=18"
},
+77
View File
@@ -0,0 +1,77 @@
import test from "node:test";
import assert from "node:assert/strict";
import { spawn } from "node:child_process";
import fs from "node:fs";
import net from "node:net";
import os from "node:os";
import path from "node:path";
import { fileURLToPath } from "node:url";
const REPO = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
const SERVER = path.join(REPO, "providers", "studio-video", "export-server.mjs");
const freePort = () => new Promise((resolve, reject) => {
const server = net.createServer();
server.once("error", reject);
server.listen(0, "127.0.0.1", () => {
const { port } = server.address();
server.close(() => resolve(port));
});
});
async function startProvider({ projectRoot, mediaRoot }) {
const port = await freePort();
const proc = spawn(process.execPath, [
SERVER,
"--port", String(port),
"--slice-root", projectRoot,
"--media-root", mediaRoot,
], { cwd: REPO, stdio: ["ignore", "pipe", "pipe"] });
let log = "";
proc.stdout.on("data", (chunk) => { log += chunk; });
proc.stderr.on("data", (chunk) => { log += chunk; });
const endpoint = `http://127.0.0.1:${port}/api/library`;
const deadline = Date.now() + 10_000;
while (Date.now() < deadline) {
if (proc.exitCode != null) throw new Error(`provider exited ${proc.exitCode}:\n${log}`);
try {
const response = await fetch(endpoint);
if (response.ok) return { proc, endpoint, log: () => log };
} catch {}
await new Promise((resolve) => setTimeout(resolve, 75));
}
proc.kill();
throw new Error(`provider did not start:\n${log}`);
}
test("the library walks the media root the install actually declared", async (t) => {
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "studio-media-root-"));
const projectRoot = path.join(tmp, "project");
const mediaRoot = path.join(tmp, "media");
const outsideRoot = path.join(tmp, "outside");
fs.mkdirSync(projectRoot, { recursive: true });
fs.mkdirSync(path.join(mediaRoot, "library"), { recursive: true });
fs.mkdirSync(path.join(mediaRoot, "footage"), { recursive: true });
fs.mkdirSync(outsideRoot, { recursive: true });
const control = path.join(mediaRoot, "library", "known.mp4");
const expected = path.join(mediaRoot, "footage", "expected.mp4");
const outside = path.join(outsideRoot, "must-not-appear.mp4");
fs.writeFileSync(control, "positive control");
fs.writeFileSync(expected, "declared-root media");
fs.writeFileSync(outside, "outside binding");
const provider = await startProvider({ projectRoot, mediaRoot });
t.after(() => { provider.proc.kill(); fs.rmSync(tmp, { recursive: true, force: true }); });
const payload = await (await fetch(provider.endpoint)).json();
assert.equal(payload.ok, true, JSON.stringify(payload));
const paths = payload.assets.map((asset) => asset.path).sort();
const realControl = fs.realpathSync(control);
const realExpected = fs.realpathSync(expected);
const realOutside = fs.realpathSync(outside);
assert.ok(paths.includes(realControl), `positive control missing: the harness did not see root/library; payload=${JSON.stringify(payload)}`);
assert.ok(paths.includes(realExpected), "media under the declared root's footage/ subtree was silently narrowed out");
assert.ok(!paths.includes(realOutside), "provider escaped the declared media root");
});
+107
View File
@@ -0,0 +1,107 @@
import test from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
const REPO = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
const MANIFEST = path.join(REPO, "apps", "mini-nle", "app.json");
const SURFACE = path.join(REPO, "apps", "mini-nle", "app", "mini-nle.html");
const forbidden = [
"/api/note",
"/api/note/done",
"/api/note/flush",
"/api/reveal-file",
];
// Exact routes the surface legitimately uses. Parameterized export-status is
// asserted separately once the SDK's approved prefix syntax lands.
const requiredExact = [
"/api/annotation",
"/api/approve",
"/api/export-final",
"/api/export-review",
"/api/focus",
"/api/health",
"/api/history-since",
"/api/library",
"/api/library/media",
"/api/library/route",
"/api/media",
"/api/patch",
"/api/patch/apply",
"/api/patch/discard",
"/api/patch/validate",
"/api/probe-duration",
"/api/project-assets",
"/api/project-assets/rebuild",
"/api/project-info",
"/api/reconform",
"/api/slot-history",
"/api/tags",
"/api/templates",
"/api/thumb",
"/api/timeline/doc",
"/api/timeline/select",
"/api/timelines",
"/api/validate-audio",
"/api/validate-export",
];
const requiredPrefixes = ["/api/export-status/"];
function readContract() {
return {
manifest: JSON.parse(fs.readFileSync(MANIFEST, "utf8")),
surface: fs.readFileSync(SURFACE, "utf8"),
};
}
function missingRoutes(manifest) {
const declared = new Set(manifest.verbs ?? []);
return requiredExact.filter((route) => !declared.has(route));
}
test("MINI-NLE declares every legitimate exact route it uses", () => {
const { manifest } = readContract();
assert.deepEqual(missingRoutes(manifest), []);
});
test("MINI-NLE declares parameterized status polling with the SDK trailing-slash prefix contract", () => {
const { manifest, surface } = readContract();
assert.match(surface, /callExportApi\(`\/api\/export-status\/\$\{encodeURIComponent\(jobId\)\}`/);
for (const prefix of requiredPrefixes) assert.ok(manifest.verbs.includes(prefix), `${prefix} prefix is undeclared`);
});
test("the route checker demonstrably fails when a known declaration is removed", () => {
const { manifest } = readContract();
const altered = { ...manifest, verbs: (manifest.verbs ?? []).filter((route) => route !== "/api/patch") };
assert.ok(missingRoutes(altered).includes("/api/patch"));
});
test("ruled-out rig injection and Finder reveal have no live surface callers", () => {
const { manifest, surface } = readContract();
for (const route of forbidden) {
assert.ok(!(manifest.verbs ?? []).includes(route), `${route} must not be declared`);
const escaped = route.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
assert.doesNotMatch(
surface,
new RegExp(`(?:callExportApi|fetch)\\(\\s*[\\\"'\\x60]${escaped}(?:[?\\\"'\\x60])`),
`${route} still has a live network caller`,
);
}
});
test("disabled review handoff explains the AGENTS path at the interaction point", () => {
const { surface } = readContract();
assert.match(surface, /review handoff[^<\n]*AGENTS|AGENTS[^<\n]*review handoff/i);
});
test("dynamic export mode resolves to concrete declared review and final routes", () => {
const { manifest, surface } = readContract();
assert.match(surface, /callExportApi\(`\/api\/export-\$\{mode\}`/);
assert.ok(manifest.verbs.includes("/api/export-review"));
assert.ok(manifest.verbs.includes("/api/export-final"));
assert.ok(!manifest.verbs.includes("/api/export-"), "dynamic source fragment is not a route declaration");
});