@officexapp/vidfarm-devcli 0.21.11 → 0.21.14

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 (28) hide show
  1. package/.agents/skills/hyperframes-core/SKILL.md +2 -0
  2. package/.agents/skills/hyperframes-creative/SKILL.md +1 -0
  3. package/.agents/skills/hyperframes-creative/references/beat-direction.md +17 -0
  4. package/.agents/skills/vidfarm-director/SKILL.md +4 -0
  5. package/.agents/skills/vidfarm-director/recipes/local-edit-render-approve.md +1 -1
  6. package/.agents/skills/vidfarm-director/references/automation-and-local-dev.md +23 -6
  7. package/.agents/skills/vidfarm-director/references/core-workflows.md +1 -1
  8. package/.agents/skills/vidfarm-director/references/editor-workflows.md +15 -2
  9. package/.agents/skills/vidfarm-director/references/primitives.md +1 -1
  10. package/.agents/skills/vidfarm-director/references/rest-api.md +1 -1
  11. package/.agents/skills/vidfarm-media/SKILL.md +16 -6
  12. package/README.md +2 -2
  13. package/SKILL.director.md +45 -11
  14. package/demo/dist/app.js +247 -226
  15. package/dist/src/cli.js +198 -30
  16. package/dist/src/devcli/composition-edit.js +99 -23
  17. package/dist/src/devcli/cost-mode.js +13 -4
  18. package/dist/src/devcli/doctor.js +65 -9
  19. package/dist/src/devcli/local-frontend-server.js +342 -50
  20. package/dist/src/devcli/port-utils.js +43 -0
  21. package/dist/src/devcli/process-scan.js +173 -0
  22. package/package.json +3 -1
  23. package/public/serve-shells/editor.html +75 -13
  24. package/public/serve-shells/library-files.html +75 -13
  25. package/public/serve-shells/library-raws.html +75 -13
  26. package/public/serve-shells/tools-clipper.html +75 -13
  27. package/public/serve-shells/tools-image.html +75 -13
  28. package/public/serve-shells/tools-video.html +75 -13
@@ -2529,12 +2529,25 @@ button.rk-clips-thumb{appearance:none;-webkit-appearance:none;border:0;margin:0;
2529
2529
  // Desktop-agents nudge banner: a full-width alert pinned to the top of the
2530
2530
  // /editor and /chat surfaces pointing to the setup guide. Both surfaces embed
2531
2531
  // this chrome script, so gate strictly by pathname. Dismissible per session.
2532
+ // The nudge is aimed at the DIRECTOR (the paid operator who drives Vidfarm with
2533
+ // a desktop AI agent). A client reviewing a shared video in the editor should
2534
+ // NOT be told to go install Claude Code / Codex — so on the editor we suppress
2535
+ // it for free-tier (client / reviewer) sessions, read from the editor boot JSON.
2532
2536
  (function mountDesktopAgentsBanner() {
2533
2537
  try {
2534
2538
  var path = location.pathname;
2535
2539
  var onEditor = path === '/editor' || path.indexOf('/editor/') === 0;
2536
2540
  var onChat = path === '/chat' || path.indexOf('/chat/') === 0;
2537
2541
  if (!onEditor && !onChat) return;
2542
+ if (onEditor) {
2543
+ var bootEl = document.getElementById('hf-boot');
2544
+ if (bootEl) {
2545
+ try {
2546
+ var boot = JSON.parse(bootEl.textContent || '{}');
2547
+ if (boot && boot.freeTier) return;
2548
+ } catch (e) {}
2549
+ }
2550
+ }
2538
2551
  if (document.querySelector('.vf-topbanner')) return;
2539
2552
  if (sessionStorage.getItem('vf-topbanner-dismissed') === '1') return;
2540
2553
  var GUIDE = '/blog/desktop-ai-agents';
@@ -2740,6 +2753,23 @@ button.rk-clips-thumb{appearance:none;-webkit-appearance:none;border:0;margin:0;
2740
2753
  setLeftMode(''); // show the conversation, not the Files/History drawer
2741
2754
  openThread(id);
2742
2755
  }
2756
+ var hadHandoff = !!handoffThread;
2757
+ // Sticky active thread per template so a page refresh reopens the SAME
2758
+ // conversation instead of a blank chat (server routes deliberately never
2759
+ // attach ?thread= to editor URLs — the browser owns "which chat is active").
2760
+ function activeThreadKey() { return 'rk-chat-active:' + TEMPLATE_ID; }
2761
+ function saveActiveThread(id) {
2762
+ try { if (id) localStorage.setItem(activeThreadKey(), id); else localStorage.removeItem(activeThreadKey()); } catch (e) {}
2763
+ }
2764
+ var restoredActive = false;
2765
+ function restoreActiveThread() {
2766
+ if (restoredActive || hadHandoff) return;
2767
+ restoredActive = true;
2768
+ if (convo.length) return; // user already chatting — don't clobber
2769
+ var saved = null;
2770
+ try { saved = localStorage.getItem(activeThreadKey()); } catch (e) {}
2771
+ if (saved) openThread(saved);
2772
+ }
2743
2773
  var busy = false;
2744
2774
  var pendingAbort = null; // AbortController for the in-flight reply (Stop button)
2745
2775
 
@@ -2790,14 +2820,20 @@ button.rk-clips-thumb{appearance:none;-webkit-appearance:none;border:0;margin:0;
2790
2820
  // outgoing user turn — verbatim to what the SPA's own chat sends — so the agent
2791
2821
  // knows which fork to read (video_context) and mutate (editor_action). Only the
2792
2822
  // /editor dock has this bridge; elsewhere it returns ''.
2823
+ // Async: the Option-B bridge's getSnapshot() returns a PROMISE (it re-reads the
2824
+ // composition through the files API). The old sync call JSON.stringify'd the
2825
+ // Promise itself, sending the model a literal "{}" editor_context — no fork id,
2826
+ // no layers, no viral DNA. Always resolve before serializing.
2793
2827
  function editorContextBlock() {
2794
- if (!isEditorDock) return '';
2828
+ if (!isEditorDock) return Promise.resolve('');
2795
2829
  var bridge = (typeof window !== 'undefined') ? window.__vidfarmEditorAction : null;
2796
- if (!bridge || typeof bridge.getSnapshot !== 'function') return '';
2830
+ if (!bridge || typeof bridge.getSnapshot !== 'function') return Promise.resolve('');
2797
2831
  var snap; try { snap = bridge.getSnapshot(); } catch (e) { snap = null; }
2798
- if (!snap) return '';
2799
- try { return '\n\n<editor_context>\n' + JSON.stringify(snap, null, 2) + '\n</editor_context>'; }
2800
- catch (e) { return ''; }
2832
+ return Promise.resolve(snap).then(function (s) {
2833
+ if (!s) return '';
2834
+ try { return '\n\n<editor_context>\n' + JSON.stringify(s, null, 2) + '\n</editor_context>'; }
2835
+ catch (e) { return ''; }
2836
+ }, function () { return ''; });
2801
2837
  }
2802
2838
  function loadBoot() {
2803
2839
  if (BOOT_STATE === 'ready' || BOOT_STATE === 'loading') return;
@@ -2820,6 +2856,7 @@ button.rk-clips-thumb{appearance:none;-webkit-appearance:none;border:0;margin:0;
2820
2856
  loadThreads();
2821
2857
  if (leftMode() === 'cloud') loadTasks();
2822
2858
  consumeHandoff();
2859
+ restoreActiveThread();
2823
2860
  })
2824
2861
  .catch(function () { BOOT_STATE = 'error'; });
2825
2862
  }
@@ -3373,8 +3410,10 @@ button.rk-clips-thumb{appearance:none;-webkit-appearance:none;border:0;margin:0;
3373
3410
  setBusy(true);
3374
3411
  if (!threadId) threadId = genId('thread');
3375
3412
  // Attach a fresh <editor_context> to the current (last) user turn only, so the
3376
- // model sees the composition state without bloating persisted history.
3377
- var ctxBlock = editorContextBlock();
3413
+ // model sees the composition state without bloating persisted history. The
3414
+ // block resolves asynchronously (files-API read) — wait for it before building
3415
+ // the outgoing messages so the model actually receives the composition state.
3416
+ editorContextBlock().then(function (ctxBlock) {
3378
3417
  // Attachments (pasted files OR files picked from the directory explorer) must
3379
3418
  // ride in the model messages as file content parts + a URL text line — the
3380
3419
  // backend only feeds the model messages[].content, NOT user_message.attachments
@@ -3392,6 +3431,9 @@ button.rk-clips-thumb{appearance:none;-webkit-appearance:none;border:0;margin:0;
3392
3431
  }
3393
3432
  return { role: m.role, content: content };
3394
3433
  });
3434
+ // The send is what turns a freshly minted thread id into a real saved
3435
+ // thread — make it the sticky-restore target from this moment on.
3436
+ saveActiveThread(threadId);
3395
3437
  var body = {
3396
3438
  messages: outMessages,
3397
3439
  thread_id: threadId,
@@ -3455,6 +3497,7 @@ button.rk-clips-thumb{appearance:none;-webkit-appearance:none;border:0;margin:0;
3455
3497
  if (!API_KEY) msg = msg + '\n\nAdd an AI provider key in Settings to chat on your own keys.';
3456
3498
  view.fail(msg); setBusy(false); if (input) input.focus();
3457
3499
  });
3500
+ }); // end editorContextBlock().then
3458
3501
  }
3459
3502
 
3460
3503
  function resetConversation() {
@@ -3708,10 +3751,25 @@ button.rk-clips-thumb{appearance:none;-webkit-appearance:none;border:0;margin:0;
3708
3751
  // fall through to chat-attach for folders / non-placeable files.
3709
3752
  if (isEditorDock && it && it.viewUrl) {
3710
3753
  var bridge = (typeof window !== 'undefined') ? window.__vidfarmEditorAction : null;
3711
- if (bridge && typeof bridge.placeMediaAtPlayhead === 'function') {
3712
- var res = bridge.placeMediaAtPlayhead({ url: it.viewUrl, name: it.name, contentType: it.contentType });
3713
- if (res && res.ok) { editorPlaceToast('Added \u201c' + (it.name || 'media') + '\u201d to the timeline'); return; }
3714
- // Non-placeable (folder / doc / not-ready): fall through to chat-attach.
3754
+ // Placeability must be decided SYNCHRONOUSLY (the Option-B bridge returns a
3755
+ // Promise, so we can't branch on its result to decide chat-attach fallback —
3756
+ // checking ".ok" on the Promise made EVERY click fall through, placing the
3757
+ // media AND attaching it to chat, with no toast). Only image/video/audio go
3758
+ // on the timeline; folders/docs still fall through to chat-attach.
3759
+ var ct = String(it.contentType || '');
3760
+ if (bridge && typeof bridge.placeMediaAtPlayhead === 'function'
3761
+ && (ct.indexOf('image/') === 0 || ct.indexOf('video/') === 0 || ct.indexOf('audio/') === 0)) {
3762
+ var placedName = it.name || 'media';
3763
+ var settlePlace = function (r) {
3764
+ if (r && r.ok) { editorPlaceToast('Added \u201c' + placedName + '\u201d to the timeline'); }
3765
+ else { editorPlaceToast('Couldn\u2019t add \u201c' + placedName + '\u201d' + ((r && r.error) ? ': ' + r.error : ''), true); }
3766
+ };
3767
+ try {
3768
+ var res = bridge.placeMediaAtPlayhead({ url: it.viewUrl, name: it.name, contentType: it.contentType });
3769
+ if (res && typeof res.then === 'function') { res.then(settlePlace, function (e) { settlePlace({ ok: false, error: (e && e.message) ? e.message : String(e) }); }); }
3770
+ else { settlePlace(res); }
3771
+ } catch (e) { settlePlace({ ok: false, error: (e && e.message) ? e.message : String(e) }); }
3772
+ return;
3715
3773
  }
3716
3774
  }
3717
3775
  // Files-only drawer (opened from the /chat page) has no chat composer of its
@@ -3828,7 +3886,11 @@ button.rk-clips-thumb{appearance:none;-webkit-appearance:none;border:0;margin:0;
3828
3886
  return wrap;
3829
3887
  }
3830
3888
  function setActiveThread(id) {
3831
- threadId = id;
3889
+ // null = "fresh unsaved chat": KEEP the freshly minted threadId (sends must
3890
+ // always carry a real thread_id or the server silently skips persistence)
3891
+ // and clear the sticky restore key; a real id becomes both current + sticky.
3892
+ if (id) { threadId = id; saveActiveThread(id); }
3893
+ else { saveActiveThread(null); }
3832
3894
  if (!histBody) return;
3833
3895
  var rows = histBody.querySelectorAll('.rk-aichat-frow');
3834
3896
  for (var i = 0; i < rows.length; i++) rows[i].classList.toggle('is-active', rows[i].getAttribute('data-id') === id);
@@ -3921,7 +3983,7 @@ button.rk-clips-thumb{appearance:none;-webkit-appearance:none;border:0;margin:0;
3921
3983
  .then(function (r) {
3922
3984
  if (!r.ok && r.status !== 404) throw new Error('http ' + r.status);
3923
3985
  threads = threads.filter(function (t) { return t.id !== id; });
3924
- if (id === threadId) resetConversation();
3986
+ if (id === threadId) { resetConversation(); saveActiveThread(null); }
3925
3987
  renderHistory();
3926
3988
  })
3927
3989
  .catch(function () {});
@@ -2116,12 +2116,25 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
2116
2116
  // Desktop-agents nudge banner: a full-width alert pinned to the top of the
2117
2117
  // /editor and /chat surfaces pointing to the setup guide. Both surfaces embed
2118
2118
  // this chrome script, so gate strictly by pathname. Dismissible per session.
2119
+ // The nudge is aimed at the DIRECTOR (the paid operator who drives Vidfarm with
2120
+ // a desktop AI agent). A client reviewing a shared video in the editor should
2121
+ // NOT be told to go install Claude Code / Codex — so on the editor we suppress
2122
+ // it for free-tier (client / reviewer) sessions, read from the editor boot JSON.
2119
2123
  (function mountDesktopAgentsBanner() {
2120
2124
  try {
2121
2125
  var path = location.pathname;
2122
2126
  var onEditor = path === '/editor' || path.indexOf('/editor/') === 0;
2123
2127
  var onChat = path === '/chat' || path.indexOf('/chat/') === 0;
2124
2128
  if (!onEditor && !onChat) return;
2129
+ if (onEditor) {
2130
+ var bootEl = document.getElementById('hf-boot');
2131
+ if (bootEl) {
2132
+ try {
2133
+ var boot = JSON.parse(bootEl.textContent || '{}');
2134
+ if (boot && boot.freeTier) return;
2135
+ } catch (e) {}
2136
+ }
2137
+ }
2125
2138
  if (document.querySelector('.vf-topbanner')) return;
2126
2139
  if (sessionStorage.getItem('vf-topbanner-dismissed') === '1') return;
2127
2140
  var GUIDE = '/blog/desktop-ai-agents';
@@ -2327,6 +2340,23 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
2327
2340
  setLeftMode(''); // show the conversation, not the Files/History drawer
2328
2341
  openThread(id);
2329
2342
  }
2343
+ var hadHandoff = !!handoffThread;
2344
+ // Sticky active thread per template so a page refresh reopens the SAME
2345
+ // conversation instead of a blank chat (server routes deliberately never
2346
+ // attach ?thread= to editor URLs — the browser owns "which chat is active").
2347
+ function activeThreadKey() { return 'rk-chat-active:' + TEMPLATE_ID; }
2348
+ function saveActiveThread(id) {
2349
+ try { if (id) localStorage.setItem(activeThreadKey(), id); else localStorage.removeItem(activeThreadKey()); } catch (e) {}
2350
+ }
2351
+ var restoredActive = false;
2352
+ function restoreActiveThread() {
2353
+ if (restoredActive || hadHandoff) return;
2354
+ restoredActive = true;
2355
+ if (convo.length) return; // user already chatting — don't clobber
2356
+ var saved = null;
2357
+ try { saved = localStorage.getItem(activeThreadKey()); } catch (e) {}
2358
+ if (saved) openThread(saved);
2359
+ }
2330
2360
  var busy = false;
2331
2361
  var pendingAbort = null; // AbortController for the in-flight reply (Stop button)
2332
2362
 
@@ -2377,14 +2407,20 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
2377
2407
  // outgoing user turn — verbatim to what the SPA's own chat sends — so the agent
2378
2408
  // knows which fork to read (video_context) and mutate (editor_action). Only the
2379
2409
  // /editor dock has this bridge; elsewhere it returns ''.
2410
+ // Async: the Option-B bridge's getSnapshot() returns a PROMISE (it re-reads the
2411
+ // composition through the files API). The old sync call JSON.stringify'd the
2412
+ // Promise itself, sending the model a literal "{}" editor_context — no fork id,
2413
+ // no layers, no viral DNA. Always resolve before serializing.
2380
2414
  function editorContextBlock() {
2381
- if (!isEditorDock) return '';
2415
+ if (!isEditorDock) return Promise.resolve('');
2382
2416
  var bridge = (typeof window !== 'undefined') ? window.__vidfarmEditorAction : null;
2383
- if (!bridge || typeof bridge.getSnapshot !== 'function') return '';
2417
+ if (!bridge || typeof bridge.getSnapshot !== 'function') return Promise.resolve('');
2384
2418
  var snap; try { snap = bridge.getSnapshot(); } catch (e) { snap = null; }
2385
- if (!snap) return '';
2386
- try { return '\n\n<editor_context>\n' + JSON.stringify(snap, null, 2) + '\n</editor_context>'; }
2387
- catch (e) { return ''; }
2419
+ return Promise.resolve(snap).then(function (s) {
2420
+ if (!s) return '';
2421
+ try { return '\n\n<editor_context>\n' + JSON.stringify(s, null, 2) + '\n</editor_context>'; }
2422
+ catch (e) { return ''; }
2423
+ }, function () { return ''; });
2388
2424
  }
2389
2425
  function loadBoot() {
2390
2426
  if (BOOT_STATE === 'ready' || BOOT_STATE === 'loading') return;
@@ -2407,6 +2443,7 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
2407
2443
  loadThreads();
2408
2444
  if (leftMode() === 'cloud') loadTasks();
2409
2445
  consumeHandoff();
2446
+ restoreActiveThread();
2410
2447
  })
2411
2448
  .catch(function () { BOOT_STATE = 'error'; });
2412
2449
  }
@@ -2960,8 +2997,10 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
2960
2997
  setBusy(true);
2961
2998
  if (!threadId) threadId = genId('thread');
2962
2999
  // Attach a fresh <editor_context> to the current (last) user turn only, so the
2963
- // model sees the composition state without bloating persisted history.
2964
- var ctxBlock = editorContextBlock();
3000
+ // model sees the composition state without bloating persisted history. The
3001
+ // block resolves asynchronously (files-API read) — wait for it before building
3002
+ // the outgoing messages so the model actually receives the composition state.
3003
+ editorContextBlock().then(function (ctxBlock) {
2965
3004
  // Attachments (pasted files OR files picked from the directory explorer) must
2966
3005
  // ride in the model messages as file content parts + a URL text line — the
2967
3006
  // backend only feeds the model messages[].content, NOT user_message.attachments
@@ -2979,6 +3018,9 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
2979
3018
  }
2980
3019
  return { role: m.role, content: content };
2981
3020
  });
3021
+ // The send is what turns a freshly minted thread id into a real saved
3022
+ // thread — make it the sticky-restore target from this moment on.
3023
+ saveActiveThread(threadId);
2982
3024
  var body = {
2983
3025
  messages: outMessages,
2984
3026
  thread_id: threadId,
@@ -3042,6 +3084,7 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
3042
3084
  if (!API_KEY) msg = msg + '\n\nAdd an AI provider key in Settings to chat on your own keys.';
3043
3085
  view.fail(msg); setBusy(false); if (input) input.focus();
3044
3086
  });
3087
+ }); // end editorContextBlock().then
3045
3088
  }
3046
3089
 
3047
3090
  function resetConversation() {
@@ -3295,10 +3338,25 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
3295
3338
  // fall through to chat-attach for folders / non-placeable files.
3296
3339
  if (isEditorDock && it && it.viewUrl) {
3297
3340
  var bridge = (typeof window !== 'undefined') ? window.__vidfarmEditorAction : null;
3298
- if (bridge && typeof bridge.placeMediaAtPlayhead === 'function') {
3299
- var res = bridge.placeMediaAtPlayhead({ url: it.viewUrl, name: it.name, contentType: it.contentType });
3300
- if (res && res.ok) { editorPlaceToast('Added \u201c' + (it.name || 'media') + '\u201d to the timeline'); return; }
3301
- // Non-placeable (folder / doc / not-ready): fall through to chat-attach.
3341
+ // Placeability must be decided SYNCHRONOUSLY (the Option-B bridge returns a
3342
+ // Promise, so we can't branch on its result to decide chat-attach fallback —
3343
+ // checking ".ok" on the Promise made EVERY click fall through, placing the
3344
+ // media AND attaching it to chat, with no toast). Only image/video/audio go
3345
+ // on the timeline; folders/docs still fall through to chat-attach.
3346
+ var ct = String(it.contentType || '');
3347
+ if (bridge && typeof bridge.placeMediaAtPlayhead === 'function'
3348
+ && (ct.indexOf('image/') === 0 || ct.indexOf('video/') === 0 || ct.indexOf('audio/') === 0)) {
3349
+ var placedName = it.name || 'media';
3350
+ var settlePlace = function (r) {
3351
+ if (r && r.ok) { editorPlaceToast('Added \u201c' + placedName + '\u201d to the timeline'); }
3352
+ else { editorPlaceToast('Couldn\u2019t add \u201c' + placedName + '\u201d' + ((r && r.error) ? ': ' + r.error : ''), true); }
3353
+ };
3354
+ try {
3355
+ var res = bridge.placeMediaAtPlayhead({ url: it.viewUrl, name: it.name, contentType: it.contentType });
3356
+ if (res && typeof res.then === 'function') { res.then(settlePlace, function (e) { settlePlace({ ok: false, error: (e && e.message) ? e.message : String(e) }); }); }
3357
+ else { settlePlace(res); }
3358
+ } catch (e) { settlePlace({ ok: false, error: (e && e.message) ? e.message : String(e) }); }
3359
+ return;
3302
3360
  }
3303
3361
  }
3304
3362
  // Files-only drawer (opened from the /chat page) has no chat composer of its
@@ -3415,7 +3473,11 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
3415
3473
  return wrap;
3416
3474
  }
3417
3475
  function setActiveThread(id) {
3418
- threadId = id;
3476
+ // null = "fresh unsaved chat": KEEP the freshly minted threadId (sends must
3477
+ // always carry a real thread_id or the server silently skips persistence)
3478
+ // and clear the sticky restore key; a real id becomes both current + sticky.
3479
+ if (id) { threadId = id; saveActiveThread(id); }
3480
+ else { saveActiveThread(null); }
3419
3481
  if (!histBody) return;
3420
3482
  var rows = histBody.querySelectorAll('.rk-aichat-frow');
3421
3483
  for (var i = 0; i < rows.length; i++) rows[i].classList.toggle('is-active', rows[i].getAttribute('data-id') === id);
@@ -3508,7 +3570,7 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
3508
3570
  .then(function (r) {
3509
3571
  if (!r.ok && r.status !== 404) throw new Error('http ' + r.status);
3510
3572
  threads = threads.filter(function (t) { return t.id !== id; });
3511
- if (id === threadId) resetConversation();
3573
+ if (id === threadId) { resetConversation(); saveActiveThread(null); }
3512
3574
  renderHistory();
3513
3575
  })
3514
3576
  .catch(function () {});
@@ -3709,12 +3709,25 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
3709
3709
  // Desktop-agents nudge banner: a full-width alert pinned to the top of the
3710
3710
  // /editor and /chat surfaces pointing to the setup guide. Both surfaces embed
3711
3711
  // this chrome script, so gate strictly by pathname. Dismissible per session.
3712
+ // The nudge is aimed at the DIRECTOR (the paid operator who drives Vidfarm with
3713
+ // a desktop AI agent). A client reviewing a shared video in the editor should
3714
+ // NOT be told to go install Claude Code / Codex — so on the editor we suppress
3715
+ // it for free-tier (client / reviewer) sessions, read from the editor boot JSON.
3712
3716
  (function mountDesktopAgentsBanner() {
3713
3717
  try {
3714
3718
  var path = location.pathname;
3715
3719
  var onEditor = path === '/editor' || path.indexOf('/editor/') === 0;
3716
3720
  var onChat = path === '/chat' || path.indexOf('/chat/') === 0;
3717
3721
  if (!onEditor && !onChat) return;
3722
+ if (onEditor) {
3723
+ var bootEl = document.getElementById('hf-boot');
3724
+ if (bootEl) {
3725
+ try {
3726
+ var boot = JSON.parse(bootEl.textContent || '{}');
3727
+ if (boot && boot.freeTier) return;
3728
+ } catch (e) {}
3729
+ }
3730
+ }
3718
3731
  if (document.querySelector('.vf-topbanner')) return;
3719
3732
  if (sessionStorage.getItem('vf-topbanner-dismissed') === '1') return;
3720
3733
  var GUIDE = '/blog/desktop-ai-agents';
@@ -3920,6 +3933,23 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
3920
3933
  setLeftMode(''); // show the conversation, not the Files/History drawer
3921
3934
  openThread(id);
3922
3935
  }
3936
+ var hadHandoff = !!handoffThread;
3937
+ // Sticky active thread per template so a page refresh reopens the SAME
3938
+ // conversation instead of a blank chat (server routes deliberately never
3939
+ // attach ?thread= to editor URLs — the browser owns "which chat is active").
3940
+ function activeThreadKey() { return 'rk-chat-active:' + TEMPLATE_ID; }
3941
+ function saveActiveThread(id) {
3942
+ try { if (id) localStorage.setItem(activeThreadKey(), id); else localStorage.removeItem(activeThreadKey()); } catch (e) {}
3943
+ }
3944
+ var restoredActive = false;
3945
+ function restoreActiveThread() {
3946
+ if (restoredActive || hadHandoff) return;
3947
+ restoredActive = true;
3948
+ if (convo.length) return; // user already chatting — don't clobber
3949
+ var saved = null;
3950
+ try { saved = localStorage.getItem(activeThreadKey()); } catch (e) {}
3951
+ if (saved) openThread(saved);
3952
+ }
3923
3953
  var busy = false;
3924
3954
  var pendingAbort = null; // AbortController for the in-flight reply (Stop button)
3925
3955
 
@@ -3970,14 +4000,20 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
3970
4000
  // outgoing user turn — verbatim to what the SPA's own chat sends — so the agent
3971
4001
  // knows which fork to read (video_context) and mutate (editor_action). Only the
3972
4002
  // /editor dock has this bridge; elsewhere it returns ''.
4003
+ // Async: the Option-B bridge's getSnapshot() returns a PROMISE (it re-reads the
4004
+ // composition through the files API). The old sync call JSON.stringify'd the
4005
+ // Promise itself, sending the model a literal "{}" editor_context — no fork id,
4006
+ // no layers, no viral DNA. Always resolve before serializing.
3973
4007
  function editorContextBlock() {
3974
- if (!isEditorDock) return '';
4008
+ if (!isEditorDock) return Promise.resolve('');
3975
4009
  var bridge = (typeof window !== 'undefined') ? window.__vidfarmEditorAction : null;
3976
- if (!bridge || typeof bridge.getSnapshot !== 'function') return '';
4010
+ if (!bridge || typeof bridge.getSnapshot !== 'function') return Promise.resolve('');
3977
4011
  var snap; try { snap = bridge.getSnapshot(); } catch (e) { snap = null; }
3978
- if (!snap) return '';
3979
- try { return '\n\n<editor_context>\n' + JSON.stringify(snap, null, 2) + '\n</editor_context>'; }
3980
- catch (e) { return ''; }
4012
+ return Promise.resolve(snap).then(function (s) {
4013
+ if (!s) return '';
4014
+ try { return '\n\n<editor_context>\n' + JSON.stringify(s, null, 2) + '\n</editor_context>'; }
4015
+ catch (e) { return ''; }
4016
+ }, function () { return ''; });
3981
4017
  }
3982
4018
  function loadBoot() {
3983
4019
  if (BOOT_STATE === 'ready' || BOOT_STATE === 'loading') return;
@@ -4000,6 +4036,7 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
4000
4036
  loadThreads();
4001
4037
  if (leftMode() === 'cloud') loadTasks();
4002
4038
  consumeHandoff();
4039
+ restoreActiveThread();
4003
4040
  })
4004
4041
  .catch(function () { BOOT_STATE = 'error'; });
4005
4042
  }
@@ -4553,8 +4590,10 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
4553
4590
  setBusy(true);
4554
4591
  if (!threadId) threadId = genId('thread');
4555
4592
  // Attach a fresh <editor_context> to the current (last) user turn only, so the
4556
- // model sees the composition state without bloating persisted history.
4557
- var ctxBlock = editorContextBlock();
4593
+ // model sees the composition state without bloating persisted history. The
4594
+ // block resolves asynchronously (files-API read) — wait for it before building
4595
+ // the outgoing messages so the model actually receives the composition state.
4596
+ editorContextBlock().then(function (ctxBlock) {
4558
4597
  // Attachments (pasted files OR files picked from the directory explorer) must
4559
4598
  // ride in the model messages as file content parts + a URL text line — the
4560
4599
  // backend only feeds the model messages[].content, NOT user_message.attachments
@@ -4572,6 +4611,9 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
4572
4611
  }
4573
4612
  return { role: m.role, content: content };
4574
4613
  });
4614
+ // The send is what turns a freshly minted thread id into a real saved
4615
+ // thread — make it the sticky-restore target from this moment on.
4616
+ saveActiveThread(threadId);
4575
4617
  var body = {
4576
4618
  messages: outMessages,
4577
4619
  thread_id: threadId,
@@ -4635,6 +4677,7 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
4635
4677
  if (!API_KEY) msg = msg + '\n\nAdd an AI provider key in Settings to chat on your own keys.';
4636
4678
  view.fail(msg); setBusy(false); if (input) input.focus();
4637
4679
  });
4680
+ }); // end editorContextBlock().then
4638
4681
  }
4639
4682
 
4640
4683
  function resetConversation() {
@@ -4888,10 +4931,25 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
4888
4931
  // fall through to chat-attach for folders / non-placeable files.
4889
4932
  if (isEditorDock && it && it.viewUrl) {
4890
4933
  var bridge = (typeof window !== 'undefined') ? window.__vidfarmEditorAction : null;
4891
- if (bridge && typeof bridge.placeMediaAtPlayhead === 'function') {
4892
- var res = bridge.placeMediaAtPlayhead({ url: it.viewUrl, name: it.name, contentType: it.contentType });
4893
- if (res && res.ok) { editorPlaceToast('Added \u201c' + (it.name || 'media') + '\u201d to the timeline'); return; }
4894
- // Non-placeable (folder / doc / not-ready): fall through to chat-attach.
4934
+ // Placeability must be decided SYNCHRONOUSLY (the Option-B bridge returns a
4935
+ // Promise, so we can't branch on its result to decide chat-attach fallback —
4936
+ // checking ".ok" on the Promise made EVERY click fall through, placing the
4937
+ // media AND attaching it to chat, with no toast). Only image/video/audio go
4938
+ // on the timeline; folders/docs still fall through to chat-attach.
4939
+ var ct = String(it.contentType || '');
4940
+ if (bridge && typeof bridge.placeMediaAtPlayhead === 'function'
4941
+ && (ct.indexOf('image/') === 0 || ct.indexOf('video/') === 0 || ct.indexOf('audio/') === 0)) {
4942
+ var placedName = it.name || 'media';
4943
+ var settlePlace = function (r) {
4944
+ if (r && r.ok) { editorPlaceToast('Added \u201c' + placedName + '\u201d to the timeline'); }
4945
+ else { editorPlaceToast('Couldn\u2019t add \u201c' + placedName + '\u201d' + ((r && r.error) ? ': ' + r.error : ''), true); }
4946
+ };
4947
+ try {
4948
+ var res = bridge.placeMediaAtPlayhead({ url: it.viewUrl, name: it.name, contentType: it.contentType });
4949
+ if (res && typeof res.then === 'function') { res.then(settlePlace, function (e) { settlePlace({ ok: false, error: (e && e.message) ? e.message : String(e) }); }); }
4950
+ else { settlePlace(res); }
4951
+ } catch (e) { settlePlace({ ok: false, error: (e && e.message) ? e.message : String(e) }); }
4952
+ return;
4895
4953
  }
4896
4954
  }
4897
4955
  // Files-only drawer (opened from the /chat page) has no chat composer of its
@@ -5008,7 +5066,11 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
5008
5066
  return wrap;
5009
5067
  }
5010
5068
  function setActiveThread(id) {
5011
- threadId = id;
5069
+ // null = "fresh unsaved chat": KEEP the freshly minted threadId (sends must
5070
+ // always carry a real thread_id or the server silently skips persistence)
5071
+ // and clear the sticky restore key; a real id becomes both current + sticky.
5072
+ if (id) { threadId = id; saveActiveThread(id); }
5073
+ else { saveActiveThread(null); }
5012
5074
  if (!histBody) return;
5013
5075
  var rows = histBody.querySelectorAll('.rk-aichat-frow');
5014
5076
  for (var i = 0; i < rows.length; i++) rows[i].classList.toggle('is-active', rows[i].getAttribute('data-id') === id);
@@ -5101,7 +5163,7 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
5101
5163
  .then(function (r) {
5102
5164
  if (!r.ok && r.status !== 404) throw new Error('http ' + r.status);
5103
5165
  threads = threads.filter(function (t) { return t.id !== id; });
5104
- if (id === threadId) resetConversation();
5166
+ if (id === threadId) { resetConversation(); saveActiveThread(null); }
5105
5167
  renderHistory();
5106
5168
  })
5107
5169
  .catch(function () {});