@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.
- package/.agents/skills/hyperframes-core/SKILL.md +2 -0
- package/.agents/skills/hyperframes-creative/SKILL.md +1 -0
- package/.agents/skills/hyperframes-creative/references/beat-direction.md +17 -0
- package/.agents/skills/vidfarm-director/SKILL.md +4 -0
- package/.agents/skills/vidfarm-director/recipes/local-edit-render-approve.md +1 -1
- package/.agents/skills/vidfarm-director/references/automation-and-local-dev.md +23 -6
- package/.agents/skills/vidfarm-director/references/core-workflows.md +1 -1
- package/.agents/skills/vidfarm-director/references/editor-workflows.md +15 -2
- package/.agents/skills/vidfarm-director/references/primitives.md +1 -1
- package/.agents/skills/vidfarm-director/references/rest-api.md +1 -1
- package/.agents/skills/vidfarm-media/SKILL.md +16 -6
- package/README.md +2 -2
- package/SKILL.director.md +45 -11
- package/demo/dist/app.js +247 -226
- package/dist/src/cli.js +198 -30
- package/dist/src/devcli/composition-edit.js +99 -23
- package/dist/src/devcli/cost-mode.js +13 -4
- package/dist/src/devcli/doctor.js +65 -9
- package/dist/src/devcli/local-frontend-server.js +342 -50
- package/dist/src/devcli/port-utils.js +43 -0
- package/dist/src/devcli/process-scan.js +173 -0
- package/package.json +3 -1
- package/public/serve-shells/editor.html +75 -13
- package/public/serve-shells/library-files.html +75 -13
- package/public/serve-shells/library-raws.html +75 -13
- package/public/serve-shells/tools-clipper.html +75 -13
- package/public/serve-shells/tools-image.html +75 -13
- 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
|
-
|
|
2799
|
-
|
|
2800
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3712
|
-
|
|
3713
|
-
|
|
3714
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3299
|
-
|
|
3300
|
-
|
|
3301
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3979
|
-
|
|
3980
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4892
|
-
|
|
4893
|
-
|
|
4894
|
-
|
|
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
|
-
|
|
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 () {});
|