kingkont 0.17.4 → 0.17.6

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.17.4",
3
+ "version": "0.17.6",
4
4
  "description": "KingKont · Chatium — нод-редактор сцен с AI-генерацией (картинки/видео/голос/SFX/музыка/текст)",
5
5
  "main": "main.js",
6
6
  "bin": {
package/renderer/board.js CHANGED
@@ -454,7 +454,22 @@ function fmtRelativeTime(ts) {
454
454
  return new Date(ts).toLocaleDateString();
455
455
  }
456
456
 
457
+ // Дедуп: на Cmd+R / WS-event burst несколько вызовов могли race'иться
458
+ // (clear grid → await getRecents → append → второй call в это же время →
459
+ // дубли карточек / наезжают). Если уже рендерим — отметим что нужен
460
+ // follow-up render и выйдем; запустим его после текущего.
461
+ let _wrInFlight = false;
462
+ let _wrPending = false;
457
463
  async function renderWelcomeRecents() {
464
+ if (_wrInFlight) { _wrPending = true; return; }
465
+ _wrInFlight = true;
466
+ try { await _renderWelcomeRecentsInner(); }
467
+ finally {
468
+ _wrInFlight = false;
469
+ if (_wrPending) { _wrPending = false; renderWelcomeRecents().catch(() => {}); }
470
+ }
471
+ }
472
+ async function _renderWelcomeRecentsInner() {
458
473
  const grid = $('welcomeRecentGrid');
459
474
  const wrap = $('welcomeRecent');
460
475
  const titleEl = $('welcomeRecentTitle');
@@ -2008,7 +2008,13 @@ async function pollJob(job, nodeId, bKey, boardHandle, kind) {
2008
2008
  const isCurrent = state.currentBoard?.key === bKey;
2009
2009
  if (typeof showToast === 'function') {
2010
2010
  const where = isCurrent ? '' : ` в ${bKey}`;
2011
- showToast(`✓ ${kind} «${nodeName || nodeId.slice(0,6)}» готов${where}`, isCurrent ? 'ok' : 'info');
2011
+ // target для click-навигации в notify-панели (откроется доска +
2012
+ // подсветится нода).
2013
+ showToast(
2014
+ `✓ ${kind} «${nodeName || nodeId.slice(0,6)}» готов${where}`,
2015
+ isCurrent ? 'ok' : 'info',
2016
+ { target: { projectKey: projectKeyAtCompletion, boardKey: bKey, nodeId } },
2017
+ );
2012
2018
  }
2013
2019
  // System notification — только когда генерация завершилась в фоне
2014
2020
  // (не на активной доске или когда окно скрыто). На активном UI
@@ -89,18 +89,36 @@
89
89
  const dt = new Date(e.ts);
90
90
  const time = `${String(dt.getHours()).padStart(2,'0')}:${String(dt.getMinutes()).padStart(2,'0')}:${String(dt.getSeconds()).padStart(2,'0')}`;
91
91
  const clickable = !!e.target;
92
- row.style.cssText = `padding:8px 10px; border-radius:4px; margin-bottom:4px; background:${c}22; border-left:3px solid ${c}; cursor:${clickable ? 'pointer' : 'default'}; transition:background 0.1s;`;
92
+ row.style.cssText = `position:relative; padding:8px 10px 8px 10px; border-radius:4px; margin-bottom:4px; background:${c}22; border-left:3px solid ${c}; cursor:${clickable ? 'pointer' : 'default'}; transition:background 0.1s;`;
93
93
  if (clickable) row.title = 'Перейти к объекту';
94
94
  const targetHint = clickable ? `<span style="color:#9ab; font-size:10px; margin-left:6px;">↗</span>` : '';
95
- row.innerHTML = `<div style="display:flex; justify-content:space-between; gap:8px; align-items:flex-start;">
95
+ row.innerHTML = `<div style="display:flex; justify-content:space-between; gap:8px; align-items:flex-start; padding-right:18px;">
96
96
  <div style="color:#e0e0e0; font-size:12px; line-height:1.4; word-break:break-word; flex:1;"></div>
97
97
  <span style="color:#777; font-size:10px; font-family:ui-monospace,monospace; flex-shrink:0;">${time}${targetHint}</span>
98
98
  </div>`;
99
99
  row.querySelector('div > div').textContent = e.text;
100
+ // Кнопка × для удаления конкретного event'a (не блокирует клик по row).
101
+ const delBtn = document.createElement('button');
102
+ delBtn.textContent = '×';
103
+ delBtn.title = 'Удалить';
104
+ delBtn.style.cssText = 'position:absolute; top:2px; right:4px; background:transparent; border:none; color:#888; cursor:pointer; font-size:14px; line-height:1; padding:2px 6px; opacity:0.5; transition:opacity 0.1s;';
105
+ delBtn.addEventListener('mouseenter', () => delBtn.style.opacity = '1');
106
+ delBtn.addEventListener('mouseleave', () => delBtn.style.opacity = '0.5');
107
+ delBtn.addEventListener('click', (ev) => {
108
+ ev.stopPropagation();
109
+ // Find by ts (events shifted by render, индекс i может измениться к click'у).
110
+ const idx = events.findIndex(x => x.ts === e.ts && x.text === e.text);
111
+ if (idx >= 0) events.splice(idx, 1);
112
+ render();
113
+ });
114
+ row.appendChild(delBtn);
100
115
  if (clickable) {
101
116
  row.addEventListener('mouseenter', () => row.style.background = `${c}44`);
102
117
  row.addEventListener('mouseleave', () => row.style.background = `${c}22`);
103
- row.addEventListener('click', () => navigateToTarget(e.target));
118
+ row.addEventListener('click', (ev) => {
119
+ if (ev.target === delBtn) return;
120
+ navigateToTarget(e.target);
121
+ });
104
122
  }
105
123
  list.appendChild(row);
106
124
  }
@@ -188,11 +206,12 @@
188
206
  // Init: ensure UI exists ASAP, hook into showToast и WS-events.
189
207
  document.addEventListener('DOMContentLoaded', () => {
190
208
  _ensureUI();
191
- // Wrap showToast чтобы дублировать в панель.
209
+ // Wrap showToast чтобы дублировать в панель + forward target для
210
+ // click-навигации (showToast(text, kind, {target: {...}})).
192
211
  if (typeof window.showToast === 'function') {
193
212
  const orig = window.showToast;
194
- window.showToast = function (text, kind) {
195
- try { addEvent({ kind: kind || 'info', text }); } catch {}
213
+ window.showToast = function (text, kind, opts) {
214
+ try { addEvent({ kind: kind || 'info', text, target: opts?.target || null }); } catch {}
196
215
  return orig.apply(this, arguments);
197
216
  };
198
217
  }
package/renderer/state.js CHANGED
@@ -201,12 +201,16 @@ window.systemNotify = systemNotify;
201
201
  // resume сработал, ...). Стек справа сверху. Auto-dismiss:
202
202
  // - ok/info — 10s (юзер должен заметить успех)
203
203
  // - error — 30s (важно, не пропустить)
204
- function showToast(text, kind) {
204
+ function showToast(text, kind, opts) {
205
+ // opts.target: {projectKey, boardKey, nodeId} — toast становится кликабельным.
205
206
  let host = document.getElementById('toastHost');
206
207
  if (!host) {
207
208
  host = document.createElement('div');
208
209
  host.id = 'toastHost';
209
- host.style.cssText = 'position:fixed; right:16px; top:64px; z-index:9999; display:flex; flex-direction:column; gap:8px; pointer-events:none; max-width:340px;';
210
+ // Bottom-left стек рядом с 🔔 кнопкой нотификаций. Новые toast'ы
211
+ // появляются СНИЗУ (flex-direction: column-reverse), так что они
212
+ // «всплывают» вверх от кнопки.
213
+ host.style.cssText = 'position:fixed; left:52px; bottom:12px; z-index:9999; display:flex; flex-direction:column-reverse; gap:8px; pointer-events:none; max-width:340px;';
210
214
  document.body.appendChild(host);
211
215
  }
212
216
  const t = document.createElement('div');
package/server.js CHANGED
@@ -356,9 +356,9 @@ async function handleChatClear(req, res) {
356
356
  async function handleJobsTrack(req, res) {
357
357
  try {
358
358
  const body = await readJson(req);
359
- const { action, projectKey, jobId, kind, name, type, taskId } = body || {};
359
+ const { action, projectKey, jobId, kind, name, type, taskId, boardKey } = body || {};
360
360
  if (!projectKey || !jobId) return send(res, 400, { error: 'projectKey + jobId обязательны' });
361
- if (action === 'start') jobsHub.start({ projectKey, jobId, kind, name, type, taskId });
361
+ if (action === 'start') jobsHub.start({ projectKey, jobId, kind, name, type, taskId, boardKey });
362
362
  else if (action === 'end') jobsHub.end({ projectKey, jobId });
363
363
  else return send(res, 400, { error: 'action: start|end' });
364
364
  send(res, 200, { ok: true });