@officexapp/vidfarm-devcli 0.21.12 → 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.
@@ -2340,6 +2340,23 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
2340
2340
  setLeftMode(''); // show the conversation, not the Files/History drawer
2341
2341
  openThread(id);
2342
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
+ }
2343
2360
  var busy = false;
2344
2361
  var pendingAbort = null; // AbortController for the in-flight reply (Stop button)
2345
2362
 
@@ -2390,14 +2407,20 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
2390
2407
  // outgoing user turn — verbatim to what the SPA's own chat sends — so the agent
2391
2408
  // knows which fork to read (video_context) and mutate (editor_action). Only the
2392
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.
2393
2414
  function editorContextBlock() {
2394
- if (!isEditorDock) return '';
2415
+ if (!isEditorDock) return Promise.resolve('');
2395
2416
  var bridge = (typeof window !== 'undefined') ? window.__vidfarmEditorAction : null;
2396
- if (!bridge || typeof bridge.getSnapshot !== 'function') return '';
2417
+ if (!bridge || typeof bridge.getSnapshot !== 'function') return Promise.resolve('');
2397
2418
  var snap; try { snap = bridge.getSnapshot(); } catch (e) { snap = null; }
2398
- if (!snap) return '';
2399
- try { return '\n\n<editor_context>\n' + JSON.stringify(snap, null, 2) + '\n</editor_context>'; }
2400
- 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 ''; });
2401
2424
  }
2402
2425
  function loadBoot() {
2403
2426
  if (BOOT_STATE === 'ready' || BOOT_STATE === 'loading') return;
@@ -2420,6 +2443,7 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
2420
2443
  loadThreads();
2421
2444
  if (leftMode() === 'cloud') loadTasks();
2422
2445
  consumeHandoff();
2446
+ restoreActiveThread();
2423
2447
  })
2424
2448
  .catch(function () { BOOT_STATE = 'error'; });
2425
2449
  }
@@ -2973,8 +2997,10 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
2973
2997
  setBusy(true);
2974
2998
  if (!threadId) threadId = genId('thread');
2975
2999
  // Attach a fresh <editor_context> to the current (last) user turn only, so the
2976
- // model sees the composition state without bloating persisted history.
2977
- 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) {
2978
3004
  // Attachments (pasted files OR files picked from the directory explorer) must
2979
3005
  // ride in the model messages as file content parts + a URL text line — the
2980
3006
  // backend only feeds the model messages[].content, NOT user_message.attachments
@@ -2992,6 +3018,9 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
2992
3018
  }
2993
3019
  return { role: m.role, content: content };
2994
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);
2995
3024
  var body = {
2996
3025
  messages: outMessages,
2997
3026
  thread_id: threadId,
@@ -3055,6 +3084,7 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
3055
3084
  if (!API_KEY) msg = msg + '\n\nAdd an AI provider key in Settings to chat on your own keys.';
3056
3085
  view.fail(msg); setBusy(false); if (input) input.focus();
3057
3086
  });
3087
+ }); // end editorContextBlock().then
3058
3088
  }
3059
3089
 
3060
3090
  function resetConversation() {
@@ -3308,10 +3338,25 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
3308
3338
  // fall through to chat-attach for folders / non-placeable files.
3309
3339
  if (isEditorDock && it && it.viewUrl) {
3310
3340
  var bridge = (typeof window !== 'undefined') ? window.__vidfarmEditorAction : null;
3311
- if (bridge && typeof bridge.placeMediaAtPlayhead === 'function') {
3312
- var res = bridge.placeMediaAtPlayhead({ url: it.viewUrl, name: it.name, contentType: it.contentType });
3313
- if (res && res.ok) { editorPlaceToast('Added \u201c' + (it.name || 'media') + '\u201d to the timeline'); return; }
3314
- // 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;
3315
3360
  }
3316
3361
  }
3317
3362
  // Files-only drawer (opened from the /chat page) has no chat composer of its
@@ -3428,7 +3473,11 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
3428
3473
  return wrap;
3429
3474
  }
3430
3475
  function setActiveThread(id) {
3431
- 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); }
3432
3481
  if (!histBody) return;
3433
3482
  var rows = histBody.querySelectorAll('.rk-aichat-frow');
3434
3483
  for (var i = 0; i < rows.length; i++) rows[i].classList.toggle('is-active', rows[i].getAttribute('data-id') === id);
@@ -3521,7 +3570,7 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
3521
3570
  .then(function (r) {
3522
3571
  if (!r.ok && r.status !== 404) throw new Error('http ' + r.status);
3523
3572
  threads = threads.filter(function (t) { return t.id !== id; });
3524
- if (id === threadId) resetConversation();
3573
+ if (id === threadId) { resetConversation(); saveActiveThread(null); }
3525
3574
  renderHistory();
3526
3575
  })
3527
3576
  .catch(function () {});
@@ -3933,6 +3933,23 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
3933
3933
  setLeftMode(''); // show the conversation, not the Files/History drawer
3934
3934
  openThread(id);
3935
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
+ }
3936
3953
  var busy = false;
3937
3954
  var pendingAbort = null; // AbortController for the in-flight reply (Stop button)
3938
3955
 
@@ -3983,14 +4000,20 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
3983
4000
  // outgoing user turn — verbatim to what the SPA's own chat sends — so the agent
3984
4001
  // knows which fork to read (video_context) and mutate (editor_action). Only the
3985
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.
3986
4007
  function editorContextBlock() {
3987
- if (!isEditorDock) return '';
4008
+ if (!isEditorDock) return Promise.resolve('');
3988
4009
  var bridge = (typeof window !== 'undefined') ? window.__vidfarmEditorAction : null;
3989
- if (!bridge || typeof bridge.getSnapshot !== 'function') return '';
4010
+ if (!bridge || typeof bridge.getSnapshot !== 'function') return Promise.resolve('');
3990
4011
  var snap; try { snap = bridge.getSnapshot(); } catch (e) { snap = null; }
3991
- if (!snap) return '';
3992
- try { return '\n\n<editor_context>\n' + JSON.stringify(snap, null, 2) + '\n</editor_context>'; }
3993
- 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 ''; });
3994
4017
  }
3995
4018
  function loadBoot() {
3996
4019
  if (BOOT_STATE === 'ready' || BOOT_STATE === 'loading') return;
@@ -4013,6 +4036,7 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
4013
4036
  loadThreads();
4014
4037
  if (leftMode() === 'cloud') loadTasks();
4015
4038
  consumeHandoff();
4039
+ restoreActiveThread();
4016
4040
  })
4017
4041
  .catch(function () { BOOT_STATE = 'error'; });
4018
4042
  }
@@ -4566,8 +4590,10 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
4566
4590
  setBusy(true);
4567
4591
  if (!threadId) threadId = genId('thread');
4568
4592
  // Attach a fresh <editor_context> to the current (last) user turn only, so the
4569
- // model sees the composition state without bloating persisted history.
4570
- 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) {
4571
4597
  // Attachments (pasted files OR files picked from the directory explorer) must
4572
4598
  // ride in the model messages as file content parts + a URL text line — the
4573
4599
  // backend only feeds the model messages[].content, NOT user_message.attachments
@@ -4585,6 +4611,9 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
4585
4611
  }
4586
4612
  return { role: m.role, content: content };
4587
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);
4588
4617
  var body = {
4589
4618
  messages: outMessages,
4590
4619
  thread_id: threadId,
@@ -4648,6 +4677,7 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
4648
4677
  if (!API_KEY) msg = msg + '\n\nAdd an AI provider key in Settings to chat on your own keys.';
4649
4678
  view.fail(msg); setBusy(false); if (input) input.focus();
4650
4679
  });
4680
+ }); // end editorContextBlock().then
4651
4681
  }
4652
4682
 
4653
4683
  function resetConversation() {
@@ -4901,10 +4931,25 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
4901
4931
  // fall through to chat-attach for folders / non-placeable files.
4902
4932
  if (isEditorDock && it && it.viewUrl) {
4903
4933
  var bridge = (typeof window !== 'undefined') ? window.__vidfarmEditorAction : null;
4904
- if (bridge && typeof bridge.placeMediaAtPlayhead === 'function') {
4905
- var res = bridge.placeMediaAtPlayhead({ url: it.viewUrl, name: it.name, contentType: it.contentType });
4906
- if (res && res.ok) { editorPlaceToast('Added \u201c' + (it.name || 'media') + '\u201d to the timeline'); return; }
4907
- // 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;
4908
4953
  }
4909
4954
  }
4910
4955
  // Files-only drawer (opened from the /chat page) has no chat composer of its
@@ -5021,7 +5066,11 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
5021
5066
  return wrap;
5022
5067
  }
5023
5068
  function setActiveThread(id) {
5024
- 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); }
5025
5074
  if (!histBody) return;
5026
5075
  var rows = histBody.querySelectorAll('.rk-aichat-frow');
5027
5076
  for (var i = 0; i < rows.length; i++) rows[i].classList.toggle('is-active', rows[i].getAttribute('data-id') === id);
@@ -5114,7 +5163,7 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
5114
5163
  .then(function (r) {
5115
5164
  if (!r.ok && r.status !== 404) throw new Error('http ' + r.status);
5116
5165
  threads = threads.filter(function (t) { return t.id !== id; });
5117
- if (id === threadId) resetConversation();
5166
+ if (id === threadId) { resetConversation(); saveActiveThread(null); }
5118
5167
  renderHistory();
5119
5168
  })
5120
5169
  .catch(function () {});
@@ -2753,6 +2753,23 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
2753
2753
  setLeftMode(''); // show the conversation, not the Files/History drawer
2754
2754
  openThread(id);
2755
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
+ }
2756
2773
  var busy = false;
2757
2774
  var pendingAbort = null; // AbortController for the in-flight reply (Stop button)
2758
2775
 
@@ -2803,14 +2820,20 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
2803
2820
  // outgoing user turn — verbatim to what the SPA's own chat sends — so the agent
2804
2821
  // knows which fork to read (video_context) and mutate (editor_action). Only the
2805
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.
2806
2827
  function editorContextBlock() {
2807
- if (!isEditorDock) return '';
2828
+ if (!isEditorDock) return Promise.resolve('');
2808
2829
  var bridge = (typeof window !== 'undefined') ? window.__vidfarmEditorAction : null;
2809
- if (!bridge || typeof bridge.getSnapshot !== 'function') return '';
2830
+ if (!bridge || typeof bridge.getSnapshot !== 'function') return Promise.resolve('');
2810
2831
  var snap; try { snap = bridge.getSnapshot(); } catch (e) { snap = null; }
2811
- if (!snap) return '';
2812
- try { return '\n\n<editor_context>\n' + JSON.stringify(snap, null, 2) + '\n</editor_context>'; }
2813
- 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 ''; });
2814
2837
  }
2815
2838
  function loadBoot() {
2816
2839
  if (BOOT_STATE === 'ready' || BOOT_STATE === 'loading') return;
@@ -2833,6 +2856,7 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
2833
2856
  loadThreads();
2834
2857
  if (leftMode() === 'cloud') loadTasks();
2835
2858
  consumeHandoff();
2859
+ restoreActiveThread();
2836
2860
  })
2837
2861
  .catch(function () { BOOT_STATE = 'error'; });
2838
2862
  }
@@ -3386,8 +3410,10 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
3386
3410
  setBusy(true);
3387
3411
  if (!threadId) threadId = genId('thread');
3388
3412
  // Attach a fresh <editor_context> to the current (last) user turn only, so the
3389
- // model sees the composition state without bloating persisted history.
3390
- 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) {
3391
3417
  // Attachments (pasted files OR files picked from the directory explorer) must
3392
3418
  // ride in the model messages as file content parts + a URL text line — the
3393
3419
  // backend only feeds the model messages[].content, NOT user_message.attachments
@@ -3405,6 +3431,9 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
3405
3431
  }
3406
3432
  return { role: m.role, content: content };
3407
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);
3408
3437
  var body = {
3409
3438
  messages: outMessages,
3410
3439
  thread_id: threadId,
@@ -3468,6 +3497,7 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
3468
3497
  if (!API_KEY) msg = msg + '\n\nAdd an AI provider key in Settings to chat on your own keys.';
3469
3498
  view.fail(msg); setBusy(false); if (input) input.focus();
3470
3499
  });
3500
+ }); // end editorContextBlock().then
3471
3501
  }
3472
3502
 
3473
3503
  function resetConversation() {
@@ -3721,10 +3751,25 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
3721
3751
  // fall through to chat-attach for folders / non-placeable files.
3722
3752
  if (isEditorDock && it && it.viewUrl) {
3723
3753
  var bridge = (typeof window !== 'undefined') ? window.__vidfarmEditorAction : null;
3724
- if (bridge && typeof bridge.placeMediaAtPlayhead === 'function') {
3725
- var res = bridge.placeMediaAtPlayhead({ url: it.viewUrl, name: it.name, contentType: it.contentType });
3726
- if (res && res.ok) { editorPlaceToast('Added \u201c' + (it.name || 'media') + '\u201d to the timeline'); return; }
3727
- // 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;
3728
3773
  }
3729
3774
  }
3730
3775
  // Files-only drawer (opened from the /chat page) has no chat composer of its
@@ -3841,7 +3886,11 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
3841
3886
  return wrap;
3842
3887
  }
3843
3888
  function setActiveThread(id) {
3844
- 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); }
3845
3894
  if (!histBody) return;
3846
3895
  var rows = histBody.querySelectorAll('.rk-aichat-frow');
3847
3896
  for (var i = 0; i < rows.length; i++) rows[i].classList.toggle('is-active', rows[i].getAttribute('data-id') === id);
@@ -3934,7 +3983,7 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
3934
3983
  .then(function (r) {
3935
3984
  if (!r.ok && r.status !== 404) throw new Error('http ' + r.status);
3936
3985
  threads = threads.filter(function (t) { return t.id !== id; });
3937
- if (id === threadId) resetConversation();
3986
+ if (id === threadId) { resetConversation(); saveActiveThread(null); }
3938
3987
  renderHistory();
3939
3988
  })
3940
3989
  .catch(function () {});