@mevdragon/vidfarm-devcli 0.18.1 → 0.19.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/.agents/skills/editor-capabilities/SKILL.md +166 -0
  2. package/.agents/skills/editor-capabilities/references/re-theme-walkthrough.md +58 -0
  3. package/.agents/skills/vidfarm-media/SKILL.md +2 -0
  4. package/SKILL.director.md +174 -16
  5. package/SKILL.platform.md +7 -3
  6. package/demo/dist/app.css +1 -1
  7. package/demo/dist/app.js +79 -75
  8. package/dist/src/account-pages-legacy.js +1 -1
  9. package/dist/src/app.js +1694 -217
  10. package/dist/src/cli.js +1365 -105
  11. package/dist/src/config.js +8 -0
  12. package/dist/src/devcli/clips.js +3 -0
  13. package/dist/src/devcli/composition-edit.js +401 -10
  14. package/dist/src/devcli/local-backend.js +123 -0
  15. package/dist/src/devcli/migrate-local.js +140 -0
  16. package/dist/src/devcli/sync.js +311 -0
  17. package/dist/src/devcli/timeline-edit.js +208 -1
  18. package/dist/src/editor-chat.js +37 -18
  19. package/dist/src/frontend/file-directory.js +271 -20
  20. package/dist/src/frontend/homepage-view.js +3 -3
  21. package/dist/src/frontend/template-editor-chat.js +6 -3
  22. package/dist/src/page-shell.js +1 -1
  23. package/dist/src/primitive-context.js +31 -2
  24. package/dist/src/primitive-registry.js +409 -4
  25. package/dist/src/reskin/chat-page.js +103 -15
  26. package/dist/src/reskin/discover-page.js +247 -10
  27. package/dist/src/reskin/document.js +147 -10
  28. package/dist/src/reskin/inpaint-clipper-page.js +649 -0
  29. package/dist/src/reskin/inpaint-page.js +2459 -452
  30. package/dist/src/reskin/inpaint-video-page.js +1339 -0
  31. package/dist/src/reskin/library-page.js +324 -82
  32. package/dist/src/reskin/portfolio-page.js +687 -0
  33. package/dist/src/reskin/theme.js +36 -11
  34. package/dist/src/services/billing.js +4 -0
  35. package/dist/src/services/clip-curation/hunt.js +2 -0
  36. package/dist/src/services/clip-records.js +28 -0
  37. package/dist/src/services/file-directory.js +6 -3
  38. package/dist/src/services/hyperframes.js +535 -86
  39. package/dist/src/services/serverless-jobs.js +3 -1
  40. package/dist/src/services/serverless-records.js +43 -0
  41. package/dist/src/services/storage.js +24 -2
  42. package/dist/src/template-editor-pages.js +1 -1
  43. package/package.json +1 -1
  44. package/public/assets/file-directory-app.js +2 -2
  45. package/public/assets/homepage-client-app.js +1 -1
  46. package/public/assets/page-runtime-client-app.js +2 -2
  47. package/public/assets/placeholders/scene-placeholder.png +0 -0
@@ -16,7 +16,7 @@
16
16
  // callbacks, or, for the auto-mounted dock instance, read from a
17
17
  // `window.__rkDirectoryHost` hook the dock's inline script sets synchronously
18
18
  // before this deferred module runs.
19
- import { buildDirectoryPath, DIRECTORY_ROOTS, normalizeFolderPath, parseDirectoryPath } from "../services/file-directory.js";
19
+ import { buildDirectoryPath, DIRECTORY_ROOT_KEYS, DIRECTORY_ROOTS, normalizeFolderPath, parseDirectoryPath } from "../services/file-directory.js";
20
20
  const DEFAULT_API_BASE = "/api/v1/user/me";
21
21
  // ── small DOM helpers ────────────────────────────────────────────────────────
22
22
  function el(tag, cls, text) {
@@ -159,13 +159,16 @@ function openMenuAt(anchor, actions) {
159
159
  // ── the explorer ─────────────────────────────────────────────────────────────
160
160
  export function createDirectoryExplorer(mount, opts = {}) {
161
161
  const apiBase = opts.apiBase || DEFAULT_API_BASE;
162
- const roots = (opts.roots && opts.roots.length ? opts.roots : ["files", "temp", "raws"]);
162
+ const roots = (opts.roots && opts.roots.length ? opts.roots : DIRECTORY_ROOT_KEYS.slice());
163
163
  const singleRoot = roots.length === 1 ? roots[0] : null;
164
164
  const mode = opts.mode || "attach";
165
165
  const showSearch = opts.showSearch !== false;
166
166
  const showMultiSelect = opts.showMultiSelect !== false;
167
167
  const showUpload = opts.showUpload != null ? opts.showUpload : mode === "attach";
168
168
  const primaryClick = opts.primaryClick || "copyPath";
169
+ const cloudAvailable = opts.cloudAvailable != null
170
+ ? opts.cloudAvailable
171
+ : typeof window !== "undefined" && window.__vidfarmCloudSpace === true;
169
172
  const host = window.__rkDirectoryHost || {};
170
173
  const emitAttach = (items) => (opts.onAttach || host.onAttach || (() => showToast("Attached " + items.length + " item(s)")))(items);
171
174
  const emitPreview = (items, i) => {
@@ -189,6 +192,11 @@ export function createDirectoryExplorer(mount, opts = {}) {
189
192
  let searchResults = [];
190
193
  let searchStatus = "idle";
191
194
  let destroyed = false;
195
+ // Which backend the explorer reads: "local" (this box's disk backend) or
196
+ // "cloud" (vidfarm.cc via the serve-box passthrough). Only meaningful when
197
+ // cloudAvailable; the /cloud fetches hit /me/cloud/directory*.
198
+ let space = "local";
199
+ function spacePrefix() { return space === "cloud" ? "/cloud" : ""; }
192
200
  function normalizeInitial(p, single) {
193
201
  const parsed = parseDirectoryPath(p || "");
194
202
  if (parsed.root)
@@ -216,6 +224,33 @@ export function createDirectoryExplorer(mount, opts = {}) {
216
224
  dropInput.hidden = true;
217
225
  dropEl.appendChild(dropText);
218
226
  dropEl.appendChild(dropInput);
227
+ // Local ⇄ Cloud space switch (serve boxes with an upstream only).
228
+ const spaceBar = el("div", "rk-dir-space");
229
+ const localBtn = el("button", "rk-dir-space-btn is-on", "Local");
230
+ const cloudBtn = el("button", "rk-dir-space-btn", "Cloud");
231
+ localBtn.type = "button";
232
+ cloudBtn.type = "button";
233
+ spaceBar.appendChild(localBtn);
234
+ spaceBar.appendChild(cloudBtn);
235
+ function setSpace(next) {
236
+ if (space === next)
237
+ return;
238
+ space = next;
239
+ localBtn.classList.toggle("is-on", next === "local");
240
+ cloudBtn.classList.toggle("is-on", next === "cloud");
241
+ // Reset to the space's root view and clear any active search.
242
+ searchMode = false;
243
+ searchResults = [];
244
+ searchInput.value = "";
245
+ selection.clear();
246
+ currentPath = singleRoot ? "/" + singleRoot : "/";
247
+ status = "idle";
248
+ load(true);
249
+ }
250
+ localBtn.addEventListener("click", () => setSpace("local"));
251
+ cloudBtn.addEventListener("click", () => setSpace("cloud"));
252
+ if (cloudAvailable)
253
+ mount.appendChild(spaceBar);
219
254
  if (showSearch)
220
255
  mount.appendChild(searchWrap);
221
256
  mount.appendChild(crumbsEl);
@@ -226,7 +261,7 @@ export function createDirectoryExplorer(mount, opts = {}) {
226
261
  mount.appendChild(dropEl);
227
262
  // ── fetch ──
228
263
  function dirUrl(path, offset) {
229
- const u = new URL(apiBase + "/directory", window.location.href);
264
+ const u = new URL(apiBase + spacePrefix() + "/directory", window.location.href);
230
265
  u.searchParams.set("path", path);
231
266
  if (offset)
232
267
  u.searchParams.set("offset", String(offset));
@@ -313,7 +348,7 @@ export function createDirectoryExplorer(mount, opts = {}) {
313
348
  searchStatus = "loading";
314
349
  render();
315
350
  const scope = singleRoot ? "/" + singleRoot : (currentPath !== "/" ? currentPath : "");
316
- fetch(apiBase + "/directory/search", {
351
+ fetch(apiBase + spacePrefix() + "/directory/search", {
317
352
  method: "POST",
318
353
  credentials: "same-origin",
319
354
  headers: { "content-type": "application/json", accept: "application/json" },
@@ -339,10 +374,14 @@ export function createDirectoryExplorer(mount, opts = {}) {
339
374
  status = "idle";
340
375
  searchMode = false;
341
376
  searchInput.value = "";
377
+ (opts.onNavigate || (() => { }))(currentPath);
342
378
  load(true);
343
379
  }
344
380
  function currentRoot() { return parseDirectoryPath(currentPath).root; }
345
381
  function currentFolder() { return parseDirectoryPath(currentPath).folderPath; }
382
+ // /projects mirrors the user's composition forks — its folders and files are
383
+ // backed by fork storage, not user-managed, so no create/rename/upload there.
384
+ function canWriteRoot(r) { return !!r && r !== "projects"; }
346
385
  // Create an empty folder under `parentFolder` (defaults to the current folder).
347
386
  // Only valid inside a root — at the virtual root there's nowhere to put it.
348
387
  function createFolder(parentFolder) {
@@ -351,6 +390,10 @@ export function createDirectoryExplorer(mount, opts = {}) {
351
390
  toast("Open a root (/files, /temp, /raws) first");
352
391
  return;
353
392
  }
393
+ if (!canWriteRoot(root)) {
394
+ toast("/projects is read-only");
395
+ return;
396
+ }
354
397
  const name = (window.prompt("New folder name") || "").trim();
355
398
  if (!name)
356
399
  return;
@@ -366,6 +409,33 @@ export function createDirectoryExplorer(mount, opts = {}) {
366
409
  .then((res) => { navigate(res.path || (root === currentRoot() ? currentPath : "/" + root)); toast("Created " + (res.path || name)); })
367
410
  .catch((e) => toast(e.message || "Couldn't create folder"));
368
411
  }
412
+ // Rename a file or folder in place via POST /me/directory/rename. Files carry
413
+ // their backend id (display-name-only rename); folders rename their trailing
414
+ // path segment (files + nested subfolders move with them server-side).
415
+ function renameItem(it) {
416
+ const isFolder = it.kind === "folder";
417
+ const next = (window.prompt(isFolder ? "Rename folder" : "Rename file", it.name) || "").trim();
418
+ if (!next || next === it.name)
419
+ return;
420
+ fetch(apiBase + "/directory/rename", {
421
+ method: "POST",
422
+ credentials: "same-origin",
423
+ headers: { "content-type": "application/json", accept: "application/json" },
424
+ body: JSON.stringify({ path: it.path, new_name: next, ...(isFolder ? {} : { file_id: it.id }) })
425
+ })
426
+ .then((r) => (r.ok ? r.json() : r.json().then((j) => Promise.reject(new Error(j.error || "http " + r.status)))))
427
+ .then(() => {
428
+ // Renaming shifts the item's path, so drop any stale selection and
429
+ // reload the current folder (the renamed child re-renders in place).
430
+ if (selection.has(it.path)) {
431
+ selection.delete(it.path);
432
+ (opts.onSelectionChange || (() => { }))(Array.from(selection.values()));
433
+ }
434
+ load(true);
435
+ toast("Renamed to " + next);
436
+ })
437
+ .catch((e) => toast(e.message || "Couldn't rename"));
438
+ }
369
439
  // ── selection ──
370
440
  function toggleSelect(it, on) {
371
441
  if (on)
@@ -412,11 +482,14 @@ export function createDirectoryExplorer(mount, opts = {}) {
412
482
  crumbsEl.appendChild(crumb(part, idx === arr.length - 1, () => navigate(target)));
413
483
  });
414
484
  // Discoverable "New folder" affordance (right-click also works anywhere).
415
- const nf = el("button", "rk-dir-newfolder", "+ Folder");
416
- nf.type = "button";
417
- nf.title = "New folder here (or right-click)";
418
- nf.addEventListener("click", () => createFolder());
419
- crumbsEl.appendChild(nf);
485
+ // Read-only roots (/projects) don't get it.
486
+ if (canWriteRoot(root)) {
487
+ const nf = el("button", "rk-dir-newfolder", "+ Folder");
488
+ nf.type = "button";
489
+ nf.title = "New folder here (or right-click)";
490
+ nf.addEventListener("click", () => createFolder());
491
+ crumbsEl.appendChild(nf);
492
+ }
420
493
  }
421
494
  function sep() { return el("span", "rk-aichat-crumb-sep", "/"); }
422
495
  function renderSelbar() {
@@ -475,12 +548,14 @@ export function createDirectoryExplorer(mount, opts = {}) {
475
548
  open.title = "Open folder";
476
549
  open.addEventListener("click", (e) => { e.stopPropagation(); navigate(it.path); });
477
550
  row.appendChild(open);
478
- // Kebab.
479
- row.appendChild(kebab(it, [
480
- { label: "Open folder", run: () => navigate(it.path) },
481
- { label: "New subfolder", run: () => createFolder(it.folderPath) },
482
- { label: "Copy path", run: () => copyPath(it.path) }
483
- ]));
551
+ // Kebab. Create/rename only for writable roots (not /projects forks).
552
+ const folderActions = [{ label: "Open folder", run: () => navigate(it.path) }];
553
+ if (canWriteRoot(it.root)) {
554
+ folderActions.push({ label: "New subfolder", run: () => createFolder(it.folderPath) });
555
+ folderActions.push({ label: "Rename", run: () => renameItem(it) });
556
+ }
557
+ folderActions.push({ label: "Copy path", run: () => copyPath(it.path) });
558
+ row.appendChild(kebab(it, folderActions));
484
559
  return row;
485
560
  }
486
561
  function fileRowEl(it, siblings, index, showPath) {
@@ -507,6 +582,17 @@ export function createDirectoryExplorer(mount, opts = {}) {
507
582
  copyPath(it.path);
508
583
  });
509
584
  row.appendChild(b);
585
+ // When the row's primary click previews (chat dock), keep attaching one click
586
+ // away with a dedicated inline "+" button so files stay easy to add to the
587
+ // composer without going through the kebab.
588
+ if (mode === "attach" && primaryClick === "preview") {
589
+ const add = el("button", "rk-aichat-fadd rk-dir-attach", "+");
590
+ add.type = "button";
591
+ add.title = "Attach " + it.name;
592
+ add.setAttribute("aria-label", "Attach " + it.name);
593
+ add.addEventListener("click", (e) => { e.stopPropagation(); emitAttach([it]); });
594
+ row.appendChild(add);
595
+ }
510
596
  // Kebab: Attach / View / Preview / Copy path.
511
597
  const actions = [];
512
598
  if (mode === "attach")
@@ -515,6 +601,10 @@ export function createDirectoryExplorer(mount, opts = {}) {
515
601
  actions.push({ label: "View", run: () => window.open(it.viewUrl, "_blank", "noopener") });
516
602
  if (it.viewUrl)
517
603
  actions.push({ label: "Preview", run: () => emitPreview(siblings, index) });
604
+ // Raws (source-named) and approved posts (title-named) can't be file-renamed;
605
+ // only /files + /temp files rename in place. Folders rename in every root.
606
+ if ((it.root === "files" || it.root === "temp") && it.id)
607
+ actions.push({ label: "Rename", run: () => renameItem(it) });
518
608
  actions.push({ label: "Copy path", run: () => copyPath(it.path) });
519
609
  row.appendChild(kebab(it, actions));
520
610
  return row;
@@ -663,8 +753,10 @@ export function createDirectoryExplorer(mount, opts = {}) {
663
753
  const it = rowEl ? findItem(rowEl.getAttribute("data-path") || "") : undefined;
664
754
  if (it && it.kind === "folder") {
665
755
  actions.push({ label: "Open folder", run: () => navigate(it.path) });
666
- if (rootSet)
756
+ if (canWriteRoot(it.root))
667
757
  actions.push({ label: "New subfolder", run: () => createFolder(it.folderPath) });
758
+ if (canWriteRoot(it.root))
759
+ actions.push({ label: "Rename", run: () => renameItem(it) });
668
760
  actions.push({ label: "Copy path", run: () => copyPath(it.path) });
669
761
  }
670
762
  else if (it) {
@@ -674,10 +766,12 @@ export function createDirectoryExplorer(mount, opts = {}) {
674
766
  actions.push({ label: "View", run: () => window.open(it.viewUrl, "_blank", "noopener") });
675
767
  if (it.viewUrl)
676
768
  actions.push({ label: "Preview", run: () => emitPreview(files, files.indexOf(it)) });
769
+ if (it.root !== "raws" && canWriteRoot(it.root) && it.id)
770
+ actions.push({ label: "Rename", run: () => renameItem(it) });
677
771
  actions.push({ label: "Copy path", run: () => copyPath(it.path) });
678
772
  }
679
- // Always offer "New folder" in the current folder when inside a root.
680
- if (rootSet)
773
+ // Always offer "New folder" in the current folder when inside a writable root.
774
+ if (rootSet && canWriteRoot(currentRoot()))
681
775
  actions.push({ label: "New folder here", run: () => createFolder() });
682
776
  if (!actions.length)
683
777
  return; // virtual root, empty space → let the browser menu show
@@ -698,11 +792,13 @@ export function createDirectoryExplorer(mount, opts = {}) {
698
792
  if (e.dataTransfer && e.dataTransfer.files)
699
793
  uploadFiles(e.dataTransfer.files);
700
794
  });
795
+ (opts.onNavigate || (() => { }))(currentPath);
701
796
  load(true);
702
797
  return {
703
798
  refresh: () => load(true),
704
799
  navigate,
705
800
  getSelection: () => Array.from(selection.values()),
801
+ getCurrentPath: () => currentPath,
706
802
  destroy: () => { destroyed = true; if (closeOpenMenu)
707
803
  closeOpenMenu(); mount.innerHTML = ""; }
708
804
  };
@@ -712,6 +808,160 @@ function cssEscape(s) {
712
808
  return window.CSS.escape(s);
713
809
  return s.replace(/["\\]/g, "\\$&");
714
810
  }
811
+ // ── pop-panel picker (modal) ──────────────────────────────────────────────────
812
+ // A self-contained modal wrapper around createDirectoryExplorer so any surface
813
+ // (editor "Replace media", inpaint "Save to Files", …) can pick a file or a
814
+ // destination folder without reimplementing the overlay + CSS each time.
815
+ const PICKER_STYLE_ID = "rk-dirpick-style";
816
+ function ensurePickerStyle() {
817
+ if (document.getElementById(PICKER_STYLE_ID))
818
+ return;
819
+ const s = document.createElement("style");
820
+ s.id = PICKER_STYLE_ID;
821
+ // Uses reskin --rk-* tokens where present, with plain fallbacks so the modal
822
+ // is legible even on a page that hasn't loaded the design-system variables.
823
+ s.textContent = [
824
+ ".rk-dirpick{position:fixed;inset:0;z-index:1200;display:grid;place-items:center;padding:20px;",
825
+ "background:rgba(28,25,23,.44);backdrop-filter:blur(2px);animation:rk-dirpick-fade .16s ease}",
826
+ "@keyframes rk-dirpick-fade{from{opacity:0}to{opacity:1}}",
827
+ ".rk-dirpick-panel{width:min(560px,100%);max-height:min(82vh,700px);display:flex;flex-direction:column;",
828
+ "background:var(--rk-surface,#fff);border:1px solid var(--rk-border,#e6e0d6);border-radius:var(--rk-r-3xl,20px);",
829
+ "box-shadow:var(--rk-shadow-lg,0 24px 60px rgba(28,25,23,.28));overflow:hidden}",
830
+ ".rk-dirpick-head{display:flex;align-items:center;justify-content:space-between;gap:10px;padding:14px 18px;",
831
+ "border-bottom:1px solid var(--rk-border,#e6e0d6)}",
832
+ ".rk-dirpick-title{font-family:var(--rk-font-display,inherit);font-weight:800;font-size:15px;color:var(--rk-ink,#1c1917)}",
833
+ ".rk-dirpick-close{border:0;background:transparent;font-size:22px;line-height:1;color:var(--rk-text-muted,#78716c);cursor:pointer;padding:0 4px}",
834
+ ".rk-dirpick-close:hover{color:var(--rk-ink,#1c1917)}",
835
+ ".rk-dirpick-hint{padding:10px 18px 0;font-size:12.5px;line-height:1.4;color:var(--rk-text-muted,#78716c)}",
836
+ ".rk-dirpick-mount{padding:12px 14px;overflow:auto;flex:1;min-height:0}",
837
+ ".rk-dirpick-foot{display:flex;align-items:center;gap:10px;padding:12px 18px;border-top:1px solid var(--rk-border,#e6e0d6)}",
838
+ ".rk-dirpick-dest{flex:1;min-width:0;font-size:12.5px;color:var(--rk-text-muted,#78716c);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}",
839
+ ".rk-dirpick-dest b{color:var(--rk-ink,#1c1917);font-weight:700}",
840
+ ".rk-dirpick-btn{flex:none;min-height:36px;padding:0 16px;border-radius:999px;border:0;cursor:pointer;",
841
+ "font:inherit;font-size:13px;font-weight:800;background:var(--rk-gold-500,var(--rk-gold-600,#fcb900));color:var(--rk-ink,#1c1917)}",
842
+ ".rk-dirpick-btn:hover:not(:disabled){background:var(--rk-gold-600,#e0a500)}",
843
+ ".rk-dirpick-btn:disabled{opacity:.5;cursor:default}",
844
+ ".rk-dirpick-btn.is-ghost{background:transparent;border:1px solid var(--rk-border,#e6e0d6);color:var(--rk-ink,#1c1917)}"
845
+ ].join("");
846
+ document.head.appendChild(s);
847
+ }
848
+ export function openDirectoryPicker(opts = {}) {
849
+ ensurePickerStyle();
850
+ const select = opts.select === "folder" ? "folder" : "file";
851
+ let done = false;
852
+ let explorer = null;
853
+ const overlay = el("div", "rk-dirpick");
854
+ const panel = el("div", "rk-dirpick-panel");
855
+ const head = el("div", "rk-dirpick-head");
856
+ head.appendChild(el("span", "rk-dirpick-title", opts.title || (select === "folder" ? "Choose a folder" : "Choose a file")));
857
+ const closeBtn = el("button", "rk-dirpick-close");
858
+ closeBtn.type = "button";
859
+ closeBtn.setAttribute("aria-label", "Close");
860
+ closeBtn.innerHTML = "×";
861
+ head.appendChild(closeBtn);
862
+ panel.appendChild(head);
863
+ if (opts.hint)
864
+ panel.appendChild(el("div", "rk-dirpick-hint", opts.hint));
865
+ const mountEl = el("div", "rk-dirpick-mount");
866
+ panel.appendChild(mountEl);
867
+ // Folder mode gets a footer that commits the currently-open folder.
868
+ let destEl = null;
869
+ let confirmBtn = null;
870
+ function finish(result) {
871
+ if (done)
872
+ return;
873
+ done = true;
874
+ close();
875
+ if (result)
876
+ (opts.onPick || (() => { }))(result);
877
+ else
878
+ (opts.onCancel || (() => { }))();
879
+ }
880
+ function folderItemFor(path) {
881
+ const parsed = parseDirectoryPath(path);
882
+ if (!parsed.root)
883
+ return null; // the virtual root "/" isn't a real folder
884
+ const folder = parsed.folderPath || "";
885
+ const name = folder ? folder.split("/").filter(Boolean).pop() || folder : parsed.root;
886
+ return { path: buildDirectoryPath(parsed.root, folder), root: parsed.root, folderPath: folder, name, kind: "folder" };
887
+ }
888
+ function syncFooter(path) {
889
+ if (!destEl || !confirmBtn)
890
+ return;
891
+ const item = folderItemFor(path);
892
+ if (item) {
893
+ destEl.innerHTML = "";
894
+ destEl.appendChild(document.createTextNode("Save to "));
895
+ const b = el("b", undefined, item.path);
896
+ destEl.appendChild(b);
897
+ confirmBtn.disabled = false;
898
+ }
899
+ else {
900
+ destEl.textContent = "Open a folder (Files, Temp, …) to choose a destination.";
901
+ confirmBtn.disabled = true;
902
+ }
903
+ }
904
+ if (select === "folder") {
905
+ const foot = el("div", "rk-dirpick-foot");
906
+ destEl = el("div", "rk-dirpick-dest");
907
+ const cancel = el("button", "rk-dirpick-btn is-ghost", "Cancel");
908
+ cancel.type = "button";
909
+ cancel.addEventListener("click", () => finish(null));
910
+ confirmBtn = el("button", "rk-dirpick-btn", opts.confirmLabel || "Use this folder");
911
+ confirmBtn.type = "button";
912
+ confirmBtn.addEventListener("click", () => {
913
+ const item = explorer ? folderItemFor(explorer.getCurrentPath()) : null;
914
+ if (item)
915
+ finish(item);
916
+ });
917
+ foot.appendChild(destEl);
918
+ foot.appendChild(cancel);
919
+ foot.appendChild(confirmBtn);
920
+ panel.appendChild(foot);
921
+ }
922
+ overlay.appendChild(panel);
923
+ document.body.appendChild(overlay);
924
+ function onKey(e) { if (e.key === "Escape")
925
+ finish(null); }
926
+ function close() {
927
+ document.removeEventListener("keydown", onKey, true);
928
+ try {
929
+ explorer && explorer.destroy();
930
+ }
931
+ catch { /* noop */ }
932
+ overlay.remove();
933
+ }
934
+ closeBtn.addEventListener("click", () => finish(null));
935
+ overlay.addEventListener("click", (e) => { if (e.target === overlay)
936
+ finish(null); });
937
+ document.addEventListener("keydown", onKey, true);
938
+ explorer = createDirectoryExplorer(mountEl, {
939
+ roots: opts.roots,
940
+ initialPath: opts.initialPath,
941
+ apiBase: opts.apiBase,
942
+ // Standalone hides Attach chrome; picking is our own affordance.
943
+ mode: "standalone",
944
+ showSearch: true,
945
+ showMultiSelect: false,
946
+ showUpload: false, // the picker only selects; it doesn't upload
947
+ // In file mode a name-click resolves the pick; folders still navigate.
948
+ primaryClick: select === "file" ? "attach" : "copyPath",
949
+ onNavigate: (p) => syncFooter(p),
950
+ onAttach: (items) => {
951
+ if (select !== "file")
952
+ return;
953
+ const it = items && items[0];
954
+ if (!it)
955
+ return;
956
+ if (opts.accept && !opts.accept(it)) {
957
+ showToast(opts.rejectMessage || "That file can't be used here.");
958
+ return;
959
+ }
960
+ finish(it);
961
+ }
962
+ });
963
+ return { close: () => finish(null) };
964
+ }
715
965
  // ── auto-mount + global bridge ───────────────────────────────────────────────
716
966
  function readDataOpts(node) {
717
967
  const d = node.dataset;
@@ -723,7 +973,8 @@ function readDataOpts(node) {
723
973
  showSearch: d.showSearch !== "false",
724
974
  showMultiSelect: d.showMultiselect !== "false",
725
975
  showUpload: d.showUpload != null ? d.showUpload === "true" : undefined,
726
- primaryClick: d.primaryClick || "copyPath"
976
+ primaryClick: d.primaryClick || "copyPath",
977
+ cloudAvailable: d.cloudAvailable != null ? d.cloudAvailable === "true" : undefined
727
978
  };
728
979
  }
729
980
  function autoMount() {
@@ -735,7 +986,7 @@ function autoMount() {
735
986
  window.__rkDirectoryInstance = createDirectoryExplorer(node, readDataOpts(node));
736
987
  });
737
988
  }
738
- window.__vidfarmDirectory = { mount: createDirectoryExplorer };
989
+ window.__vidfarmDirectory = { mount: createDirectoryExplorer, pick: openDirectoryPicker };
739
990
  if (document.readyState === "loading")
740
991
  document.addEventListener("DOMContentLoaded", autoMount);
741
992
  else
@@ -66,7 +66,7 @@ function PrimaryNav({ account }) {
66
66
  ? [
67
67
  { id: "chat", href: withAccountQuery("/chat", account.userId), label: "Chat" },
68
68
  { id: "job-runs", href: withAccountQuery("/job-runs", account.userId), label: "Run History" },
69
- { id: "inpaint", href: withAccountQuery("/inpaint", account.userId), label: "Inpaint" },
69
+ { id: "tools", href: withAccountQuery("/tools/image", account.userId), label: "Tools" },
70
70
  { id: "help", href: "/help", label: "Help" },
71
71
  { id: "logout", action: "logout", label: "Log Out" }
72
72
  ]
@@ -286,7 +286,7 @@ function editorHref(templateId, accountUserId) {
286
286
  : withAccountQuery(`/templates/${encodeURIComponent(templateId)}/editor/docs`, accountUserId);
287
287
  }
288
288
  function CreateButton({ templateId, accountUserId }) {
289
- return _jsx("a", { className: "cta-button", href: editorHref(templateId, accountUserId), children: "Editor" });
289
+ return _jsx("a", { className: "cta-button", href: editorHref(templateId, accountUserId), target: "_blank", rel: "noopener", children: "Editor" });
290
290
  }
291
291
  function CardMenu(input) {
292
292
  const { template } = input;
@@ -343,7 +343,7 @@ function CardMenu(input) {
343
343
  window.alert(result.error || "Unable to delete this template.");
344
344
  }
345
345
  };
346
- return (_jsxs("div", { className: "discover-card-menu", ref: menuRef, children: [_jsx("button", { type: "button", className: "discover-card-menu-trigger", "aria-haspopup": "menu", "aria-expanded": isOpen, "aria-label": `More actions for ${template.title}`, title: "More actions", onClick: () => setIsOpen((prev) => !prev), children: _jsx(MoreIcon, {}) }), isOpen ? (_jsxs("div", { className: "discover-card-menu-panel", role: "menu", children: [_jsx("button", { type: "button", className: "discover-card-menu-item", role: "menuitem", onClick: () => void handleCopyId(), children: copied ? "Copied!" : "Copy ID" }), isPending ? (_jsx("span", { className: "discover-card-menu-item is-disabled", role: "menuitem", "aria-disabled": "true", children: "Open in Editor" })) : (_jsx("a", { className: "discover-card-menu-item", role: "menuitem", href: editorHref(template.templateId, input.accountUserId), children: "Open in Editor" })), _jsx("button", { type: "button", className: "discover-card-menu-item", role: "menuitem", onClick: () => {
346
+ return (_jsxs("div", { className: "discover-card-menu", ref: menuRef, children: [_jsx("button", { type: "button", className: "discover-card-menu-trigger", "aria-haspopup": "menu", "aria-expanded": isOpen, "aria-label": `More actions for ${template.title}`, title: "More actions", onClick: () => setIsOpen((prev) => !prev), children: _jsx(MoreIcon, {}) }), isOpen ? (_jsxs("div", { className: "discover-card-menu-panel", role: "menu", children: [_jsx("button", { type: "button", className: "discover-card-menu-item", role: "menuitem", onClick: () => void handleCopyId(), children: copied ? "Copied!" : "Copy ID" }), isPending ? (_jsx("span", { className: "discover-card-menu-item is-disabled", role: "menuitem", "aria-disabled": "true", children: "Open in Editor" })) : (_jsx("a", { className: "discover-card-menu-item", role: "menuitem", href: editorHref(template.templateId, input.accountUserId), target: "_blank", rel: "noopener", children: "Open in Editor" })), _jsx("button", { type: "button", className: "discover-card-menu-item", role: "menuitem", onClick: () => {
347
347
  input.onToggleBookmark?.(template.templateId);
348
348
  setIsOpen(false);
349
349
  }, children: input.isBookmarked ? "Remove Bookmark" : "Bookmark" }), canDelete ? (_jsx("button", { type: "button", className: "discover-card-menu-item is-danger", role: "menuitem", disabled: deleting, onClick: () => void handleDelete(), children: deleting ? "Deleting…" : "Delete" })) : null] })) : null] }));
@@ -390,8 +390,11 @@ function collectLabeledUrls(value, output, seen = new Set(), path = []) {
390
390
  }
391
391
  }
392
392
  function buildJobQueuedMessage(job) {
393
+ // MUST stay string-identical to buildAsyncJobHandoffText in src/editor-chat.ts —
394
+ // the server streams that text and this client copy dedupes it by substring.
395
+ // Job-KIND-agnostic ("job", not "render"): fires for any queued async job.
393
396
  return [
394
- `Started render \`${job.job_id}\`${job.tracer ? ` with tracer \`${job.tracer}\`` : ""}.`,
397
+ `Started job \`${job.job_id}\`${job.tracer ? ` with tracer \`${job.tracer}\`` : ""}.`,
395
398
  "If it takes a while, ask for an update later and I can check it for you."
396
399
  ].join("\n\n");
397
400
  }
@@ -1479,7 +1482,7 @@ function mediaInpaintHref(preview, ensureTracer = false) {
1479
1482
  if (typeof window === "undefined" || preview.kind !== "image" || !preview.url) {
1480
1483
  return "";
1481
1484
  }
1482
- const pageUrl = new URL("/inpaint", window.location.origin);
1485
+ const pageUrl = new URL("/tools/image", window.location.origin);
1483
1486
  const bridge = window.__vidfarmEditorTracers;
1484
1487
  const existingTracers = bridge?.get() ?? readTracersFromLocation();
1485
1488
  const tracer = existingTracers[0] || (ensureTracer ? bridge?.add() ?? "" : "");
@@ -1500,7 +1503,7 @@ function MediaPreviewModal(props) {
1500
1503
  const activePreview = gallery[activeIndex] || props.preview;
1501
1504
  const canNavigate = gallery.length > 1;
1502
1505
  const canInpaint = activePreview.kind === "image";
1503
- const initialInpaintHref = canInpaint ? mediaInpaintHref(activePreview, false) || "/inpaint" : "/inpaint";
1506
+ const initialInpaintHref = canInpaint ? mediaInpaintHref(activePreview, false) || "/tools/image" : "/tools/image";
1504
1507
  useEffect(() => {
1505
1508
  setIsMounted(true);
1506
1509
  }, []);
@@ -82,7 +82,7 @@ export function renderPrimaryNav(input) {
82
82
  ? [
83
83
  { id: "chat", href: withAccountQuery("/chat", input.accountId), label: "Chat" },
84
84
  { id: "job-runs", href: withAccountQuery("/job-runs", input.accountId), label: "Run History" },
85
- { id: "inpaint", href: withAccountQuery("/inpaint", input.accountId), label: "Inpaint" },
85
+ { id: "tools", href: withAccountQuery("/tools/image", input.accountId), label: "Tools" },
86
86
  { id: "help", href: "/help", label: "Help" },
87
87
  { id: "logout", action: "logout", label: "Log Out" }
88
88
  ]
@@ -4,6 +4,7 @@ import { config } from "./config.js";
4
4
  import { devErrorFields, devLog } from "./lib/dev-log.js";
5
5
  import { createId } from "./lib/ids.js";
6
6
  import { jobLogStore } from "./services/job-logs.js";
7
+ import { RenderInProgressError } from "./services/hyperframes.js";
7
8
  import { estimateNormalizationCostUsd, normalizeVideoAsset } from "./services/video-normalization.js";
8
9
  export function createPrimitiveJobContext(input) {
9
10
  const prefix = input.storage.primitiveJobPrefix(input.primitive.id, input.customer.id, input.job.id);
@@ -202,11 +203,30 @@ export function createPrimitiveJobContext(input) {
202
203
  hyperframes: {
203
204
  async render(request) {
204
205
  const hyperframesOutputKey = `renders/vidfarm/primitives/${input.primitive.id}/${input.customer.id}/${input.job.id}/output.mp4`;
205
- const render = await input.hyperframes.render({
206
+ // Re-entrant, non-blocking render: the first invocation dispatches the
207
+ // render to its own Step Functions state machine and throws
208
+ // RenderInProgressError so the job runner parks as `waiting_for_render`;
209
+ // Step Functions waits + re-invokes, and a later invocation resumes the
210
+ // SAME render (keyed by the deterministic execution name) and finalizes
211
+ // once it terminates. No single Lambda holds the render open for its
212
+ // 15-minute lifetime.
213
+ const step = await input.hyperframes.renderStep({
206
214
  ...request,
207
215
  outputKey: hyperframesOutputKey,
208
- wait: true
216
+ executionName: renderExecutionName(input.job.id)
209
217
  });
218
+ if (step.state === "running") {
219
+ throw new RenderInProgressError({
220
+ executionArn: step.executionArn,
221
+ renderId: step.renderId,
222
+ waitSeconds: config.RENDER_POLL_WAIT_SECONDS,
223
+ progressFraction: step.progressFraction
224
+ });
225
+ }
226
+ if (step.state === "failed") {
227
+ throw new Error(step.message);
228
+ }
229
+ const render = step;
210
230
  let outputUrl = render.outputUrl;
211
231
  let platformOutputKey = null;
212
232
  const localOutputLocation = typeof render.metadata.outputLocation === "string"
@@ -370,4 +390,13 @@ export function createPrimitiveJobContext(input) {
370
390
  function buildPlatformMediaUrl(storageKey) {
371
391
  return `${config.PUBLIC_BASE_URL.replace(/\/$/, "")}/template-media?key=${encodeURIComponent(storageKey)}`;
372
392
  }
393
+ /**
394
+ * Deterministic Step Functions execution name for a job's render. Stable across
395
+ * job-runner re-invocations so each poll resumes the same render execution
396
+ * rather than starting a new one. Step Functions execution names allow
397
+ * [A-Za-z0-9-_] and cap at 80 chars.
398
+ */
399
+ function renderExecutionName(jobId) {
400
+ return `vfr-${jobId.replace(/[^A-Za-z0-9_-]/g, "-")}`.slice(0, 80);
401
+ }
373
402
  //# sourceMappingURL=primitive-context.js.map