kingkont 0.10.7 → 0.10.9

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.10.7",
3
+ "version": "0.10.9",
4
4
  "description": "KingKont · Chatium — нод-редактор сцен с AI-генерацией (картинки/видео/голос/SFX/музыка/текст)",
5
5
  "main": "main.js",
6
6
  "bin": {
package/renderer/board.js CHANGED
@@ -913,6 +913,10 @@ async function openFilm(handle) {
913
913
  showEmpty();
914
914
  // Cloud-кнопки: показать «☁ Сохранить на сервер» когда проект открыт.
915
915
  if (window.cloudProjects?.setVisibility) window.cloudProjects.setVisibility();
916
+ // Загружаем chat-историю проекта (если есть .kingkont-chat.json).
917
+ if (window.kingChat?.loadFromCurrentProject) {
918
+ window.kingChat.loadFromCurrentProject().catch(() => {});
919
+ }
916
920
 
917
921
  const raw = localStorage.getItem(`lastBoard:${handle.name}`);
918
922
  if (raw) {
@@ -956,10 +960,10 @@ async function closeProject() {
956
960
  // Помечаем что юзер вышел явно — на следующем старте autoload пропускается.
957
961
  // Cmd+R после close = welcome, а не реоткрытие.
958
962
  try { localStorage.setItem('welcomeOnNextStart', '1'); } catch {}
959
- // Чат привязан к одному проекту — чистим историю при выходе чтобы
960
- // не путал контекст следующего открытого проекта. И прячем панель —
961
- // на welcome-экране чат не имеет смысла (нет state.currentBoard).
962
- if (window.kingChat?.clear) window.kingChat.clear();
963
+ // Чат привязан к одному проекту — сбрасываем in-memory (НЕ трогая
964
+ // .kingkont-chat.json на диске; при следующем открытии загрузится).
965
+ // Прячем панель — на welcome без проекта чат не имеет смысла.
966
+ if (window.kingChat?.resetInMemory) window.kingChat.resetInMemory();
963
967
  if (window.kingChat?.close) window.kingChat.close();
964
968
  stopExternalWatcher();
965
969
  // Сбрасываем UI таймлайна/превью — иначе при возврате через welcome
package/renderer/chat.js CHANGED
@@ -219,6 +219,10 @@
219
219
  lines.push('- Если нужен новый id для add_node — оставь поле пустым, вернётся в результате.');
220
220
  lines.push('- Для генерации используй generate_node ПОСЛЕ add_node с promptом.');
221
221
  lines.push('- Отвечай по-русски, кратко. Объясняй что делаешь, без лишней воды.');
222
+ lines.push('');
223
+ lines.push('ВАЖНО: НИКОГДА не пиши <tool_result>...</tool_result> сам — это формат который Я');
224
+ lines.push('пришлю тебе с результатами выполнения. В своих сообщениях ты только зовёшь tools');
225
+ lines.push('через <tool>...</tool> и пишешь обычный текст для пользователя.');
222
226
  return lines.join('\n');
223
227
  }
224
228
 
@@ -240,7 +244,58 @@
240
244
  }
241
245
  function stripToolCalls(text) {
242
246
  if (typeof text !== 'string') return '';
243
- return text.replace(/<tool>[\s\S]*?<\/tool>/g, '').trim();
247
+ // Убираем оба тэга:
248
+ // <tool>JSON</tool> — наш command-protocol (модель так зовёт tools)
249
+ // <tool_result>JSON</tool_result> — модель иногда его галюцинирует
250
+ // в outputе, копируя format из user-msg.
251
+ return text
252
+ .replace(/<tool>[\s\S]*?<\/tool>/g, '')
253
+ .replace(/<tool_result>[\s\S]*?<\/tool_result>/g, '')
254
+ .trim();
255
+ }
256
+
257
+ // ============== PERSISTENCE ==============
258
+ // Чат-история живёт в `<filmHandle>/.kingkont-chat.json`. Работает для
259
+ // папочных проектов и cloud (cloudFs-shim — те же FSAH-методы).
260
+ // Загружается в loadHistoryFromCurrentProject() — board.js зовёт после openFilm.
261
+ // Сохраняется debounced на каждое изменение history (см. persistDebounced).
262
+ const CHAT_FILE = '.kingkont-chat.json';
263
+ let _persistTimer = null;
264
+ async function persistNow() {
265
+ if (!state.filmHandle) return;
266
+ try {
267
+ const fh = await state.filmHandle.getFileHandle(CHAT_FILE, { create: true });
268
+ const w = await fh.createWritable();
269
+ // Не сохраняем role==='system' (буфер) и tool_result-турны (промежуточные,
270
+ // нужны только модели). При re-load восстановим только pure user/assistant.
271
+ const persistable = history.filter(m =>
272
+ m.role !== 'system' &&
273
+ !(m.role === 'user' && m.content?.startsWith('<tool_result>')),
274
+ );
275
+ await w.write(JSON.stringify({ history: persistable, savedAt: Date.now() }, null, 2));
276
+ await w.close();
277
+ } catch (e) {
278
+ console.warn('[chat] persist failed:', e?.message || e);
279
+ }
280
+ }
281
+ function persistDebounced() {
282
+ clearTimeout(_persistTimer);
283
+ _persistTimer = setTimeout(persistNow, 600);
284
+ }
285
+ async function loadHistoryFromCurrentProject() {
286
+ if (!state.filmHandle) { history = []; renderHistory(); return; }
287
+ try {
288
+ const fh = await state.filmHandle.getFileHandle(CHAT_FILE);
289
+ const txt = await (await fh.getFile()).text();
290
+ const data = JSON.parse(txt);
291
+ if (Array.isArray(data?.history)) {
292
+ history = data.history;
293
+ renderHistory();
294
+ return;
295
+ }
296
+ } catch {} // нет файла или JSON — норма для нового проекта
297
+ history = [];
298
+ renderHistory();
244
299
  }
245
300
 
246
301
  // ============== UI ==============
@@ -364,6 +419,7 @@
364
419
  busy = true;
365
420
  history.push({ role: 'user', content: userText });
366
421
  renderHistory();
422
+ persistDebounced();
367
423
  const status = appendStatus('Claude думает…');
368
424
  const system = buildSystemPrompt();
369
425
  try {
@@ -384,6 +440,7 @@
384
440
  // Финальный ответ — больше нечего исполнять.
385
441
  status.remove();
386
442
  renderHistory();
443
+ persistDebounced();
387
444
  break;
388
445
  }
389
446
 
@@ -406,6 +463,7 @@
406
463
  }
407
464
  }
408
465
  renderHistory();
466
+ persistDebounced(); // assistant-турн с tool-вызовами уже частично готов
409
467
 
410
468
  // Шлём результаты обратно как user-message (чтобы Claude увидел и продолжил).
411
469
  const resultsMsg = results.map(r => `<tool_result>${JSON.stringify(r)}</tool_result>`).join('\n');
@@ -515,6 +573,9 @@
515
573
  const panel = $('chatPanel');
516
574
  panel.classList.toggle('hidden');
517
575
  if (!panel.classList.contains('hidden')) {
576
+ // Отрисовываем сохранённую историю при показе (на случай если она
577
+ // была подгружена в фоне через loadFromCurrentProject до первого toggle).
578
+ renderHistory();
518
579
  setTimeout(() => $('chatInput')?.focus(), 50);
519
580
  }
520
581
  }
@@ -525,7 +586,13 @@
525
586
  open: () => { ensureUI(); $('chatPanel').classList.remove('hidden'); setTimeout(() => $('chatInput')?.focus(), 50); },
526
587
  close: () => $('chatPanel')?.classList.add('hidden'),
527
588
  send,
528
- clear: () => { history = []; renderHistory(); },
589
+ // User-clear: чистит И на диске тоже (юзер явно нажал ⌫).
590
+ clear: () => { history = []; renderHistory(); persistNow().catch(() => {}); },
591
+ // Reset-on-close: только in-memory + UI, на диске НЕ трогает (история
592
+ // должна остаться чтобы при следующем открытии того же проекта подгрузилась).
593
+ resetInMemory: () => { history = []; renderHistory(); },
594
+ // board.js зовёт после openFilm чтобы подгрузить chat-историю проекта.
595
+ loadFromCurrentProject: () => loadHistoryFromCurrentProject(),
529
596
  tools: TOOLS,
530
597
  };
531
598
 
@@ -666,8 +666,13 @@
666
666
  });
667
667
  if (typeof positionFloatingMenu === 'function') {
668
668
  positionFloatingMenu(menu, e.clientX, e.clientY);
669
+ } else {
670
+ menu.style.cssText = `position:fixed; left:${e.clientX}px; top:${e.clientY}px; z-index:9999;`;
669
671
  }
670
- setTimeout(() => document.addEventListener('mousedown', () => menu.classList.add('hidden'), { once: true }), 0);
672
+ // Используем глобальный closeNodeMenu он умеет НЕ закрываться на клик
673
+ // по активной кнопке внутри меню (даёт click-обработчику сработать).
674
+ // Без этого mousedown на кнопке закрывал меню до click → нажатие тонуло.
675
+ setTimeout(() => document.addEventListener('mousedown', closeNodeMenu, { once: true }), 0);
671
676
  });
672
677
  setCloudButtonsVisibility();
673
678
  // Переинициализируем видимость кнопок раз в 5 сек — для случая когда юзер