kingkont 0.14.3 → 0.14.5

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kingkont",
3
- "version": "0.14.3",
3
+ "version": "0.14.5",
4
4
  "description": "KingKont · Chatium — нод-редактор сцен с AI-генерацией (картинки/видео/голос/SFX/музыка/текст)",
5
5
  "main": "main.js",
6
6
  "bin": {
package/renderer/board.js CHANGED
@@ -1873,9 +1873,20 @@ async function selectBoard(board) {
1873
1873
  applyZoomStyles(state.zoom);
1874
1874
  $('zoomLabel').textContent = Math.round(state.zoom * 100) + '%';
1875
1875
  }
1876
- // ВАЖНО: для backward-compat старых view (где scrollLeft/Top сохранены
1877
- // БЕЗ padding'а — до этой фичи) добавляем padding к сохранённому
1878
- // значению. Если view свежий и >= padding'а, считаем что padding уже учтён.
1876
+ } else {
1877
+ state.zoom = 1;
1878
+ applyZoomStyles(1);
1879
+ $('zoomLabel').textContent = '100%';
1880
+ }
1881
+ // ВАЖНО: applyZoomStyles ресайзит frame через setTimeout 200ms. Если
1882
+ // ставить scrollLeft СРАЗУ — браузер клампит к старому frame.scrollWidth
1883
+ // (zoom<1 → frame маленький → scroll урезается). Поэтому сначала
1884
+ // форсим frame в полный размер прямо сейчас, потом ставим scroll.
1885
+ const z = state.zoom || 1;
1886
+ document.querySelector('.canvas-frame').style.width = (6000 * z + 2 * padX) + 'px';
1887
+ document.querySelector('.canvas-frame').style.height = (4000 * z + 2 * padY) + 'px';
1888
+ if (view) {
1889
+ // Backward-compat старых view (без padding) — добавляем.
1879
1890
  if (typeof view.scrollLeft === 'number') {
1880
1891
  canvasWrap.scrollLeft = view.scrollLeft >= padX ? view.scrollLeft : view.scrollLeft + padX;
1881
1892
  } else canvasWrap.scrollLeft = padX;
@@ -1883,21 +1894,14 @@ async function selectBoard(board) {
1883
1894
  canvasWrap.scrollTop = view.scrollTop >= padY ? view.scrollTop : view.scrollTop + padY;
1884
1895
  } else canvasWrap.scrollTop = padY;
1885
1896
  } else {
1886
- state.zoom = 1;
1887
- applyZoomStyles(1);
1888
- $('zoomLabel').textContent = '100%';
1889
1897
  canvasWrap.scrollLeft = padX;
1890
1898
  canvasWrap.scrollTop = padY;
1891
1899
  }
1892
1900
 
1893
- // Auto-scroll к bbox нод ТОЛЬКО если view не был сохранён ранее
1894
- // (т.е. это первый раз открываем доску, или юзер ни разу не скроллил).
1895
- // Раньше срабатывало всегда из-за гонки с applyZoomStyles (resize
1896
- // через setTimeout 200ms) scrollLeft при zoom<1 клампился, bbox казался
1897
- // невидимым, view перебивался → юзер терял сохранённую позицию.
1898
- if (!view || (typeof view.scrollLeft !== 'number' && typeof view.scrollTop !== 'number')) {
1899
- requestAnimationFrame(() => _autoScrollToNodesIfHidden(padX, padY));
1900
- }
1901
+ // Auto-scroll к bbox нод если ни одна не попадает в viewport.
1902
+ // Frame уже ресайзнут синхронно выше, scroll проставлен корректно
1903
+ // в next frame _autoScrollToNodesIfHidden имеет валидную картинку.
1904
+ requestAnimationFrame(() => _autoScrollToNodesIfHidden(padX, padY));
1901
1905
 
1902
1906
  // Возобновить незавершённые джобы текущей доски
1903
1907
  for (const n of state.currentBoard.metadata.nodes) {
package/renderer/chat.js CHANGED
@@ -209,6 +209,17 @@
209
209
  },
210
210
  },
211
211
 
212
+ take_snapshot: {
213
+ description: 'Сделать снимок проекта ДО серьёзного изменения (удаление, массовое редактирование промптов, очистка таймлайна, перезапуск генерации). Юзер потом может откатить через UI tool-блока в чате. Label — короткое описание что собираешься делать («перед удалением 3 нод», «перед очисткой timeline»). НЕ делай snapshot перед read-only действиями (read_scene, list_scenes) — только перед destructive.',
214
+ params: '{"label":"<короткое описание что будет сделано>"}',
215
+ async handler({ label }) {
216
+ const snap = await takeSnapshot(label || 'без метки');
217
+ if (!snap) throw new Error('не удалось сделать snapshot (нет filmHandle?)');
218
+ const idx = snapshots.indexOf(snap);
219
+ return { ok: true, snapshotIdx: idx, label: snap.label, ts: snap.ts };
220
+ },
221
+ },
222
+
212
223
  delete_node: {
213
224
  description: 'Удалить ноду. Файл (если есть) переедет в _deleted/.',
214
225
  params: '{"id":"<node-id>"}',
@@ -421,6 +432,13 @@
421
432
  lines.push('- Не выдумывай имена сцен — сначала list_scenes.');
422
433
  lines.push('- Для генерации сразу после add_node — ВОЗЬМИ id из result.id ПРЕДЫДУЩЕГО вызова и передай в generate_node.');
423
434
  lines.push('- Когда нужно создать несколько нод сразу — выдавай add_node + generate_node чередуя, или сначала все add_node, потом все generate_node (используя id из их result-ов).');
435
+ lines.push('');
436
+ lines.push('SNAPSHOT для отката (важно):');
437
+ lines.push('- Перед DESTRUCTIVE действиями (delete_node, update_node_prompt, generate_node на ноду которая уже сгенерирована, clear_timeline, remove_clip_from_timeline) — позови take_snapshot ПЕРВЫМ tool в этом турне.');
438
+ lines.push('- Label snapshot\'a — короткое описание того что сейчас делаешь («перед удалением 3 нод», «перед перегенерацией картинки Закат», «перед очисткой таймлайна»).');
439
+ lines.push('- НЕ зови take_snapshot для read-only действий (read_scene, list_scenes, list_timeline) и для безопасных операций добавления (add_node без перетирания, add_clip_to_timeline).');
440
+ lines.push('- Если в одном турне НЕСКОЛЬКО destructive-вызовов — одного take_snapshot в начале достаточно.');
441
+ lines.push('');
424
442
  lines.push('- Отвечай по-русски, кратко. Объясняй что делаешь, без лишней воды.');
425
443
  lines.push('');
426
444
  lines.push('ВАЖНО: НИКОГДА не пиши <tool_result>...</tool_result> сам — это формат который Я');
@@ -703,8 +721,13 @@
703
721
  }
704
722
 
705
723
  function renderSnapshotBar() {
724
+ // Snapshot-bar отключён — откаты теперь inline в chat tool-блоке.
725
+ // (Кнопка ↩ Откатить «label» под каждым take_snapshot tool'ом.)
706
726
  const bar = $('chatSnapshotBar');
707
727
  if (!bar) return;
728
+ bar.style.display = 'none';
729
+ return;
730
+ // -- legacy code below, не выполняется --
708
731
  bar.innerHTML = '';
709
732
  if (!snapshots.length) { bar.style.display = 'none'; return; }
710
733
  bar.style.display = '';
@@ -1089,10 +1112,8 @@
1089
1112
  if (!userText.trim()) return;
1090
1113
  const key = sessionKey();
1091
1114
  if (!key) { alert('Сначала открой проект'); return; }
1092
- // Snapshot ДО мутацииклиентский (для отката чат-действий).
1093
- try {
1094
- await takeSnapshot(userText.length > 60 ? userText.slice(0, 60) + '…' : userText);
1095
- } catch (e) { console.warn('snapshot failed:', e?.message); }
1115
+ // Snapshot НЕ берём автоматически теперь это явный tool take_snapshot,
1116
+ // который Claude вызывает перед destructive-действиями. См. system-prompt.
1096
1117
  // Контекст + system-prompt: формируем тут (на клиенте) и отдаём серверу.
1097
1118
  const ctxSnap = buildContextSnapshot();
1098
1119
  const system = buildSystemPrompt() + '\n\n' + buildContextBlock(ctxSnap);
@@ -1190,6 +1211,30 @@
1190
1211
  const pre = document.createElement('pre');
1191
1212
  pre.textContent = JSON.stringify(dumpAll, null, 2);
1192
1213
  t.appendChild(pre);
1214
+ // Если среди tools есть take_snapshot — добавляем кнопки отката.
1215
+ // Pattern: одна кнопка ↩ на каждый snapshot tool-call. snapshotIdx
1216
+ // приходит из result handler'а (см. take_snapshot tool).
1217
+ for (const tc of m.tools) {
1218
+ if (tc.name !== 'take_snapshot' || !tc._ok || tc._error) continue;
1219
+ const idx = tc.result?.snapshotIdx;
1220
+ const lbl = tc.result?.label || tc.args?.label || '';
1221
+ if (typeof idx !== 'number') continue;
1222
+ const rb = document.createElement('button');
1223
+ rb.className = 'chat-snapshot-rollback';
1224
+ rb.textContent = `↩ Откатить «${lbl}»`;
1225
+ rb.title = `Откат до snapshot'а #${idx}`;
1226
+ rb.addEventListener('click', () => {
1227
+ // snapshots могли уже измениться (новые добавились) — find by ts.
1228
+ const realIdx = snapshots.findIndex(s => s.ts === tc.result?.ts);
1229
+ if (realIdx < 0) {
1230
+ alert('Snapshot потерялся (возможно вы переключили проект).');
1231
+ return;
1232
+ }
1233
+ if (!confirm(`Откатить до «${lbl}»?`)) return;
1234
+ rollbackToSnapshot(realIdx);
1235
+ });
1236
+ div.appendChild(rb);
1237
+ }
1193
1238
  div.appendChild(t);
1194
1239
  }
1195
1240
  list.appendChild(div);
@@ -300,6 +300,17 @@
300
300
  border-radius: 4px; padding: 2px 8px; cursor: pointer; font-size: 12px;
301
301
  }
302
302
  .chat-header button:hover { background: #2a2a2a; color: #fff; }
303
+ /* Кнопка отката снепшота прямо в чат-сообщении (под take_snapshot tool). */
304
+ .chat-snapshot-rollback {
305
+ margin-top: 4px; align-self: flex-start;
306
+ background: #232a36; border: 1px solid #3a4a5a; color: #aac;
307
+ font-size: 11px; padding: 3px 8px; border-radius: 999px;
308
+ cursor: pointer; opacity: 0.85;
309
+ }
310
+ .chat-snapshot-rollback:hover {
311
+ background: #2a3548; border-color: #4a6a8a; color: #cde; opacity: 1;
312
+ }
313
+
303
314
  .chat-snapshot-bar {
304
315
  padding: 6px 10px; background: #1f1f1f; border-bottom: 1px solid #2a2a2a;
305
316
  display: flex; align-items: center; gap: 6px; flex-shrink: 0;