From 8efddea6991ed28f0c6ff1127e2f7dddf8967bc6 Mon Sep 17 00:00:00 2001 From: MerfiDEV Date: Fri, 18 Sep 2026 14:10:12 +0300 Subject: [PATCH 1/6] =?UTF-8?q?feat:=20=D0=BF=D0=BB=D0=B0=D0=B2=D0=BD?= =?UTF-8?q?=D0=B0=D1=8F=20=D0=B0=D0=BD=D0=B8=D0=BC=D0=B0=D1=86=D0=B8=D1=8F?= =?UTF-8?q?=20=D0=BE=D0=BA=D0=BD=D0=B0=20=D0=B8=D0=BD=D0=B8=D1=86=D0=B8?= =?UTF-8?q?=D0=B0=D0=BB=D0=B8=D0=B7=D0=B0=D1=86=D0=B8=D0=B8,=20=D1=81?= =?UTF-8?q?=D0=BA=D1=80=D1=8B=D1=82=D0=B8=D0=B5=20=D0=B4=D0=B0=D1=88=D0=B1?= =?UTF-8?q?=D0=BE=D1=80=D0=B4=D0=B0=20=D0=BF=D1=80=D0=B8=20=D0=B2=D0=B2?= =?UTF-8?q?=D0=BE=D0=B4=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/preload/dom/state.js | 3 + src/preload/dom/stats-dashboard.js | 44 +++- src/preload/overlay/events.js | 2 +- src/preload/overlay/template.js | 22 ++ src/preload/overlay/ui.js | 345 +++++++++++++++++------------ 5 files changed, 269 insertions(+), 147 deletions(-) diff --git a/src/preload/dom/state.js b/src/preload/dom/state.js index a2afee7..0724563 100644 --- a/src/preload/dom/state.js +++ b/src/preload/dom/state.js @@ -13,6 +13,9 @@ module.exports = { sendDelayMax: 4000, // 当前项目目录(null 表示未初始化) currentProjectDir: null, + // Пользователь вручную закрыл окно «Инициализировать проект» (крестиком). + // В рамках сессии окно больше не показывается. + firstTimeDialogDismissed: false, // Включена ли кастомизация (визуальный рендеринг tool-блоков и эффектов). // Парсинг и выполнение tool-вызовов работают независимо от этого флага. customizationEnabled: true, diff --git a/src/preload/dom/stats-dashboard.js b/src/preload/dom/stats-dashboard.js index 1e8e7f6..96e3761 100644 --- a/src/preload/dom/stats-dashboard.js +++ b/src/preload/dom/stats-dashboard.js @@ -62,6 +62,36 @@ function isSettingsOpen() { } } +/** + * Есть ли текст в поле ввода чата (на home-странице дашборд при этом прячем). + * Игнорирует поля, принадлежащие оверлею/дашборду Cookie Code. + */ +function isHomeInputFilled() { + try { + const nodes = document.querySelectorAll( + 'textarea, div[contenteditable="true"], [role="textbox"]', + ); + for (const el of nodes) { + if (el.closest("#cuckoo-overlay, #" + ROOT_ID)) continue; + const st = window.getComputedStyle(el); + if ( + st.display === "none" || + st.visibility === "hidden" || + st.opacity === "0" + ) + continue; + const text = + el.tagName === "TEXTAREA" || el.tagName === "INPUT" + ? el.value + : el.textContent; + if (text && text.trim()) return true; + } + return false; + } catch (_) { + return false; + } +} + /** * Является ли текущая страница домашней (по провайдеру). */ @@ -516,8 +546,8 @@ async function syncVisibility(force) { return; } const home = isHomePage(); - // Настройки открыты — прячем дашборд, чтобы не мешал. - if (!home || isSettingsOpen()) { + // Настройки открыты ИЛИ в поле ввода уже что-то набрано — прячем дашборд. + if (!home || isSettingsOpen() || isHomeInputFilled()) { rootEl.classList.add("cuckoo-hidden"); return; } @@ -630,6 +660,16 @@ function start() { window.addEventListener("popstate", () => syncVisibility(true)); window.addEventListener("hashchange", () => syncVisibility(true)); + // Мгновенная реакция на начало/очистку ввода текста на home-странице. + // Покрывает и textarea (input), и contenteditable (input), и очистку. + const onAnyInput = () => { + try { + syncVisibility(); + } catch (_) {} + }; + document.addEventListener("input", onAnyInput, true); + document.addEventListener("compositionend", onAnyInput, true); + // SPA-навигация через history.pushState/replaceState не вызывает // popstate/hashchange — оборачиваем, чтобы дашборд скрывался сразу // при входе в сессию (а не через 1.5 с по таймеру). diff --git a/src/preload/overlay/events.js b/src/preload/overlay/events.js index b06d264..9088754 100644 --- a/src/preload/overlay/events.js +++ b/src/preload/overlay/events.js @@ -541,7 +541,7 @@ function bindEvents() { // 首次使用提示浮窗:关闭按钮 const firstCloseBtn = document.getElementById("cuckoo-btn-first-close"); - firstCloseBtn?.addEventListener("click", hideFirstTimeDialog); + firstCloseBtn?.addEventListener("click", () => hideFirstTimeDialog(true)); clearBtn?.addEventListener("click", () => { commandHistory.length = 0; renderHistory(); diff --git a/src/preload/overlay/template.js b/src/preload/overlay/template.js index 775ca37..2e41294 100644 --- a/src/preload/overlay/template.js +++ b/src/preload/overlay/template.js @@ -598,6 +598,15 @@ const OVERLAY_CSS = [ " pointer-events: none;", "}", ".cuckoo-first-time-dialog.cuckoo-hidden { display: none !important; }", + ".cuckoo-first-time-dialog.cuckoo-enter {", + " animation: cuckoo-first-time-fade-in 0.35s cubic-bezier(0.16, 1, 0.3, 1) both;", + "}", + ".cuckoo-first-time-dialog.cuckoo-enter .cuckoo-first-time-box {", + " animation: cuckoo-first-time-box-in 0.4s cubic-bezier(0.16, 1, 0.3, 1) both;", + "}", + ".cuckoo-first-time-dialog.cuckoo-closing {", + " animation: cuckoo-first-time-fade-out 0.22s ease-in both;", + "}", ".cuckoo-first-time-box {", " position: relative; max-width: 420px; width: 90%;", " background: rgba(17, 19, 34, 0.98);", @@ -609,6 +618,19 @@ const OVERLAY_CSS = [ " text-align: center;", " pointer-events: auto;", "}", + ".cuckoo-first-time-dialog.cuckoo-closing .cuckoo-first-time-box {", + " animation: cuckoo-first-time-box-out 0.22s ease-in both;", + "}", + "@keyframes cuckoo-first-time-fade-in { from { opacity: 0; } to { opacity: 1; } }", + "@keyframes cuckoo-first-time-fade-out { from { opacity: 1; } to { opacity: 0; } }", + "@keyframes cuckoo-first-time-box-in {", + " from { opacity: 0; transform: translateY(-14px) scale(0.96); }", + " to { opacity: 1; transform: translateY(0) scale(1); }", + "}", + "@keyframes cuckoo-first-time-box-out {", + " from { opacity: 1; transform: translateY(0) scale(1); }", + " to { opacity: 0; transform: translateY(-10px) scale(0.97); }", + "}", ".cuckoo-first-time-close { position: absolute; top: 10px; right: 10px; }", ".cuckoo-first-time-text { margin-bottom: 18px; color: #dde1ff; }", ".cuckoo-first-time-box .cuckoo-actions { justify-content: center; }", diff --git a/src/preload/overlay/ui.js b/src/preload/overlay/ui.js index 7ba1456..ffcd914 100644 --- a/src/preload/overlay/ui.js +++ b/src/preload/overlay/ui.js @@ -2,21 +2,31 @@ * 覆盖层 UI 基础能力:注入、提示、历史记录、徽章、面板显隐与巡检 * 由原 preload.js 拆分而来,逻辑保持不变。 */ -const fs = require('fs'); -const path = require('path'); -const { buildOverlayHTML, OVERLAY_CSS } = require('./template'); -const { getProviderByUrl } = require('../../../src/providers'); -const state = require('../dom/state'); -const { t } = require('../i18n/i18n'); +const fs = require("fs"); +const path = require("path"); +const { buildOverlayHTML, OVERLAY_CSS } = require("./template"); +const { getProviderByUrl } = require("../../../src/providers"); +const state = require("../dom/state"); +const { t } = require("../i18n/i18n"); // Инлайн-SVG логотипа DeepSeek (вставляется в круглый бейдж оверлея). // Читается один раз при загрузке preload, чтобы не дёргать диск при каждом рендере. -let DEEPSEEK_LOGO_SVG = ''; +let DEEPSEEK_LOGO_SVG = ""; try { - const svgPath = path.join(__dirname, '..', '..', 'ui', 'logos', 'deepseek.svg'); - DEEPSEEK_LOGO_SVG = fs.readFileSync(svgPath, 'utf-8'); + const svgPath = path.join( + __dirname, + "..", + "..", + "ui", + "logos", + "deepseek.svg", + ); + DEEPSEEK_LOGO_SVG = fs.readFileSync(svgPath, "utf-8"); } catch (err) { - console.error('[Cookie Code] Не удалось прочитать логотип DeepSeek:', err.message); + console.error( + "[Cookie Code] Не удалось прочитать логотип DeepSeek:", + err.message, + ); } // ========== 注入样式 ========== @@ -24,32 +34,32 @@ try { * 注入覆盖层 CSS 样式到页面头部 */ function injectCSS() { - const style = document.createElement('style'); + const style = document.createElement("style"); style.textContent = OVERLAY_CSS; document.head.appendChild(style); } -const OVERLAY_POS_KEY = 'cuckoo-overlay-pos'; +const OVERLAY_POS_KEY = "cuckoo-overlay-pos"; /** * Восстановить сохранённую позицию оверлея из localStorage. */ function restoreOverlayPosition() { - const overlay = document.getElementById('cuckoo-overlay'); + const overlay = document.getElementById("cuckoo-overlay"); if (!overlay) return; try { const raw = localStorage.getItem(OVERLAY_POS_KEY); if (!raw) return; const pos = JSON.parse(raw); - if (typeof pos.left === 'number' && typeof pos.top === 'number') { + if (typeof pos.left === "number" && typeof pos.top === "number") { const maxLeft = Math.max(0, window.innerWidth - 60); const maxTop = Math.max(0, window.innerHeight - 40); const left = Math.max(0, Math.min(maxLeft, pos.left)); const top = Math.max(0, Math.min(maxTop, pos.top)); - overlay.style.left = left + 'px'; - overlay.style.top = top + 'px'; - overlay.style.right = 'auto'; - overlay.style.bottom = 'auto'; + overlay.style.left = left + "px"; + overlay.style.top = top + "px"; + overlay.style.right = "auto"; + overlay.style.bottom = "auto"; } } catch (_) {} } @@ -58,15 +68,19 @@ function restoreOverlayPosition() { * Сделать панель оверлея перетаскиваемой за шапку (аналогично todo-panel). */ function makeOverlayDraggable() { - const overlay = document.getElementById('cuckoo-overlay'); - const handle = document.getElementById('cuckoo-overlay-drag'); + const overlay = document.getElementById("cuckoo-overlay"); + const handle = document.getElementById("cuckoo-overlay-drag"); if (!overlay || !handle) return; let dragging = false; - let startX = 0, startY = 0, startLeft = 0, startTop = 0; + let startX = 0, + startY = 0, + startLeft = 0, + startTop = 0; const onDown = (e) => { - if (e.target.closest('button') || e.target.closest('.cuckoo-btn-icon')) return; + if (e.target.closest("button") || e.target.closest(".cuckoo-btn-icon")) + return; dragging = true; const rect = overlay.getBoundingClientRect(); @@ -75,8 +89,8 @@ function makeOverlayDraggable() { startX = e.clientX; startY = e.clientY; - document.addEventListener('mousemove', onMove); - document.addEventListener('mouseup', onUp); + document.addEventListener("mousemove", onMove); + document.addEventListener("mouseup", onUp); e.preventDefault(); }; @@ -86,24 +100,27 @@ function makeOverlayDraggable() { let top = startTop + (e.clientY - startY); left = Math.max(0, Math.min(window.innerWidth - 60, left)); top = Math.max(0, Math.min(window.innerHeight - 40, top)); - overlay.style.left = left + 'px'; - overlay.style.top = top + 'px'; - overlay.style.right = 'auto'; - overlay.style.bottom = 'auto'; + overlay.style.left = left + "px"; + overlay.style.top = top + "px"; + overlay.style.right = "auto"; + overlay.style.bottom = "auto"; }; const onUp = () => { if (!dragging) return; dragging = false; - document.removeEventListener('mousemove', onMove); - document.removeEventListener('mouseup', onUp); + document.removeEventListener("mousemove", onMove); + document.removeEventListener("mouseup", onUp); try { const rect = overlay.getBoundingClientRect(); - localStorage.setItem(OVERLAY_POS_KEY, JSON.stringify({ left: rect.left, top: rect.top })); + localStorage.setItem( + OVERLAY_POS_KEY, + JSON.stringify({ left: rect.left, top: rect.top }), + ); } catch (_) {} }; - handle.addEventListener('mousedown', onDown); + handle.addEventListener("mousedown", onDown); } // ========== 注入覆盖层 HTML ========== @@ -112,14 +129,14 @@ function makeOverlayDraggable() { * 创建 cuckoo-root 容器并填充 OVERLAY_HTML 内容 */ function injectOverlay() { - const container = document.createElement('div'); - container.id = 'cuckoo-root'; + const container = document.createElement("div"); + container.id = "cuckoo-root"; container.innerHTML = buildOverlayHTML(); document.body.appendChild(container); // Вставляем логотип DeepSeek в круглый бейдж (замена текстовой «C») if (DEEPSEEK_LOGO_SVG) { - const fabIcon = container.querySelector('.cuckoo-fab-icon'); + const fabIcon = container.querySelector(".cuckoo-fab-icon"); if (fabIcon) fabIcon.innerHTML = DEEPSEEK_LOGO_SVG; } @@ -150,7 +167,7 @@ function generateId() { */ function formatTime(ts) { const d = new Date(ts); - const pad = (n) => String(n).padStart(2, '0'); + const pad = (n) => String(n).padStart(2, "0"); return `${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`; } @@ -161,8 +178,8 @@ function formatTime(ts) { * @returns {string} 截断后的文本 */ function truncate(text, maxLen = 50) { - if (!text || text.length <= maxLen) return text || ''; - return text.substring(0, maxLen) + '...'; + if (!text || text.length <= maxLen) return text || ""; + return text.substring(0, maxLen) + "..."; } /** @@ -171,7 +188,7 @@ function truncate(text, maxLen = 50) { * @returns {string} 转义后的 HTML 字符串 */ function escapeHtml(text) { - const div = document.createElement('div'); + const div = document.createElement("div"); div.textContent = text; return div.innerHTML; } @@ -182,18 +199,18 @@ function escapeHtml(text) { * @param {number} duration - 显示时长(毫秒),默认 2200 */ function showToast(text, duration = 2200) { - let toast = document.getElementById('cuckoo-toast'); + let toast = document.getElementById("cuckoo-toast"); if (!toast) { - toast = document.createElement('div'); - toast.id = 'cuckoo-toast'; - toast.className = 'cuckoo-toast'; + toast = document.createElement("div"); + toast.id = "cuckoo-toast"; + toast.className = "cuckoo-toast"; document.body.appendChild(toast); } toast.textContent = text; - requestAnimationFrame(() => toast.classList.add('show')); + requestAnimationFrame(() => toast.classList.add("show")); clearTimeout(showToast._timer); showToast._timer = setTimeout(() => { - toast.classList.remove('show'); + toast.classList.remove("show"); }, duration); } @@ -208,17 +225,17 @@ function showToast(text, duration = 2200) { */ function showConfirmDialog(text, options) { const opts = options || {}; - const okText = opts.okText || '确定'; + const okText = opts.okText || "确定"; const showCancel = !!opts.showCancel; - const cancelText = opts.cancelText || '取消'; + const cancelText = opts.cancelText || "取消"; // 移除旧弹窗 - const old = document.getElementById('cuckoo-confirm-dialog'); + const old = document.getElementById("cuckoo-confirm-dialog"); if (old) old.remove(); return new Promise((resolve) => { - const dialog = document.createElement('div'); - dialog.id = 'cuckoo-confirm-dialog'; + const dialog = document.createElement("div"); + dialog.id = "cuckoo-confirm-dialog"; dialog.style.cssText = ` position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); @@ -242,7 +259,7 @@ function showConfirmDialog(text, options) { font-size: 13px; font-weight: 600; cursor: pointer; margin-right: 10px; transition: all 0.2s; ">${cancelText}` - : ''; + : ""; dialog.innerHTML = `
${text}
${cancelBtnHtml} @@ -257,15 +274,17 @@ function showConfirmDialog(text, options) { document.body.appendChild(dialog); const cleanup = () => dialog.remove(); - dialog.querySelector('#cuckoo-confirm-ok').addEventListener('click', () => { + dialog.querySelector("#cuckoo-confirm-ok").addEventListener("click", () => { cleanup(); resolve(true); }); if (showCancel) { - dialog.querySelector('#cuckoo-confirm-cancel').addEventListener('click', () => { - cleanup(); - resolve(false); - }); + dialog + .querySelector("#cuckoo-confirm-cancel") + .addEventListener("click", () => { + cleanup(); + resolve(false); + }); } }); } @@ -284,9 +303,9 @@ function showConfirmDialog(text, options) { */ function showBannerNotification(text, options) { const opts = options || {}; - const btnText = opts.btnText !== undefined ? opts.btnText : 'OK'; - const showBtn = btnText !== false && btnText !== null && btnText !== ''; - const duration = typeof opts.duration === 'number' ? opts.duration : 0; + const btnText = opts.btnText !== undefined ? opts.btnText : "OK"; + const showBtn = btnText !== false && btnText !== null && btnText !== ""; + const duration = typeof opts.duration === "number" ? opts.duration : 0; // Закрываем предыдущий кастомный баннер, если был открыт hideBannerNotification(); @@ -295,21 +314,21 @@ function showBannerNotification(text, options) { let resolved = false; let timer = null; - const overlayWrap = document.createElement('div'); - overlayWrap.id = 'cuckoo-custom-banner-dialog'; - overlayWrap.className = 'cuckoo-first-time-dialog'; + const overlayWrap = document.createElement("div"); + overlayWrap.id = "cuckoo-custom-banner-dialog"; + overlayWrap.className = "cuckoo-first-time-dialog"; - const box = document.createElement('div'); - box.className = 'cuckoo-first-time-box'; + const box = document.createElement("div"); + box.className = "cuckoo-first-time-box"; - const closeBtn = document.createElement('button'); - closeBtn.className = 'cuckoo-btn-icon cuckoo-first-time-close'; - closeBtn.title = 'Close'; - closeBtn.textContent = '×'; + const closeBtn = document.createElement("button"); + closeBtn.className = "cuckoo-btn-icon cuckoo-first-time-close"; + closeBtn.title = "Close"; + closeBtn.textContent = "×"; - const textEl = document.createElement('div'); - textEl.className = 'cuckoo-first-time-text'; - textEl.style.whiteSpace = 'pre-wrap'; + const textEl = document.createElement("div"); + textEl.className = "cuckoo-first-time-text"; + textEl.style.whiteSpace = "pre-wrap"; textEl.textContent = text; box.appendChild(closeBtn); @@ -317,10 +336,10 @@ function showBannerNotification(text, options) { let actionBtn = null; if (showBtn) { - const actionsEl = document.createElement('div'); - actionsEl.className = 'cuckoo-actions'; - actionBtn = document.createElement('button'); - actionBtn.className = 'cuckoo-btn cuckoo-btn-primary'; + const actionsEl = document.createElement("div"); + actionsEl.className = "cuckoo-actions"; + actionBtn = document.createElement("button"); + actionBtn.className = "cuckoo-btn cuckoo-btn-primary"; actionBtn.textContent = btnText; actionsEl.appendChild(actionBtn); box.appendChild(actionsEl); @@ -335,21 +354,25 @@ function showBannerNotification(text, options) { if (timer) clearTimeout(timer); overlayWrap.remove(); if (wasAction) { - if (typeof opts.onAction === 'function') { - try { opts.onAction(); } catch (_) {} + if (typeof opts.onAction === "function") { + try { + opts.onAction(); + } catch (_) {} } resolve(true); } else { - if (typeof opts.onClose === 'function') { - try { opts.onClose(); } catch (_) {} + if (typeof opts.onClose === "function") { + try { + opts.onClose(); + } catch (_) {} } resolve(false); } }; - closeBtn.addEventListener('click', () => cleanup(false)); + closeBtn.addEventListener("click", () => cleanup(false)); if (actionBtn) { - actionBtn.addEventListener('click', () => cleanup(true)); + actionBtn.addEventListener("click", () => cleanup(true)); } if (duration > 0) { @@ -362,7 +385,7 @@ function showBannerNotification(text, options) { * Скрыть текущее кастомное уведомление-баннер. */ function hideBannerNotification() { - const el = document.getElementById('cuckoo-custom-banner-dialog'); + const el = document.getElementById("cuckoo-custom-banner-dialog"); if (el) el.remove(); } @@ -371,9 +394,9 @@ function hideBannerNotification() { * @param {boolean} running - 是否执行中 */ function setTaskStatus(running) { - const status = document.getElementById('cuckoo-task-status'); + const status = document.getElementById("cuckoo-task-status"); if (status) { - status.classList.toggle('cuckoo-hidden', !running); + status.classList.toggle("cuckoo-hidden", !running); } } @@ -382,20 +405,27 @@ function setTaskStatus(running) { * Отправляет IPC kill-process → processManager.killAll() (taskkill /T /F на Windows). */ async function handleKillProcess() { - const btn = document.getElementById('cuckoo-btn-kill'); + const btn = document.getElementById("cuckoo-btn-kill"); if (btn) btn.disabled = true; try { const res = await window.electronAPI.killProcess(); if (res && res.success) { - showToast(res.count > 0 - ? t('overlay.task.killed', { count: res.count }) - : t('overlay.task.stopped'), 2500); + showToast( + res.count > 0 + ? t("overlay.task.killed", { count: res.count }) + : t("overlay.task.stopped"), + 2500, + ); setTaskStatus(false); } else { - showToast(t('overlay.task.killError') + (res && res.error ? ': ' + res.error : ''), 3000); + showToast( + t("overlay.task.killError") + + (res && res.error ? ": " + res.error : ""), + 3000, + ); } } catch (err) { - showToast(t('overlay.task.killError') + ': ' + (err.message || err), 3000); + showToast(t("overlay.task.killError") + ": " + (err.message || err), 3000); } finally { if (btn) btn.disabled = false; } @@ -405,16 +435,16 @@ async function handleKillProcess() { * 显示覆盖层(移除 hidden 类) */ function showOverlay() { - const el = document.getElementById('cuckoo-overlay'); - if (el) el.classList.remove('cuckoo-hidden'); + const el = document.getElementById("cuckoo-overlay"); + if (el) el.classList.remove("cuckoo-hidden"); } /** * 隐藏覆盖层(添加 hidden 类) */ function hideOverlay() { - const el = document.getElementById('cuckoo-overlay'); - if (el) el.classList.add('cuckoo-hidden'); + const el = document.getElementById("cuckoo-overlay"); + if (el) el.classList.add("cuckoo-hidden"); } /** @@ -423,26 +453,24 @@ function hideOverlay() { */ function displayCommand(cmdData) { currentCommand = cmdData; - const preview = document.getElementById('cuckoo-cmd-preview'); - const resultSection = document.getElementById('cuckoo-result-section'); + const preview = document.getElementById("cuckoo-cmd-preview"); + const resultSection = document.getElementById("cuckoo-result-section"); if (preview) preview.textContent = cmdData.command; - if (resultSection) resultSection.classList.add('cuckoo-hidden'); - showToast('发现可执行的命令'); + if (resultSection) resultSection.classList.add("cuckoo-hidden"); + showToast("发现可执行的命令"); } /** * 确认执行当前显示的命令 * 已移除:确认执行按钮及相关交互。保留空函数以防其他引用。 */ -async function handleExecute() { -} +async function handleExecute() {} /** * 忽略当前命令 * 已移除:忽略按钮及相关交互。保留空函数以防其他引用。 */ -function handleIgnore() { -} +function handleIgnore() {} /** * 添加一条历史记录 @@ -459,42 +487,52 @@ function addHistory(entry) { * 将 commandHistory 中的记录渲染到界面,并为每条记录绑定点击事件以查看详情 */ function renderHistory() { - const list = document.getElementById('cuckoo-history-list'); + const list = document.getElementById("cuckoo-history-list"); if (!list) return; if (commandHistory.length === 0) { - list.innerHTML = '
暂无记录
'; + list.innerHTML = + '
暂无记录
'; return; } const items = commandHistory.slice(0, 20); - list.innerHTML = items.map((item) => ` + list.innerHTML = items + .map( + (item) => `
${escapeHtml(truncate(item.command, 60))} - - ${item.canceled ? '⏹ 已忽略' : item.success ? '✅ 成功' : '❌ 失败'} + + ${item.canceled ? "⏹ 已忽略" : item.success ? "✅ 成功" : "❌ 失败"} ${formatTime(item.timestamp)}
- `).join(''); + `, + ) + .join(""); - list.querySelectorAll('.cuckoo-history-item').forEach((el) => { - el.addEventListener('click', () => { + list.querySelectorAll(".cuckoo-history-item").forEach((el) => { + el.addEventListener("click", () => { const id = el.dataset.id; const entry = commandHistory.find((h) => h.id === id); if (entry) { - const preview = document.getElementById('cuckoo-cmd-preview'); - const resultSection = document.getElementById('cuckoo-result-section'); - const resultStatus = document.getElementById('cuckoo-result-status'); - const resultOutput = document.getElementById('cuckoo-result-output'); + const preview = document.getElementById("cuckoo-cmd-preview"); + const resultSection = document.getElementById("cuckoo-result-section"); + const resultStatus = document.getElementById("cuckoo-result-status"); + const resultOutput = document.getElementById("cuckoo-result-output"); if (preview) preview.textContent = entry.command; if (entry.output && resultSection) { - resultSection.classList.remove('cuckoo-hidden'); + resultSection.classList.remove("cuckoo-hidden"); if (resultStatus) { - resultStatus.textContent = entry.canceled ? '⏹ 已忽略' : entry.success ? '✅ 执行成功' : '❌ 执行失败'; - resultStatus.className = `cuckoo-result-status ${entry.success ? 'success' : 'error'}`; + resultStatus.textContent = entry.canceled + ? "⏹ 已忽略" + : entry.success + ? "✅ 执行成功" + : "❌ 执行失败"; + resultStatus.className = `cuckoo-result-status ${entry.success ? "success" : "error"}`; } - if (resultOutput) resultOutput.textContent = entry.output || '(无输出)'; + if (resultOutput) + resultOutput.textContent = entry.output || "(无输出)"; } showOverlay(); } @@ -506,22 +544,22 @@ function renderHistory() { * 闪烁状态徽章提示 */ function flashBadge() { - const badge = document.getElementById('cuckoo-status-badge'); - const dot = document.getElementById('cuckoo-status-dot'); + const badge = document.getElementById("cuckoo-status-badge"); + const dot = document.getElementById("cuckoo-status-dot"); if (badge) { - badge.style.background = 'rgba(124,255,178,0.25)'; - badge.style.borderColor = 'rgba(124,255,178,0.6)'; + badge.style.background = "rgba(124,255,178,0.25)"; + badge.style.borderColor = "rgba(124,255,178,0.6)"; setTimeout(() => { - badge.style.background = 'rgba(139, 147, 255, 0.22)'; - badge.style.borderColor = 'rgba(139, 147, 255, 0.4)'; + badge.style.background = "rgba(139, 147, 255, 0.22)"; + badge.style.borderColor = "rgba(139, 147, 255, 0.4)"; }, 3000); } if (dot) { - dot.style.background = '#ffc107'; - dot.style.animation = 'none'; + dot.style.background = "#ffc107"; + dot.style.animation = "none"; setTimeout(() => { - dot.style.background = '#7cffb2'; - dot.style.animation = 'cuckoo-pulse 2s infinite'; + dot.style.background = "#7cffb2"; + dot.style.animation = "cuckoo-pulse 2s infinite"; }, 3000); } } @@ -534,14 +572,17 @@ function flashBadge() { function updateHomeMode() { const url = window.location.href; const provider = getProviderByUrl(url); - const isHome = provider && provider.homeUrlPattern ? provider.homeUrlPattern.test(url) : false; - const overlay = document.getElementById('cuckoo-overlay'); + const isHome = + provider && provider.homeUrlPattern + ? provider.homeUrlPattern.test(url) + : false; + const overlay = document.getElementById("cuckoo-overlay"); if (overlay) { if (isHome) { - overlay.classList.add('cuckoo-home-mode'); + overlay.classList.add("cuckoo-home-mode"); showFirstTimeDialog(); } else { - overlay.classList.remove('cuckoo-home-mode'); + overlay.classList.remove("cuckoo-home-mode"); hideFirstTimeDialog(); } } @@ -549,22 +590,38 @@ function updateHomeMode() { /** * 显示首次使用提示浮窗(居中) + * Не показывается, если проект уже выбран или окно закрыто вручную в этой сессии. */ function showFirstTimeDialog() { - if (state.currentProjectDir) { + if (state.currentProjectDir || state.firstTimeDialogDismissed) { hideFirstTimeDialog(); return; } - const dialog = document.getElementById('cuckoo-first-time-dialog'); - if (dialog) dialog.classList.remove('cuckoo-hidden'); + const dialog = document.getElementById("cuckoo-first-time-dialog"); + if (!dialog) return; + // Уже видим — ничего не делаем (иначе анимация будет перезапускаться). + if (!dialog.classList.contains("cuckoo-hidden")) return; + dialog.classList.remove("cuckoo-closing"); + dialog.classList.add("cuckoo-enter"); + dialog.classList.remove("cuckoo-hidden"); } /** - * 隐藏首次使用提示浮窗 + * 隐藏首次使用提示浮窗 с плавной анимацией исчезновения. + * @param {boolean} [permanent] - если true, окно больше не показывается в этой сессии */ -function hideFirstTimeDialog() { - const dialog = document.getElementById('cuckoo-first-time-dialog'); - if (dialog) dialog.classList.add('cuckoo-hidden'); +function hideFirstTimeDialog(permanent) { + const dialog = document.getElementById("cuckoo-first-time-dialog"); + if (!dialog) return; + if (permanent) state.firstTimeDialogDismissed = true; + if (dialog.classList.contains("cuckoo-hidden")) return; + if (dialog.classList.contains("cuckoo-closing")) return; + dialog.classList.add("cuckoo-closing"); + setTimeout(() => { + dialog.classList.add("cuckoo-hidden"); + dialog.classList.remove("cuckoo-closing"); + dialog.classList.remove("cuckoo-enter"); + }, 220); } /** @@ -572,12 +629,12 @@ function hideFirstTimeDialog() { * 用于兜底恢复因异常被隐藏的面板 */ function forceShowOverlay() { - const overlay = document.getElementById('cuckoo-overlay'); + const overlay = document.getElementById("cuckoo-overlay"); if (overlay) { - overlay.classList.remove('cuckoo-hidden'); - overlay.style.transform = 'translateX(0)'; - overlay.style.opacity = '1'; - overlay.style.pointerEvents = 'auto'; + overlay.classList.remove("cuckoo-hidden"); + overlay.style.transform = "translateX(0)"; + overlay.style.opacity = "1"; + overlay.style.pointerEvents = "auto"; } } From 3a96575c1d27875b8b5f683259a3324384f4a2e4 Mon Sep 17 00:00:00 2001 From: MerfiDEV Date: Fri, 18 Sep 2026 14:20:25 +0300 Subject: [PATCH 2/6] =?UTF-8?q?fix(stats):=20=D1=82=D0=BE=D1=87=D0=BD?= =?UTF-8?q?=D1=8B=D0=B9=20=D1=83=D1=87=D1=91=D1=82=20=D1=81=D0=BE=D0=BE?= =?UTF-8?q?=D0=B1=D1=89=D0=B5=D0=BD=D0=B8=D0=B9=20=D1=87=D0=B5=D1=80=D0=B5?= =?UTF-8?q?=D0=B7=20message-counter=20=D1=81=20=D0=B4=D0=B5=D0=B4=D1=83?= =?UTF-8?q?=D0=BF=D0=BB=D0=B8=D0=BA=D0=B0=D1=86=D0=B8=D0=B5=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/stats-store.js | 19 ++++ src/preload/dom/message-counter.js | 151 +++++++++++++++++++++++++++++ src/preload/dom/observer.js | 4 - src/preload/dom/stats-recorder.js | 11 ++- src/preload/index.js | 3 + 5 files changed, 183 insertions(+), 5 deletions(-) create mode 100644 src/preload/dom/message-counter.js diff --git a/src/main/stats-store.js b/src/main/stats-store.js index f61de34..1dc5445 100644 --- a/src/main/stats-store.js +++ b/src/main/stats-store.js @@ -48,8 +48,14 @@ const EMPTY = { models: {}, // общий счётчик сообщений и токенов (для быстрого доступа) totals: { sessions: 0, messages: 0, tokens: 0 }, + // sessionId -> [hash, ...] — какие сообщения уже учтены (дедупликация). + // Ограничиваем список, чтобы файл не рос бесконечно. + countedHashes: {}, }; +// Максимум хранимых хэшей на сессию. +const MAX_HASHES_PER_SESSION = 500; + function readStats() { try { const file = getStatsPath(); @@ -62,6 +68,7 @@ function readStats() { days: parsed.days || {}, models: parsed.models || {}, totals: { ...EMPTY.totals, ...(parsed.totals || {}) }, + countedHashes: parsed.countedHashes || {}, }; } catch (err) { console.error("[Cookie Code] 读取 статистики失败:", err.message); @@ -85,6 +92,7 @@ function writeStats(stats) { * @param {object} ev * @param {string} [ev.sessionId] * @param {'user'|'ai'} ev.role + * @param {string} [ev.hash] стабильный хэш сообщения (для дедупликации) * @param {number} [ev.tokens] дельта токенов (если известна) * @param {string} [ev.model] */ @@ -96,6 +104,17 @@ function recordMessage(ev) { if (!stats.createdAt) stats.createdAt = now; + // Дедупликация по хэшу: одно и то же сообщение (в т.ч. после перерендера + // диалога при SPA-навигации) учитывается ровно один раз за сессию. + const hash = ev && ev.hash ? String(ev.hash) : ""; + if (hash) { + const seen = stats.countedHashes[sid] || []; + if (seen.indexOf(hash) !== -1) return stats; + seen.push(hash); + if (seen.length > MAX_HASHES_PER_SESSION) seen.shift(); + stats.countedHashes[sid] = seen; + } + // Сессия if (!stats.sessions[sid]) { stats.sessions[sid] = { diff --git a/src/preload/dom/message-counter.js b/src/preload/dom/message-counter.js new file mode 100644 index 0000000..2cc7c3e --- /dev/null +++ b/src/preload/dom/message-counter.js @@ -0,0 +1,151 @@ +/** + * Подсчёт сообщений для статистики дашборда. + * + * Точка учёта — появление новых .ds-message в DOM: + * - пользовательские сообщения (не служебные, отправленные Cookie Code) → role: 'user'; + * - ответы AI → role: 'ai'. + * + * Идемпотентность: + * 1) WeakSet 'counted' — один DOM-узел учитывается один раз; + * 2) хэш (role + индекс + текст) уходит в main-процесс, где дедуплицируется — + * повторный рендер диалога (SPA-навигация туда-обратно) не раздувает счётчики. + * + * Служебные сообщения Cookie Code (результаты инструментов, сводки JS, контекст) + * не считаются пользовательскими. + */ +const { getProviderByUrl } = require("../../../src/providers"); +const statsRecorder = require("./stats-recorder"); + +const MSG_SELECTOR = ".ds-message"; +// Даём сообщению немного времени наполниться контентом после появления узла. +const COUNT_DELAY_MS = 400; + +const counted = new WeakSet(); +let observer = null; +let started = false; +let scanTimer = null; + +/** Служебные сообщения, отправленные Cookie Code (не «пользователь»). */ +function isServiceText(text) { + const head = (text || "").slice(0, 60); + return ( + head.indexOf("【工具执行结果】") !== -1 || + head.indexOf("【JS 执行结果汇总】") !== -1 || + head.indexOf("【КОНТЕКСТ ИЗ ПРЕДЫДУЩЕГО ЧАТА】") !== -1 || + head.indexOf("【Контекст") !== -1 + ); +} + +/** Текст сообщения без кнопок/тулбаров/заголовков код-блоков. */ +function messageText(node) { + if (!node) return ""; + try { + const clone = node.cloneNode(true); + clone + .querySelectorAll( + 'button, [class*="toolbar"], [class*="copy"], [class*="download"], [class*="code-block-header"], [class*="lang"], [class*="header"]', + ) + .forEach((el) => el.remove && el.remove()); + return (clone.textContent || "").trim(); + } catch (_) { + return ""; + } +} + +function hashStr(s) { + let h = 0; + for (let i = 0; i < s.length; i++) h = (h * 31 + s.charCodeAt(i)) | 0; + return h.toString(36) + ":" + s.length.toString(36); +} + +function countNode(node) { + if (!node || counted.has(node)) return; + try { + const provider = getProviderByUrl(window.location.href); + if (!provider) return; + const text = messageText(node); + if (!text) return; + const isUser = + typeof provider.isUserMessage === "function" + ? provider.isUserMessage(node) + : false; + counted.add(node); + if (isUser && isServiceText(text)) return; // служебное — не считаем + + // Индекс среди .ds-message — стабилен при перерендере, поэтому попадает + // в хэш: одинаковый текст двух разных сообщений не склеится в одно. + let idx = -1; + try { + idx = Array.prototype.indexOf.call( + document.querySelectorAll(MSG_SELECTOR), + node, + ); + } catch (_) {} + + // Хэш без текста: индекс в списке сообщений стабилен при перерендере, + // а текст растёт по мере стриминга — иначе одна и та же AI-реплика + // учитывалась бы несколько раз (короткая версия, полная версия и т.д.). + const role = isUser ? "user" : "ai"; + const hash = hashStr(role + "|" + idx); + statsRecorder.recordMessage(role, { hash }); + } catch (_) {} +} + +function scanAll() { + try { + document.querySelectorAll(MSG_SELECTOR).forEach(countNode); + } catch (_) {} +} + +function scheduleScan() { + if (scanTimer) return; + scanTimer = setTimeout(() => { + scanTimer = null; + scanAll(); + }, COUNT_DELAY_MS); +} + +// Периодический fallback: узлы .ds-message могут появляться пустыми и +// наполняться позже (стриминг ответа) — MutationObserver такое не ловит. +const FALLBACK_INTERVAL_MS = 3000; + +/** Запуск: первичный скан + наблюдение за DOM + fallback-опрос. */ +function start() { + if (started) return; + started = true; + + scanAll(); + + try { + observer = new MutationObserver((mutations) => { + for (const m of mutations) { + if (!m.addedNodes || m.addedNodes.length === 0) continue; + for (const n of m.addedNodes) { + if (!(n instanceof Element)) continue; + if ( + n.matches(MSG_SELECTOR) || + (n.querySelector && n.querySelector(MSG_SELECTOR)) + ) { + scheduleScan(); + break; + } + } + } + }); + observer.observe(document.body || document.documentElement, { + childList: true, + subtree: true, + }); + } catch (_) {} + + // Fallback-опрос: покрывает случай «узел добавлен пустым, наполнился позже». + try { + setInterval(scanAll, FALLBACK_INTERVAL_MS); + } catch (_) {} + + // Смена URL (SPA-навигация между чатами) — перепроверяем DOM. + window.addEventListener("popstate", scanAll); + window.addEventListener("hashchange", scanAll); +} + +module.exports = { start, scanAll }; diff --git a/src/preload/dom/observer.js b/src/preload/dom/observer.js index 6043771..18c283f 100644 --- a/src/preload/dom/observer.js +++ b/src/preload/dom/observer.js @@ -23,7 +23,6 @@ const { const toolRender = require("./tool-render"); const toolResultInline = require("./tool-result-inline"); const responseMeta = require("./response-meta"); -const statsRecorder = require("./stats-recorder"); const { sendToolResultToChat, @@ -414,9 +413,6 @@ async function processLatestAIResponseInner(retryCount = 0, force = false) { return; // 已处理过,跳过 } - // Учёт статистики: одно AI-сообщение на завершённый ответ. - safe("observer.statsRecordAi", () => statsRecorder.recordMessage("ai")); - // 跳过用户消息(其中包含系统提示词里的示例代码块,不应被执行) const providerForUser = getCurrentProvider(); if ( diff --git a/src/preload/dom/stats-recorder.js b/src/preload/dom/stats-recorder.js index eaead7a..e799f22 100644 --- a/src/preload/dom/stats-recorder.js +++ b/src/preload/dom/stats-recorder.js @@ -27,7 +27,15 @@ function enabled() { return state.statsEnabled !== false; } -/** Записать сообщение (user / ai). */ +/** + * Записать сообщение (user / ai). + * @param {'user'|'ai'} role + * @param {object} [extra] + * @param {string} [extra.hash] стабильный хэш сообщения — для дедупликации + * (перерендер диалога не раздувает счётчик) + * @param {string} [extra.model] + * @param {number} [extra.tokens] + */ function recordMessage(role, extra) { if (!enabled()) return; try { @@ -39,6 +47,7 @@ function recordMessage(role, extra) { window.electronAPI.statsRecordMessage({ sessionId: currentSessionId(), role, + hash: (extra && extra.hash) || "", model: (extra && extra.model) || "", tokens: (extra && extra.tokens) || 0, }); diff --git a/src/preload/index.js b/src/preload/index.js index ff816e2..7fafd5d 100644 --- a/src/preload/index.js +++ b/src/preload/index.js @@ -33,6 +33,7 @@ const contextPort = require("./dom/context-port"); const fonts = require("./dom/fonts"); const statsDashboard = require("./dom/stats-dashboard"); const statsRecorder = require("./dom/stats-recorder"); +const messageCounter = require("./dom/message-counter"); const whatsNew = require("./dom/whats-new"); const i18n = require("./i18n/i18n"); const state = require("./dom/state"); @@ -109,6 +110,8 @@ async function init() { // Сбор статистики использования (токены + сообщения). safe("init.statsRecorderStart", () => statsRecorder.start()); + // Учёт сообщений (user / ai) по появлению .ds-message в DOM. + safe("init.messageCounterStart", () => messageCounter.start()); // Дашборд статистики на домашней странице. safe("init.statsDashboardStart", () => statsDashboard.start()); From 3deffe2fff41634ad2669191d2ed4d8346defe93 Mon Sep 17 00:00:00 2001 From: MerfiDEV Date: Fri, 18 Sep 2026 14:27:21 +0300 Subject: [PATCH 3/6] =?UTF-8?q?style(settings):=20=D0=BA=D0=B0=D1=81=D1=82?= =?UTF-8?q?=D0=BE=D0=BC=D0=BD=D1=8B=D0=B9=20=D1=81=D0=BA=D1=80=D0=BE=D0=BB?= =?UTF-8?q?=D0=BB=D0=B1=D0=B0=D1=80=20=D0=B2=20=D1=81=D1=82=D0=B8=D0=BB?= =?UTF-8?q?=D0=B5=20=D1=82=D0=B5=D0=BC=D1=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/preload/dom/settings-tab.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/preload/dom/settings-tab.js b/src/preload/dom/settings-tab.js index fe97f2f..cae3088 100644 --- a/src/preload/dom/settings-tab.js +++ b/src/preload/dom/settings-tab.js @@ -175,6 +175,13 @@ function buildContentHTML() { " word-break: normal; overflow-wrap: break-word; white-space: normal; }" + " .ck-card, .ck-stack { min-width: 0; }" + " #cuckoo-settings-content { min-width: 0; width: 100%; align-items: stretch; }" + + // ===== Кастомный скроллбар в стиле настроек ===== + " #cuckoo-settings-content::-webkit-scrollbar { width: 8px; }" + + " #cuckoo-settings-content::-webkit-scrollbar-track { background: rgba(255,255,255,0.04); border-radius: 8px; margin: 8px 0; }" + + " #cuckoo-settings-content::-webkit-scrollbar-thumb { background: rgba(139,147,255,0.35); border-radius: 8px; " + + " border: 2px solid transparent; background-clip: padding-box; transition: background 0.16s; }" + + " #cuckoo-settings-content::-webkit-scrollbar-thumb:hover { background: rgba(139,147,255,0.6); background-clip: padding-box; }" + + " #cuckoo-settings-content { scrollbar-width: thin; scrollbar-color: rgba(139,147,255,0.4) rgba(255,255,255,0.04); }" + " #cuckoo-settings-content > div { min-width: 0; width: 100%; flex-shrink: 0; }" + " #cuckoo-settings-content * { word-break: normal; overflow-wrap: break-word; }" + " .ck-badge { display: inline-block; font-size: 10px; font-weight: 700; letter-spacing: 0.04em; " + From 2fe86c9eff7b0c45a242c723a0c5c788f9eaba64 Mon Sep 17 00:00:00 2001 From: MerfiDEV Date: Fri, 18 Sep 2026 17:01:05 +0300 Subject: [PATCH 4/6] docs: add bilingual Code of Conduct; fix license to MIT in CONTRIBUTING --- CODE_OF_CONDUCT.md | 225 +++++++++++++++++++++++++++++++++++++++++++++ CONTRIBUTING.md | 47 ++++++---- 2 files changed, 253 insertions(+), 19 deletions(-) create mode 100644 CODE_OF_CONDUCT.md diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..7d2cebf --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,225 @@ +# Кодекс поведения участника + +## Наше обязательство + +Мы, как участники, контрибьюторы и лидеры, обязуемся сделать участие в нашем +сообществе свободным от притеснений для всех — независимо от возраста, телосложения, +видимой или невидимой инвалидности, этнической принадлежности, половых признаков, +гендерной идентичности и самовыражения, уровня опыта, образования, социально-экономического +статуса, национальности, внешности, расы, религии, сексуальной идентичности и ориентации. + +Мы обязуемся действовать и взаимодействовать так, чтобы сообщество было открытым, +дружелюбным, разнообразным, инклюзивным и здоровым. + +## Наши стандарты + +Примеры поведения, которое создаёт позитивную атмосферу: + +- Проявление эмпатии и доброты к другим людям +- Уважение к разным мнениям, точкам зрения и опыту +- Конструктивная обратная связь и её достойное принятие +- Признание своих ошибок, извинения перед пострадавшими и извлечение уроков +- Ориентация на то, что лучше для сообщества в целом + +Примеры неприемлемого поведения: + +- Сексуализированные выражения или образы, нежелательное сексуальное внимание +- Троллинг, оскорбительные или уничижительные комментарии, личные или политические атаки +- Публичное или приватное преследование +- Публикация личной информации других лиц без их явного разрешения +- Иное поведение, которое разумно можно считать неуместным в профессиональной среде + +## Ответственность за соблюдение + +Лидеры сообщества отвечают за разъяснение и соблюдение наших стандартов и будут +принимать справедливые и соразмерные корректирующие меры в ответ на любое поведение, +которое они сочтут неуместным, угрожающим, оскорбительным или вредным. + +Лидеры сообщества имеют право и обязанность удалять, редактировать или отклонять +комментарии, коммиты, код, правки wiki, issues и другие вклады, не соответствующие +настоящему Кодексу поведения, и при необходимости сообщать причины своих решений. + +## Область применения + +Настоящий Кодекс поведения применяется во всех пространствах сообщества, а также +в тех случаях, когда человек официально представляет сообщество в публичных +пространствах. + +## Обеспечение соблюдения + +О случаях оскорбительного, преследующего или иного неприемлемого поведения можно +сообщить лидерам сообщества, ответственным за соблюдение Кодекса. +Все жалобы будут рассмотрены оперативно и справедливо. + +Лидеры сообщества обязаны уважать приватность и безопасность того, кто сообщает +о происшествии. + +## Руководство по применению + +Лидеры сообщества будут следовать этим правилам при определении последствий +для действий, нарушающих настоящий Кодекс: + +### 1. Исправление + +**Воздействие на сообщество:** неуместные выражения или иное поведение, +сочтённое непрофессиональным или нежелательным. + +**Последствие:** приватное письменное предупреждение от лидеров сообщества +с разъяснением характера нарушения и объяснением, почему поведение было +неуместным. Может быть запрошено публичное извинение. + +### 2. Предупреждение + +**Воздействие на сообщество:** нарушение в результате единичного инцидента +или серии действий. + +**Последствие:** предупреждение с последствиями при продолжении поведения. +Никакого взаимодействия с вовлечёнными лицами, включая нежелательный контакт +с теми, кто обеспечивает соблюдение Кодекса, в течение определённого времени. +Это включает избегание взаимодействия в пространствах сообщества, а также +во внешних каналах, таких как социальные сети. Нарушение этих условий может +привести к временному или постоянному бану. + +### 3. Временный бан + +**Воздействие на сообщество:** серьёзное нарушение стандартов сообщества, +включая устойчивое неуместное поведение. + +**Последствие:** временный запрет на любое взаимодействие или публичное +общение с сообществом на определённый срок. В этот период не допускается +публичное или приватное взаимодействие с вовлечёнными лицами, включая +нежелательный контакт с теми, кто обеспечивает соблюдение Кодекса. +Нарушение этих условий может привести к постоянному бану. + +### 4. Постоянный бан + +**Воздействие на сообщество:** демонстрация систематического нарушения +стандартов сообщества, включая устойчивое неуместное поведение, преследование +отдельного человека, агрессию или уничижение групп людей. + +**Последствие:** постоянный запрет на любое публичное взаимодействие +в рамках сообщества. + +## Атрибуция + +Настоящий Кодекс поведения адаптирован из +[Contributor Covenant](https://www.contributor-covenant.org), версия 2.1, +доступной по адресу +https://www.contributor-covenant.org/version/2/1/code_of_conduct.html. + +--- + +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, religion, or sexual identity and +orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment: + +- Demonstrating empathy and kindness toward other people +- Being respectful of differing opinions, viewpoints, and experiences +- Giving and gracefully accepting constructive feedback +- Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +- Focusing on what is best not just for us as individuals, but for the overall + community + +Examples of unacceptable behavior: + +- The use of sexualized language or imagery, and sexual attention or advances + of any kind +- Trolling, insulting or derogatory comments, and personal or political attacks +- Public or private harassment +- Publishing others' private information, such as a physical or email address, + without their explicit permission +- Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement. +All complaints will be reviewed and investigated promptly and fairly. + +Community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact:** Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence:** A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact:** A violation through a single incident or series of actions. + +**Consequence:** A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or permanent +ban. + +### 3. Temporary Ban + +**Community Impact:** A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence:** A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact:** Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence:** A permanent ban from any sort of public interaction within the +community. + +## Attribution + +This Code of Conduct is adapted from the +[Contributor Covenant](https://www.contributor-covenant.org), version 2.1, +available at +https://www.contributor-covenant.org/version/2/1/code_of_conduct.html. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index db74ba5..f282c02 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -25,10 +25,10 @@ - Добавляйте комментарии, особенно к сложной логике - Пишите просто, следуйте принципу «наименьшего удивления» - Перед началом работы создавайте отдельную ветку для feature или исправления. - Используйте формат `feature/` или `fix/`, например: - `feature/super-puper-update`. + Используйте формат `feature/` или `fix/`, например: + `feature/super-puper-update`. - Следуйте существующей архитектуре, структуре файлов, API и стилю проекта. - Не добавляйте новый подход, если в проекте уже есть подходящий шаблон. + Не добавляйте новый подход, если в проекте уже есть подходящий шаблон. ## Pull Request @@ -61,9 +61,13 @@ 4. Зарегистрируйте его в `ToolRegistry` (см. `src/main/tool-registry.js`). 5. Обновите `tools/rules.md` и `README.md`. +## Кодекс поведения + +Участвуя в проекте, вы соглашаетесь соблюдать [Кодекс поведения](CODE_OF_CONDUCT.md). + ## Лицензия -Проект распространяется под лицензией GPL-3.0. Все вклады подпадают под неё же. +Проект распространяется под лицензией MIT. Все вклады подпадают под неё же. --- @@ -80,12 +84,12 @@ Thank you for helping improve the project! We welcome all contributions: 1. Fork the repository and clone it locally. 2. Install dependencies: `npm install` - - If npm reports that the Electron postinstall script was blocked by - `allowScripts`, run: - - `npm install-scripts approve electron` - - then run `npm install` again. - - Otherwise, the Electron binary will not be downloaded and the app will - fail to start. + - If npm reports that the Electron postinstall script was blocked by + `allowScripts`, run: + - `npm install-scripts approve electron` + - then run `npm install` again. + - Otherwise, the Electron binary will not be downloaded and the app will + fail to start. 3. Start the application: `npm start` ## Code Style @@ -96,20 +100,20 @@ Thank you for helping improve the project! We welcome all contributions: - Add comments for complex logic where they improve maintainability. - Keep the implementation simple and follow the principle of least surprise. - Create a separate branch for every feature or fix before making changes. - Use `feature/` or `fix/`, for example: - `feature/super-puper-update`. + Use `feature/` or `fix/`, for example: + `feature/super-puper-update`. - Follow the existing project architecture, file structure, APIs, and coding - style. Do not introduce a new pattern when an existing one fits. + style. Do not introduce a new pattern when an existing one fits. ## Pull Requests 1. Base your branch on the latest `master`. 2. Test your changes before submitting them: `npm start`. 3. Keep commit messages short and precise. Use the project convention: - - `feat: add support for a new tool` - - `fix: fix command execution timeout` - - `docs: update README` - - `refactor: restructure tool registration` + - `feat: add support for a new tool` + - `fix: fix command execution timeout` + - `docs: update README` + - `refactor: restructure tool registration` 4. Describe what changed and how it was tested in the pull request. ## Bug Reports @@ -132,7 +136,12 @@ Use the existing implementations in `tools/` as a reference: 4. Register it in `ToolRegistry` (see `src/main/tool-registry.js`). 5. Update `tools/rules.md` and `README.md`. +## Code of Conduct + +By participating in this project, you agree to abide by the +[Code of Conduct](CODE_OF_CONDUCT.md). + ## License -The project is distributed under the GPL-3.0 license. All contributions are -subject to the same license. +The project is distributed under the MIT license. All contributions are subject +to the same license. From b230eab3e0b6cefe4ef6cfc6a264e0dab481a812 Mon Sep 17 00:00:00 2001 From: MerfiDEV Date: Sat, 19 Sep 2026 01:33:01 +0300 Subject: [PATCH 5/6] Add custom font color support Introduce a fontColor setting and UI to let users choose a custom text color. Defaults to empty (use system color). Implemented normalizeColor(), applyColor(), and wired color into buildFontCss() to emit !important color rules for Cookie Code UI and page (excludes overlay and icon fonts). Updated settings loader/listener to handle fontColor. Added a new built-in font (ndot-47) and its OTF file. Added color picker, reset button, handlers and refresh logic in settings-tab, and i18n keys for the new UI labels. --- src/main/settings-store.js | 2 + src/preload/dom/fonts.js | 125 ++++++++++++++++++- src/preload/dom/settings-tab.js | 84 +++++++++++++ src/preload/i18n/i18n.js | 3 + src/ui/fonts/ndot-47-inspired-by-nothing.otf | Bin 0 -> 62188 bytes 5 files changed, 208 insertions(+), 6 deletions(-) create mode 100644 src/ui/fonts/ndot-47-inspired-by-nothing.otf diff --git a/src/main/settings-store.js b/src/main/settings-store.js index ed93b07..2104061 100644 --- a/src/main/settings-store.js +++ b/src/main/settings-store.js @@ -30,6 +30,8 @@ const DEFAULTS = { font: "system", // Жирность шрифта (100–900). Применяется и к системному, и к кастомному. fontWeight: 400, + // Кастомный цвет текста ("#rrggbb"). "" — системный (не переопределять). + fontColor: "", backgroundBlur: 0, // px — размытие самой картинки фона headerBlur: 12, // px — стекло верхней панели sidebarBlur: 12, // px — стекло левого сайдбара diff --git a/src/preload/dom/fonts.js b/src/preload/dom/fonts.js index ce1b1f5..daa8906 100644 --- a/src/preload/dom/fonts.js +++ b/src/preload/dom/fonts.js @@ -29,6 +29,12 @@ const BUILTIN_FONTS = [ file: ["Anthropic Mono Web.otf", "Anthropic Mono Web Regular Italic.otf"], family: "Anthropic Mono", }, + { + id: "ndot-47", + label: "Ndot 47", + file: "ndot-47-inspired-by-nothing.otf", + family: "Ndot 47", + }, ]; const DEFAULT_ID = "system"; @@ -36,9 +42,11 @@ const DEFAULT_ID = "system"; // Жирность по умолчанию (обычное начертание). const DEFAULT_WEIGHT = 400; -// Текущий выбранный шрифт и жирность (для повторного применения). +// Текущий выбранный шрифт, жирность и цвет (для повторного применения). let currentFontId = DEFAULT_ID; let currentWeight = DEFAULT_WEIGHT; +// Кастомный цвет текста ("#rrggbb"). "" — системный (правило не применяется). +let currentColor = ""; // Стиль инъекции (id