kingkont 0.7.65 → 0.7.67

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/index.html CHANGED
@@ -457,6 +457,38 @@
457
457
  </div>
458
458
 
459
459
  <!-- ===== Полноэкранный просмотр (image/video) ===== -->
460
+ <!-- ===== Updates overlay (вместо отдельного BrowserWindow — на Sequoia
461
+ child-windows крашат V8 в ValueSerializer). ===== -->
462
+ <div class="modal hidden" id="updatesOverlay">
463
+ <div class="modal-card" style="min-width:480px; max-width:560px;">
464
+ <h3>Обновления KingKont</h3>
465
+ <div class="hint" style="margin-bottom:14px;">Источник: <code>registry.npmjs.org/kingkont</code></div>
466
+ <div style="display:flex; gap:24px; margin-bottom:14px;">
467
+ <div style="flex:1;">
468
+ <div style="color:#888; font-size:11px; text-transform:uppercase; letter-spacing:0.5px; margin-bottom:4px;">Установлено</div>
469
+ <div id="upCurrent" style="font-size:18px; font-family:ui-monospace,monospace;">—</div>
470
+ </div>
471
+ <div style="flex:1;">
472
+ <div style="color:#888; font-size:11px; text-transform:uppercase; letter-spacing:0.5px; margin-bottom:4px;">Доступно</div>
473
+ <div id="upLatest" style="font-size:18px; font-family:ui-monospace,monospace;">…</div>
474
+ </div>
475
+ </div>
476
+ <div id="upUpdateBlock" style="display:none;">
477
+ <div style="display:flex; gap:8px; align-items:center; margin-bottom:8px; flex-wrap:wrap;">
478
+ <button id="upInstallBtn" class="primary">⬇ Установить и перезапустить</button>
479
+ <button id="upRelaunchBtn" class="primary" style="display:none; background:#16a34a;">↻ Перезапустить сейчас</button>
480
+ </div>
481
+ <pre id="upInstallLog" style="display:none; max-height:200px; overflow-y:auto; margin-top:10px; font-family:ui-monospace,monospace; font-size:11px; line-height:1.4; background:#0e0e0e; border:1px solid #2a2a2a; border-radius:4px; padding:8px 10px; color:#aaa; white-space:pre-wrap; word-break:break-all;"></pre>
482
+ </div>
483
+ <div id="upToDate" style="display:none; color:#16a34a; font-size:12px;">✓ У вас последняя версия.</div>
484
+ <div id="upError" style="display:none; color:#ef4444; font-size:12px; margin-top:12px;"></div>
485
+ <div class="modal-actions">
486
+ <button id="upRecheck">Проверить ещё раз</button>
487
+ <button id="upClose" class="primary">Закрыть</button>
488
+ </div>
489
+ </div>
490
+ </div>
491
+
460
492
  <!-- ===== Дефолтные промпты сцены (multi, с тегами kind) ===== -->
461
493
  <div class="modal hidden" id="defaultPromptsModal">
462
494
  <div class="modal-card" style="min-width:600px; max-width:760px;">
package/main.js CHANGED
@@ -398,37 +398,20 @@ ipcMain.handle('settings-window:close', () => {
398
398
  });
399
399
  ipcMain.handle('settings-window:open', () => openSettingsWindow());
400
400
 
401
- // ===== Updates window + npm version check =====
402
- let updatesWin = null;
401
+ // ===== Updates: показываем как overlay внутри основного окна =====
402
+ // Раньше открывалось отдельное BrowserWindow. На macOS Sequoia 15.7 это
403
+ // крашило V8 в ValueSerializer при IPC (даже после bump'а до Electron 33).
404
+ // Решение — рендерить overlay внутри main window'а, child window не создаём.
403
405
  function openUpdatesWindow() {
404
- if (updatesWin && !updatesWin.isDestroyed()) {
405
- updatesWin.focus();
406
- return;
406
+ if (win && !win.isDestroyed()) {
407
+ win.show();
408
+ win.focus();
409
+ win.webContents.send('menu:open-updates');
407
410
  }
408
- // Не используем parent: win — на macOS Sequoia 15.7 + Electron 32 это
409
- // приводит к V8 CHECK-крашу `v8::BackingStore::MaxByteLength` при создании
410
- // child-window. Окно обновлений и так логически независимое.
411
- updatesWin = new BrowserWindow({
412
- width: 480,
413
- height: 380,
414
- title: 'Обновления',
415
- resizable: false,
416
- minimizable: false,
417
- maximizable: false,
418
- backgroundColor: '#1a1a1a',
419
- webPreferences: {
420
- contextIsolation: true,
421
- nodeIntegration: false,
422
- preload: path.join(__dirname, 'preload.js'),
423
- },
424
- });
425
- updatesWin.removeMenu();
426
- updatesWin.loadFile(path.join(__dirname, 'updates.html'));
427
- updatesWin.on('closed', () => { updatesWin = null; });
428
411
  }
429
- ipcMain.handle('updates-window:close', () => {
430
- if (updatesWin && !updatesWin.isDestroyed()) updatesWin.close();
431
- });
412
+ // Старый close-window IPC — оставлен no-op для backward-compat (если old
413
+ // updates.html ещё где-то остался). Renderer теперь сам закрывает overlay.
414
+ ipcMain.handle('updates-window:close', () => { /* no-op */ });
432
415
 
433
416
  // Простой semver-сравнитель: возвращает true если b > a (есть более свежая).
434
417
  // Поддерживает X.Y.Z и pre-release suffix через '-' (rc/beta пропускаем как not-newer).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kingkont",
3
- "version": "0.7.65",
3
+ "version": "0.7.67",
4
4
  "description": "KingKont · Chatium — нод-редактор сцен с AI-генерацией (картинки/видео/голос/SFX/музыка/текст)",
5
5
  "main": "main.js",
6
6
  "bin": {
package/preload.js CHANGED
@@ -18,7 +18,7 @@ contextBridge.exposeInMainWorld('appMenu', {
18
18
  on: (channel, cb) => {
19
19
  const allowed = ['open-film', 'new-episode', 'new-character', 'new-location',
20
20
  'undo', 'redo', 'toggle-timeline', 'close-project',
21
- 'close-window-or-project', 'open-settings'];
21
+ 'close-window-or-project', 'open-settings', 'open-updates'];
22
22
  if (!allowed.includes(channel)) return;
23
23
  ipcRenderer.on(`menu:${channel}`, (_e, payload) => cb(payload));
24
24
  },
package/renderer/board.js CHANGED
@@ -36,6 +36,7 @@ window.addEventListener('DOMContentLoaded', async () => {
36
36
  else window.appWindow?.minimize();
37
37
  });
38
38
  window.appMenu.on('open-settings', () => openSettings());
39
+ window.appMenu.on('open-updates', () => openUpdatesOverlay());
39
40
  }
40
41
  // Восстановить состояние панелей таймлайна/превью/реплик
41
42
  const tlOpen = localStorage.getItem('timelineOpen') === '1';
@@ -1315,6 +1316,91 @@ async function selectBoard(board) {
1315
1316
  startExternalWatcher();
1316
1317
  }
1317
1318
 
1319
+ // =============================================================================
1320
+ // Updates overlay — проверка/установка обновлений KingKont прямо в основном
1321
+ // окне (без создания child BrowserWindow, который на macOS Sequoia крашит
1322
+ // V8 в ValueSerializer при IPC).
1323
+ // =============================================================================
1324
+
1325
+ let _updatesUnsubLog = null;
1326
+
1327
+ async function openUpdatesOverlay() {
1328
+ const overlay = $('updatesOverlay');
1329
+ if (!overlay) return;
1330
+ overlay.classList.remove('hidden');
1331
+ // Сброс state.
1332
+ $('upError').style.display = 'none';
1333
+ $('upUpdateBlock').style.display = 'none';
1334
+ $('upToDate').style.display = 'none';
1335
+ $('upInstallLog').style.display = 'none';
1336
+ $('upRelaunchBtn').style.display = 'none';
1337
+ $('upInstallBtn').style.display = '';
1338
+ $('upInstallBtn').disabled = false;
1339
+ $('upInstallBtn').textContent = '⬇ Установить и перезапустить';
1340
+ $('upLatest').textContent = '…';
1341
+ await checkUpdates();
1342
+ }
1343
+
1344
+ async function checkUpdates() {
1345
+ $('upError').style.display = 'none';
1346
+ $('upUpdateBlock').style.display = 'none';
1347
+ $('upToDate').style.display = 'none';
1348
+ $('upLatest').textContent = '…';
1349
+ $('upRecheck').disabled = true;
1350
+ try {
1351
+ const r = await window.appUpdates.check();
1352
+ $('upCurrent').textContent = r.current;
1353
+ $('upLatest').textContent = r.latest;
1354
+ if (r.isNew) {
1355
+ $('upUpdateBlock').style.display = '';
1356
+ } else {
1357
+ $('upToDate').style.display = '';
1358
+ }
1359
+ $('upUpdateBlock').dataset.target = r.latest;
1360
+ } catch (e) {
1361
+ $('upLatest').textContent = '—';
1362
+ $('upError').style.display = '';
1363
+ $('upError').textContent = 'Не удалось проверить: ' + (e?.message || String(e));
1364
+ } finally {
1365
+ $('upRecheck').disabled = false;
1366
+ }
1367
+ }
1368
+
1369
+ document.getElementById('upClose')?.addEventListener('click', () => {
1370
+ $('updatesOverlay').classList.add('hidden');
1371
+ if (_updatesUnsubLog) { _updatesUnsubLog(); _updatesUnsubLog = null; }
1372
+ });
1373
+ document.getElementById('upRecheck')?.addEventListener('click', () => checkUpdates());
1374
+ document.getElementById('upInstallBtn')?.addEventListener('click', async () => {
1375
+ const btn = $('upInstallBtn');
1376
+ const log = $('upInstallLog');
1377
+ const target = $('upUpdateBlock').dataset.target || 'latest';
1378
+ btn.disabled = true;
1379
+ btn.textContent = 'Устанавливаю…';
1380
+ log.style.display = '';
1381
+ log.textContent = '';
1382
+ log.style.color = '#aaa';
1383
+ if (_updatesUnsubLog) _updatesUnsubLog();
1384
+ _updatesUnsubLog = window.appUpdates.onInstallOutput(({ stream, text }) => {
1385
+ log.textContent += text;
1386
+ log.scrollTop = log.scrollHeight;
1387
+ if (stream === 'stderr') log.style.color = '#f88';
1388
+ });
1389
+ try {
1390
+ await window.appUpdates.install(target);
1391
+ btn.style.display = 'none';
1392
+ $('upRelaunchBtn').style.display = '';
1393
+ } catch (e) {
1394
+ log.style.color = '#f88';
1395
+ log.textContent += `\n[error] ${e?.message || String(e)}`;
1396
+ btn.disabled = false;
1397
+ btn.textContent = '⬇ Попробовать снова';
1398
+ } finally {
1399
+ if (_updatesUnsubLog) { _updatesUnsubLog(); _updatesUnsubLog = null; }
1400
+ }
1401
+ });
1402
+ document.getElementById('upRelaunchBtn')?.addEventListener('click', () => window.appUpdates.relaunch());
1403
+
1318
1404
  // =============================================================================
1319
1405
  // External file watcher (FSAH polling).
1320
1406
  // FSAH не имеет нативного file-watch API — поллим scene.json's lastModified
package/renderer/media.js CHANGED
@@ -1119,40 +1119,81 @@ function updateMentionPopup() {
1119
1119
  let items = [];
1120
1120
 
1121
1121
  if (dotIdx >= 0) {
1122
- // Уже выбран character ИЛИ location показываем его картинки.
1122
+ // Уже выбран character / location / __scene (текущая доска) — drilldown.
1123
1123
  const ownerName = query.slice(0, dotIdx);
1124
1124
  const imgQuery = query.slice(dotIdx + 1);
1125
- const c = state.charactersInfo.find(x => x.name.toLowerCase() === ownerName);
1126
- if (c) {
1127
- for (const img of (c.imageNodes || [])) {
1128
- if (!imgQuery || img.name.toLowerCase().includes(imgQuery)) {
1129
- items.push({
1130
- key: `${c.name}.${img.name}`,
1131
- label: img.name,
1132
- type: 'image',
1133
- scope: 'char-image',
1134
- charName: c.name,
1135
- file: img.file,
1136
- boardHandle: c.handle,
1137
- });
1125
+ if (ownerName === '__scene') {
1126
+ // Drilldown в ноды ТЕКУЩЕЙ доски (тип фильтруем по genKind).
1127
+ const allowed = state.genKind === 'image' ? ['image', 'text']
1128
+ : state.genKind === 'video' ? ['image', 'video', 'audio', 'text']
1129
+ : ['text', 'image', 'video', 'audio'];
1130
+ // Дедуп: если у двух нод одинаковый label (basename), второй
1131
+ // получает суффикс -<id4>. Это match'ит логику _scanBoardForRefs
1132
+ // для chars/locs.
1133
+ const seen = new Set();
1134
+ for (const n of state.currentBoard.metadata.nodes) {
1135
+ if (!n.id) continue;
1136
+ if (!allowed.includes(n.type)) continue;
1137
+ let label = (n.name || '').trim();
1138
+ if (!label) {
1139
+ label = n.file
1140
+ ? (n.file.split('/').pop() || '').replace(/\.[^.]+$/, '')
1141
+ : n.id.slice(0, 8);
1138
1142
  }
1143
+ if (!label) label = n.id.slice(0, 8);
1144
+ if (seen.has(label)) label = `${label}-${n.id.slice(0, 4)}`;
1145
+ seen.add(label);
1146
+ if (imgQuery && !label.toLowerCase().includes(imgQuery)) continue;
1147
+ items.push({
1148
+ // key = label: красивая ссылка типа [@kitchen] для безымянной
1149
+ // картинки kitchen.jpg. resolveMentions для board-нод сначала
1150
+ // ищет по name; если не нашёл — gatherMediaRefs упустит,
1151
+ // но мы переименуем после: для board-drill final key = label.
1152
+ // Чтобы он попал в gatherMediaRefs — добавим временный n.name
1153
+ // ниже. На самом деле проще: всегда insert = [@<label>], и в
1154
+ // gatherMediaRefs сравнение идёт по nodeRefKey = name||id.
1155
+ // Если label != name && != id — НЕ попадёт. Поэтому используем
1156
+ // n.name || n.id (id как fallback) — это работает в
1157
+ // resolveMentions.
1158
+ key: n.name || n.id,
1159
+ label, type: n.type, scope: 'board',
1160
+ file: n.file || null,
1161
+ boardHandle: state.currentBoard.handle,
1162
+ });
1139
1163
  }
1140
1164
  } else {
1141
- const l = state.locationsInfo.find(x => x.name.toLowerCase() === ownerName);
1142
- if (l) {
1143
- for (const img of (l.imageNodes || [])) {
1165
+ const c = state.charactersInfo.find(x => x.name.toLowerCase() === ownerName);
1166
+ if (c) {
1167
+ for (const img of (c.imageNodes || [])) {
1144
1168
  if (!imgQuery || img.name.toLowerCase().includes(imgQuery)) {
1145
1169
  items.push({
1146
- key: `${l.name}.${img.name}`,
1170
+ key: `${c.name}.${img.name}`,
1147
1171
  label: img.name,
1148
1172
  type: 'image',
1149
- scope: 'loc-image',
1150
- locName: l.name,
1173
+ scope: 'char-image',
1174
+ charName: c.name,
1151
1175
  file: img.file,
1152
- boardHandle: l.handle,
1176
+ boardHandle: c.handle,
1153
1177
  });
1154
1178
  }
1155
1179
  }
1180
+ } else {
1181
+ const l = state.locationsInfo.find(x => x.name.toLowerCase() === ownerName);
1182
+ if (l) {
1183
+ for (const img of (l.imageNodes || [])) {
1184
+ if (!imgQuery || img.name.toLowerCase().includes(imgQuery)) {
1185
+ items.push({
1186
+ key: `${l.name}.${img.name}`,
1187
+ label: img.name,
1188
+ type: 'image',
1189
+ scope: 'loc-image',
1190
+ locName: l.name,
1191
+ file: img.file,
1192
+ boardHandle: l.handle,
1193
+ });
1194
+ }
1195
+ }
1196
+ }
1156
1197
  }
1157
1198
  }
1158
1199
  } else if (isCharOnly) {
@@ -1173,17 +1214,26 @@ function updateMentionPopup() {
1173
1214
  }
1174
1215
  items = items.filter(s => s.key.toLowerCase().includes(query)).slice(0, 16);
1175
1216
  } else {
1176
- // Верхний уровень: локальные именованные ноды + персонажи (одна запись на персонажа)
1177
- // Текстовые ноды разрешены везде resolveMentions инлайнит их .md в промпт.
1217
+ // Верхний уровень: вместо плоского списка нод текущей доски
1218
+ // одна запись «Эта сцена» (drilldown в её ноды). Это убирает
1219
+ // лишний шум когда в проекте много персонажей/локаций. Click →
1220
+ // selectMention вставит [@__scene. и реоткроет popup в drilldown.
1221
+ // Если у текущей доски НЕТ нод вообще — пункт не показываем.
1178
1222
  const allowed = state.genKind === 'image' ? ['image', 'text']
1179
1223
  : state.genKind === 'video' ? ['image', 'video', 'audio', 'text']
1180
1224
  : ['text', 'image', 'video', 'audio'];
1181
- for (const n of state.currentBoard.metadata.nodes) {
1182
- if (!n.name || !n.id) continue;
1183
- if (!allowed.includes(n.type)) continue;
1225
+ const boardHasNodes = (state.currentBoard?.metadata?.nodes || []).some(n =>
1226
+ n.id && allowed.includes(n.type)
1227
+ );
1228
+ if (boardHasNodes) {
1229
+ // Превью первой image-ноды для миниатюры (если есть).
1230
+ const firstImg = state.currentBoard.metadata.nodes.find(n => n.type === 'image' && n.file);
1184
1231
  items.push({
1185
- key: n.name, label: n.name, type: n.type, scope: 'board',
1186
- file: n.file || null,
1232
+ key: '__scene__top', // не для вставки drillFromSceneFolder заменит
1233
+ label: 'Эта сцена',
1234
+ type: 'image', // для type-чипа
1235
+ scope: 'scene-folder',
1236
+ file: firstImg?.file || null,
1187
1237
  boardHandle: state.currentBoard.handle,
1188
1238
  });
1189
1239
  }
@@ -1251,6 +1301,7 @@ function updateMentionPopup() {
1251
1301
  : s.scope === 'char-image' ? `персонаж·${s.charName}`
1252
1302
  : s.scope === 'loc' ? 'локация'
1253
1303
  : s.scope === 'loc-image' ? `локация·${s.locName}`
1304
+ : s.scope === 'scene-folder' ? 'сцена'
1254
1305
  : s.type;
1255
1306
  const nm = document.createElement('span');
1256
1307
  nm.textContent = s.label;
@@ -1301,11 +1352,17 @@ function selectMention(idx = mentionState.selected) {
1301
1352
  const after = ta.value.slice(cursor);
1302
1353
  // Если выбрали персонажа/локацию и у них есть картинки — авто-drill:
1303
1354
  // оставляем @name. для дальнейшего ввода имени картинки.
1355
+ // Если выбрали «Эта сцена» (scope='scene-folder') — то же самое со
1356
+ // спец-префиксом __scene. Финальный select board-ноды стирает префикс
1357
+ // и вставляет [@nodename] (см. ветку drillFromScene ниже).
1304
1358
  let insert;
1305
1359
  let reopen = false;
1306
1360
  if ((item.scope === 'char' || item.scope === 'loc') && item.hasImages) {
1307
1361
  insert = '[@' + item.key + '.';
1308
1362
  reopen = true;
1363
+ } else if (item.scope === 'scene-folder') {
1364
+ insert = '[@__scene.';
1365
+ reopen = true;
1309
1366
  } else {
1310
1367
  insert = '[@' + (item.key || item.name) + '] ';
1311
1368
  }