' +
t("settings.section.background") +
@@ -591,6 +630,73 @@ function buildContentHTML() {
);
}
+/**
+ * HTML-секция «Чубрики (петы)» — карточка в настройках Cookie Code.
+ * Список спрайтов + тумблер debug-режима + кнопка открыть папку.
+ * Данные подгружаются асинхронно через window.electronAPI.listPets().
+ */
+function buildPetsSection() {
+ return (
+ "
" +
+ '
' +
+ t("settings.section.pets") +
+ "
" +
+ '
' +
+ '
" +
+ '
' +
+ "
" +
+ '
' +
+ t("settings.pets.pickTitle") +
+ "
" +
+ '
' +
+ t("settings.pets.pickHint") +
+ "
" +
+ "
" +
+ '
' +
+ ' " +
+ ' " +
+ ' " +
+ ' " +
+ "
" +
+ '
' +
+ '
' +
+ t("settings.pets.empty") +
+ "
" +
+ "
" +
+ '
" +
+ "
" +
+ "
"
+ );
+}
+
function escapeHtml(s) {
return String(s)
.replace(/&/g, "&")
@@ -633,6 +739,529 @@ function bindBackgroundGrid() {
});
}
+// ===== Чубрики (петы) =====
+
+/**
+ * Обработчики секции «Чубрики»: список спрайтов, тумблер debug-режима,
+ * кнопки «Обновить» / «Открыть папку», клики по превью.
+ */
+function bindPetsSection() {
+ const grid = document.getElementById("cuckoo-pets-grid");
+ const empty = document.getElementById("cuckoo-pets-empty");
+ const refreshBtn = document.getElementById("cuckoo-pets-refresh");
+ const openBtn = document.getElementById("cuckoo-pets-open-folder");
+ const importBtn = document.getElementById("cuckoo-pets-import");
+ const resetBtn = document.getElementById("cuckoo-pets-reset");
+ const debugChk = document.getElementById("cuckoo-pet-debug-mode");
+ const enabledChk = document.getElementById("cuckoo-pet-enabled");
+ if (!grid) return;
+
+ let currentPetId = "";
+ let currentPetFile = "";
+
+ const renderPets = (pets) => {
+ grid.innerHTML = "";
+ if (!pets || pets.length === 0) {
+ empty.style.display = "";
+ return;
+ }
+ empty.style.display = "none";
+ pets.forEach((p) => {
+ const item = document.createElement("div");
+ item.className = "cuckoo-bg-item";
+ item.setAttribute("data-pet-id", p.id);
+ item.setAttribute("data-pet-file", p.file);
+ const active =
+ p.id === currentPetId || p.file === currentPetFile
+ ? " cuckoo-bg-selected"
+ : "";
+ item.className += active;
+ item.style.position = "relative";
+ item.title = p.label;
+ // Превью
+ const prev = document.createElement("div");
+ prev.className = "cuckoo-bg-preview";
+ prev.style.backgroundImage =
+ 'url("' + background.getPreviewUri(p.file) + '")';
+ prev.style.backgroundSize = "contain";
+ prev.style.backgroundRepeat = "no-repeat";
+ item.appendChild(prev);
+ // Подпись
+ const lbl = document.createElement("div");
+ lbl.className = "cuckoo-bg-label";
+ lbl.textContent = p.label;
+ item.appendChild(lbl);
+
+ // Кнопка «Вырезать фон» — только для GIF
+ const isGif = /\.gif$/i.test(p.file);
+ if (isGif) {
+ const chromaBtn = document.createElement("button");
+ chromaBtn.className = "ck-btn";
+ chromaBtn.style.cssText =
+ "position:absolute;top:4px;right:4px;padding:3px 6px;font-size:10px;" +
+ "border-radius:6px;background:rgba(139,147,255,0.85);color:#fff;" +
+ "border:none;cursor:pointer;z-index:2;opacity:0.9;";
+ chromaBtn.textContent = t("settings.pets.chromaBtn");
+ chromaBtn.title = t("settings.pets.chromaTitle");
+ chromaBtn.addEventListener("click", (e) => {
+ e.stopPropagation();
+ openChromaModal(p);
+ });
+ item.appendChild(chromaBtn);
+ }
+
+ // Клик — выбрать
+ item.addEventListener("click", async () => {
+ try {
+ currentPetId = p.id;
+ currentPetFile = p.file;
+ await window.electronAPI.setCuckooSetting("petId", p.id);
+ // Просим пет перечитать настройки (если preload уже отрисовал пета)
+ try {
+ const pet = require("./pet");
+ if (pet && typeof pet.reloadSettings === "function") {
+ await pet.reloadSettings();
+ }
+ } catch (_) {}
+ // Перерисовать сетку с активной рамкой
+ grid.querySelectorAll(".cuckoo-bg-item").forEach((el) => {
+ el.classList.toggle(
+ "cuckoo-bg-selected",
+ el.getAttribute("data-pet-id") === p.id,
+ );
+ });
+ } catch (err) {
+ console.error("[Cookie Code] Не удалось выбрать пета:", err.message);
+ }
+ });
+ grid.appendChild(item);
+ });
+ };
+
+ const loadPets = async () => {
+ try {
+ const settings = await window.electronAPI.getCuckooSettings();
+ currentPetId = (settings && settings.petId) || "";
+ const res = await window.electronAPI.listPets();
+ const pets = (res && res.pets) || [];
+ // Если petId пустой — пометим первый файл как выбранный
+ if (!currentPetId && pets.length > 0) currentPetId = pets[0].id;
+ renderPets(pets);
+ } catch (err) {
+ console.error(
+ "[Cookie Code] Не удалось загрузить список петов:",
+ err.message,
+ );
+ empty.style.display = "";
+ }
+ };
+
+ if (refreshBtn) refreshBtn.addEventListener("click", loadPets);
+
+ if (resetBtn)
+ resetBtn.addEventListener("click", async () => {
+ try {
+ try {
+ const pet = require("./pet");
+ if (pet && typeof pet.resetAllPetSettings === "function") {
+ await pet.resetAllPetSettings();
+ }
+ } catch (_) {}
+ window.electronAPI.showBannerNotification(
+ t("settings.pets.resetDone"),
+ { duration: 3500 },
+ );
+ await loadPets();
+ } catch (err) {
+ console.error("[Cookie Code] Сброс настроек пета失败:", err.message);
+ }
+ });
+
+ if (importBtn)
+ importBtn.addEventListener("click", async () => {
+ try {
+ const res = await window.electronAPI.importPet();
+ if (!res || res.canceled) return;
+ if (!res.success) {
+ window.electronAPI.showBannerNotification(
+ t("settings.pets.importError").replace(
+ "{msg}",
+ res.error || "unknown",
+ ),
+ { duration: 6000 },
+ );
+ return;
+ }
+
+ // Если файл был сжат — сообщим размеры до/после
+ if (res.normalized && res.before && res.after) {
+ window.electronAPI.showBannerNotification(
+ t("settings.pets.importCompressed")
+ .replace("{w1}", String(res.before.w))
+ .replace("{h1}", String(res.before.h))
+ .replace("{w2}", String(res.after.w))
+ .replace("{h2}", String(res.after.h)),
+ { duration: 5000 },
+ );
+ }
+
+ // Автоматически выбираем загруженного пета
+ try {
+ await window.electronAPI.setCuckooSetting("petId", res.id);
+ currentPetId = res.id;
+ try {
+ const pet = require("./pet");
+ if (pet && typeof pet.reloadSettings === "function") {
+ await pet.reloadSettings();
+ }
+ } catch (_) {}
+ } catch (_) {}
+
+ await loadPets();
+ } catch (err) {
+ console.error("[Cookie Code] Импорт пета失败:", err.message);
+ }
+ });
+
+ if (openBtn)
+ openBtn.addEventListener("click", async () => {
+ try {
+ await window.electronAPI.openPetsFolder();
+ // после открытия — обновим список через небольшую задержку
+ setTimeout(loadPets, 800);
+ } catch (err) {
+ console.error(
+ "[Cookie Code] Не удалось открыть папку петов:",
+ err.message,
+ );
+ }
+ });
+
+ if (enabledChk) {
+ enabledChk.addEventListener("change", async () => {
+ const on = enabledChk.checked;
+ try {
+ await window.electronAPI.setCuckooSetting("petEnabled", on);
+ try {
+ const pet = require("./pet");
+ if (pet && typeof pet.setEnabled === "function") pet.setEnabled(on);
+ } catch (_) {}
+ } catch (err) {
+ console.error(
+ "[Cookie Code] Не удалось сохранить petEnabled:",
+ err.message,
+ );
+ }
+ });
+ }
+
+ if (debugChk) {
+ debugChk.addEventListener("change", async () => {
+ const on = debugChk.checked;
+ try {
+ await window.electronAPI.setCuckooSetting("petDebugMode", on);
+ try {
+ const pet = require("./pet");
+ if (pet && typeof pet.setDebugMode === "function")
+ pet.setDebugMode(on);
+ } catch (_) {}
+ } catch (err) {
+ console.error(
+ "[Cookie Code] Не удалось сохранить petDebugMode:",
+ err.message,
+ );
+ }
+ });
+ }
+
+ // Начальное состояние тумблера debug-режима + загрузка списка
+ (async () => {
+ try {
+ const settings = await window.electronAPI.getCuckooSettings();
+ if (debugChk) debugChk.checked = !!(settings && settings.petDebugMode);
+ // petEnabled: по умолчанию true → галочка стоит, если не выставлено false.
+ if (enabledChk)
+ enabledChk.checked = !settings || settings.petEnabled !== false;
+ } catch (_) {}
+ await loadPets();
+ })();
+}
+
+// ===== Chroma-key редактор для GIF =====
+
+/**
+ * Открыть модалку редактора фона для GIF.
+ * Показывает первый кадр, даёт пипетку и слайдер tolerance, применяет
+ * через main-процесс (IPC cuckoo-pets-chroma).
+ *
+ * @param {{id: string, label: string, file: string}} pet
+ */
+function openChromaModal(pet) {
+ // Уже есть — закрываем старую
+ const old = document.getElementById("cuckoo-chroma-modal");
+ if (old) old.remove();
+
+ const modal = document.createElement("div");
+ modal.id = "cuckoo-chroma-modal";
+ modal.style.cssText =
+ "position:fixed;inset:0;z-index:2147483650;background:rgba(6,8,18,0.78);" +
+ "display:flex;align-items:center;justify-content:center;padding:20px;";
+
+ const inner = document.createElement("div");
+ inner.style.cssText =
+ "background:#141726;border:1px solid rgba(139,147,255,0.35);border-radius:14px;" +
+ "width:min(640px,100%);max-height:90vh;display:flex;flex-direction:column;" +
+ "box-shadow:0 20px 60px rgba(0,0,0,0.6);overflow:hidden;";
+
+ // Header
+ const header = document.createElement("div");
+ header.style.cssText =
+ "padding:14px 18px;border-bottom:1px solid rgba(255,255,255,0.07);" +
+ "display:flex;justify-content:space-between;align-items:center;";
+ const title = document.createElement("div");
+ title.style.cssText = "font-size:15px;font-weight:700;color:#eef0ff;";
+ title.textContent = t("settings.pets.chromaModalTitle");
+ const closeBtn = document.createElement("button");
+ closeBtn.className = "ck-btn";
+ closeBtn.style.cssText = "padding:6px 12px;font-size:12px;";
+ closeBtn.textContent = "✕";
+ closeBtn.addEventListener("click", () => modal.remove());
+ header.appendChild(title);
+ header.appendChild(closeBtn);
+ inner.appendChild(header);
+
+ // Body
+ const body = document.createElement("div");
+ body.style.cssText =
+ "padding:14px 18px;overflow-y:auto;display:flex;flex-direction:column;gap:12px;";
+
+ const hint = document.createElement("div");
+ hint.className = "ck-row-hint";
+ hint.style.margin = "0";
+ hint.textContent = t("settings.pets.chromaHint");
+ body.appendChild(hint);
+
+ // Canvas
+ const canvasWrap = document.createElement("div");
+ canvasWrap.style.cssText =
+ "background:repeating-conic-gradient(#2a2d40 0% 25%, #1d2030 0% 50%) 50% / 16px 16px;" +
+ "border-radius:10px;padding:8px;display:flex;justify-content:center;";
+ const canvas = document.createElement("canvas");
+ canvas.style.cssText =
+ "max-width:100%;max-height:320px;image-rendering:pixelated;cursor:crosshair;" +
+ "border-radius:6px;";
+ canvasWrap.appendChild(canvas);
+ body.appendChild(canvasWrap);
+
+ const previewHint = document.createElement("div");
+ previewHint.className = "ck-row-hint";
+ previewHint.style.margin = "0";
+ previewHint.textContent = t("settings.pets.chromaPreviewHint");
+ body.appendChild(previewHint);
+
+ // Выбранный цвет
+ const colorRow = document.createElement("div");
+ colorRow.style.cssText =
+ "display:flex;align-items:center;gap:10px;font-size:12px;color:#cfd3ff;";
+ const colorSwatch = document.createElement("div");
+ colorSwatch.style.cssText =
+ "width:24px;height:24px;border-radius:6px;border:1px solid rgba(255,255,255,0.25);" +
+ "background:#000;flex-shrink:0;";
+ const colorLabel = document.createElement("span");
+ colorLabel.textContent = t("settings.pets.chromaPicked").replace(
+ "{color}",
+ "—",
+ );
+ colorRow.appendChild(colorSwatch);
+ colorRow.appendChild(colorLabel);
+ body.appendChild(colorRow);
+
+ // Tolerance
+ const tolRow = document.createElement("div");
+ tolRow.className = "cuckoo-blur-row";
+ tolRow.style.cssText =
+ "display:flex;flex-direction:column;gap:6px;padding:10px 12px;" +
+ "background:rgba(255,255,255,0.035);border:1px solid rgba(255,255,255,0.07);" +
+ "border-radius:10px;";
+ const tolLabel = document.createElement("div");
+ tolLabel.className = "cuckoo-blur-label";
+ tolLabel.innerHTML =
+ "
" +
+ t("settings.pets.chromaTolerance") +
+ "" +
+ '
16';
+ const tolInput = document.createElement("input");
+ tolInput.type = "range";
+ tolInput.min = "0";
+ tolInput.max = "128";
+ tolInput.value = "16";
+ tolInput.className = "cuckoo-blur-slider";
+ tolInput.id = "cuckoo-chroma-tol";
+ tolInput.addEventListener("input", () => {
+ document.getElementById("cuckoo-chroma-tol-val").textContent =
+ tolInput.value;
+ });
+ tolRow.appendChild(tolLabel);
+ tolRow.appendChild(tolInput);
+ body.appendChild(tolRow);
+
+ // Footer
+ const footer = document.createElement("div");
+ footer.style.cssText =
+ "padding:12px 18px;border-top:1px solid rgba(255,255,255,0.07);" +
+ "display:flex;gap:8px;justify-content:flex-end;";
+ const cancelBtn = document.createElement("button");
+ cancelBtn.className = "ck-btn";
+ cancelBtn.textContent = t("settings.pets.chromaCancel");
+ cancelBtn.addEventListener("click", () => modal.remove());
+ const applyBtn = document.createElement("button");
+ applyBtn.className = "ck-btn";
+ applyBtn.style.background = "rgba(139,147,255,0.35)";
+ applyBtn.textContent = t("settings.pets.chromaApply");
+ footer.appendChild(cancelBtn);
+ footer.appendChild(applyBtn);
+
+ inner.appendChild(body);
+ inner.appendChild(footer);
+ modal.appendChild(inner);
+ document.body.appendChild(modal);
+
+ // ===== Загрузка GIF в canvas =====
+ const img = new Image();
+ const dataUri = background.getPreviewUri(pet.file);
+ let pickedColor = null; // {r,g,b,hex}
+
+ const redraw = () => {
+ if (!img.naturalWidth) return;
+ canvas.width = img.naturalWidth;
+ canvas.height = img.naturalHeight;
+ const ctx = canvas.getContext("2d");
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
+ ctx.drawImage(img, 0, 0);
+ };
+
+ img.onload = () => {
+ redraw();
+ };
+ img.onerror = () => {
+ console.error("[Cookie Code] Не удалось загрузить GIF:", pet.file);
+ };
+ img.src = dataUri;
+
+ // ===== Пипетка =====
+ canvas.addEventListener("click", (e) => {
+ const rect = canvas.getBoundingClientRect();
+ const x = Math.floor(((e.clientX - rect.left) / rect.width) * canvas.width);
+ const y = Math.floor(
+ ((e.clientY - rect.top) / rect.height) * canvas.height,
+ );
+ if (x < 0 || y < 0 || x >= canvas.width || y >= canvas.height) return;
+ const ctx = canvas.getContext("2d");
+ const px = ctx.getImageData(x, y, 1, 1).data;
+ const r = px[0];
+ const g = px[1];
+ const b = px[2];
+ const hex =
+ "#" +
+ ((1 << 24) | (r << 16) | (g << 8) | b)
+ .toString(16)
+ .slice(1)
+ .toUpperCase();
+ pickedColor = { r, g, b, hex };
+ colorSwatch.style.background = hex;
+ colorLabel.textContent = t("settings.pets.chromaPicked").replace(
+ "{color}",
+ hex,
+ );
+ });
+
+ // ===== Применить =====
+ applyBtn.addEventListener("click", async () => {
+ if (!pickedColor) {
+ await window.electronAPI.showBannerNotification(
+ t("settings.pets.chromaNoColor"),
+ { duration: 4000 },
+ );
+ return;
+ }
+ applyBtn.disabled = true;
+ const oldText = applyBtn.textContent;
+ applyBtn.textContent = t("settings.pets.chromaProcessing");
+ try {
+ const tolerance = Number(tolInput.value) || 0;
+ const res = await window.electronAPI.chromaKeyPet(
+ pet.file,
+ pickedColor.hex,
+ tolerance,
+ );
+ if (res && res.success) {
+ await window.electronAPI.showBannerNotification(
+ t("settings.pets.chromaDone"),
+ { duration: 4000 },
+ );
+ modal.remove();
+ // Сброс кэша data-URI в background, перерисовка превью и пета.
+ try {
+ if (typeof background.invalidatePreviewCache === "function") {
+ background.invalidatePreviewCache(pet.file);
+ }
+ } catch (_) {}
+ try {
+ const itemEl = document.querySelector(
+ '#cuckoo-pets-grid .cuckoo-bg-item[data-pet-file="' +
+ String(pet.file).replace(/"/g, '\\"') +
+ '"]',
+ );
+ if (itemEl) {
+ const prev = itemEl.querySelector(".cuckoo-bg-preview");
+ if (prev) {
+ const newUri = background.getPreviewUri(pet.file);
+ prev.style.backgroundImage = 'url("' + newUri + '")';
+ }
+ }
+ } catch (err) {
+ console.error(
+ "[Cookie Code] Не удалось обновить превью:",
+ err.message,
+ );
+ }
+ try {
+ const petMod = require("./pet");
+ if (petMod && typeof petMod.reloadSettings === "function") {
+ await petMod.reloadSettings();
+ }
+ } catch (_) {}
+ } else {
+ await window.electronAPI.showBannerNotification(
+ t("settings.pets.chromaError").replace(
+ "{msg}",
+ (res && res.error) || "unknown",
+ ),
+ { duration: 6000 },
+ );
+ applyBtn.disabled = false;
+ applyBtn.textContent = oldText;
+ }
+ } catch (err) {
+ await window.electronAPI.showBannerNotification(
+ t("settings.pets.chromaError").replace("{msg}", err.message),
+ { duration: 6000 },
+ );
+ applyBtn.disabled = false;
+ applyBtn.textContent = oldText;
+ }
+ });
+
+ // Esc — закрыть
+ const onKey = (e) => {
+ if (e.key === "Escape") {
+ modal.remove();
+ window.removeEventListener("keydown", onKey, true);
+ }
+ };
+ window.addEventListener("keydown", onKey, true);
+}
+
/**
* Собрать HTML одного превью фона.
*/
@@ -1390,6 +2019,43 @@ function bindAgentSettings() {
});
});
+ // ===== Таймаут JS-скриптов =====
+ const timeoutInput = document.getElementById("cuckoo-js-timeout");
+ const timeoutSave = document.getElementById("cuckoo-js-timeout-save");
+ if (timeoutInput && timeoutSave) {
+ const trySave = async () => {
+ const raw = Number(timeoutInput.value);
+ if (!Number.isFinite(raw) || raw < 10 || raw > 1000) {
+ window.electronAPI.showBannerNotification(
+ t("settings.jsTimeout.invalid"),
+ { duration: 4000 },
+ );
+ return;
+ }
+ const sec = Math.round(raw);
+ timeoutInput.value = sec;
+ try {
+ await window.electronAPI.setCuckooSetting("jsTimeoutSec", sec);
+ window.electronAPI.showBannerNotification(
+ t("settings.jsTimeout.saved").replace("{sec}", String(sec)),
+ { duration: 3500 },
+ );
+ } catch (err) {
+ console.error(
+ "[Cookie Code] Не удалось сохранить jsTimeoutSec:",
+ err.message,
+ );
+ }
+ };
+ timeoutSave.addEventListener("click", trySave);
+ timeoutInput.addEventListener("keydown", (e) => {
+ if (e.key === "Enter") {
+ e.preventDefault();
+ trySave();
+ }
+ });
+ }
+
const cb = document.getElementById("cuckoo-hide-system-messages");
if (cb) {
cb.addEventListener("change", async () => {
@@ -1565,6 +2231,14 @@ async function refreshAgentSettings() {
applyConvTokensVisibility(convTokensCb.checked);
} catch (_) {}
}
+ // Таймаут JS-скриптов
+ const timeoutInput = document.getElementById("cuckoo-js-timeout");
+ if (timeoutInput) {
+ const sec = Number(s && s.jsTimeoutSec);
+ timeoutInput.value = String(
+ Number.isFinite(sec) && sec >= 10 && sec <= 1000 ? Math.round(sec) : 60,
+ );
+ }
} catch (_) {}
}
diff --git a/src/preload/i18n/i18n.js b/src/preload/i18n/i18n.js
index 9f41611..a805900 100644
--- a/src/preload/i18n/i18n.js
+++ b/src/preload/i18n/i18n.js
@@ -321,6 +321,108 @@ const KEYS = {
en: "Dangerous commands (regex, one per line)",
},
"settings.section.background": { ru: "Фон страницы", en: "Page background" },
+ // ---- Чубрики (петы) ----
+ "settings.section.pets": { ru: "Чубрики", en: "Pets" },
+ "settings.pets.chromaBtn": { ru: "🎨 Фон", en: "🎨 BG" },
+ "settings.pets.chromaTitle": {
+ ru: "Вырезать цвет фона (chroma-key)",
+ en: "Remove background color (chroma-key)",
+ },
+ "settings.pets.chromaModalTitle": {
+ ru: "Вырезать фон GIF",
+ en: "Remove GIF background",
+ },
+ "settings.pets.chromaHint": {
+ ru: "Кликните по картинке, чтобы выбрать цвет фона. Все пиксели этого цвета станут прозрачными во всех кадрах.",
+ en: "Click the image to pick the background color. All pixels of that color become transparent in every frame.",
+ },
+ "settings.pets.chromaPicked": {
+ ru: "Выбран цвет: {color}",
+ en: "Picked color: {color}",
+ },
+ "settings.pets.chromaTolerance": {
+ ru: "Допуск (похожесть оттенков)",
+ en: "Tolerance (color similarity)",
+ },
+ "settings.pets.chromaApply": { ru: "Применить", en: "Apply" },
+ "settings.pets.chromaCancel": { ru: "Отмена", en: "Cancel" },
+ "settings.pets.chromaNoColor": {
+ ru: "Сначала выберите цвет кликом по картинке",
+ en: "Pick a color by clicking the image first",
+ },
+ "settings.pets.chromaDone": {
+ ru: "Готово! Фон удалён во всех кадрах.",
+ en: "Done! Background removed in all frames.",
+ },
+ "settings.pets.chromaError": {
+ ru: "Ошибка: {msg}",
+ en: "Error: {msg}",
+ },
+ "settings.pets.chromaProcessing": {
+ ru: "Обработка…",
+ en: "Processing…",
+ },
+ "settings.pets.chromaPreviewHint": {
+ ru: "Превью — 1-й кадр. Цвет применяется ко всем кадрам.",
+ en: "Preview is frame 1. Color is applied to every frame.",
+ },
+ "settings.pets.enabledTitle": { ru: "Пет включён", en: "Pet enabled" },
+ "settings.pets.enabledHint": {
+ ru: "Показывать чубрика на экране. По умолчанию — да.",
+ en: "Show the pet on screen. Enabled by default.",
+ },
+ "settings.pets.pickTitle": { ru: "Выбрать пета", en: "Choose a pet" },
+ "settings.pets.pickHint": {
+ ru: "PNG/GIF/WebP/JPG. Большие картинки автоматически сжимаются до 600×600. Кликните на превью, чтобы выбрать.",
+ en: "PNG/GIF/WebP/JPG. Large images are auto-resized to 600×600. Click a preview to select.",
+ },
+ "settings.pets.import": { ru: "Загрузить…", en: "Upload…" },
+ "settings.pets.reset": { ru: "Сбросить", en: "Reset" },
+ // ---- Таймаут JS-скриптов ----
+ "settings.jsTimeout.title": {
+ ru: "Таймаут выполнения JS-скриптов",
+ en: "JS script execution timeout",
+ },
+ "settings.jsTimeout.hint": {
+ ru: "Максимальное время выполнения одного блока кода от AI. От 10 до 1000 секунд. По умолчанию 60 сек.",
+ en: "Maximum execution time for a single AI code block. From 10 to 1000 seconds. Default is 60 sec.",
+ },
+ "settings.jsTimeout.unit": { ru: "сек", en: "sec" },
+ "settings.jsTimeout.save": { ru: "Сохранить", en: "Save" },
+ "settings.jsTimeout.saved": {
+ ru: "Таймаут сохранён: {sec} сек",
+ en: "Timeout saved: {sec} sec",
+ },
+ "settings.jsTimeout.invalid": {
+ ru: "Введите число от 10 до 1000",
+ en: "Enter a number between 10 and 1000",
+ },
+ "settings.pets.resetDone": {
+ ru: "Настройки пета сброшены к дефолтам",
+ en: "Pet settings reset to defaults",
+ },
+ "settings.pets.refresh": { ru: "Обновить", en: "Refresh" },
+ "settings.pets.openFolder": { ru: "Открыть папку", en: "Open folder" },
+ "settings.pets.empty": {
+ ru: "В папке пока нет PNG/GIF. Нажмите «Открыть папку» и положите туда спрайт.",
+ en: 'No PNG/GIF in the folder yet. Click "Open folder" and drop a sprite there.',
+ },
+ "settings.pets.debugTitle": {
+ ru: "Debug-режим пета",
+ en: "Pet debug mode",
+ },
+ "settings.pets.debugHint": {
+ ru: "Разблокирует горячие клавиши: F8 — прицел (посадить пета), F9 — рамка поля, F10 — режим, F11 — сброс размера.",
+ en: "Unlocks hotkeys: F8 — aim (place the pet), F9 — field frame, F10 — mode, F11 — reset size.",
+ },
+ "settings.pets.importError": {
+ ru: "Не удалось загрузить пета: {msg}",
+ en: "Failed to upload pet: {msg}",
+ },
+ "settings.pets.importCompressed": {
+ ru: "Пет сжат: {w1}×{h1} → {w2}×{h2}",
+ en: "Pet compressed: {w1}×{h1} → {w2}×{h2}",
+ },
"settings.bg.openFolder": {
ru: "Открыть папку фонов",
en: "Open backgrounds folder",
diff --git a/src/preload/index.js b/src/preload/index.js
index 6a50a50..3c9ed04 100644
--- a/src/preload/index.js
+++ b/src/preload/index.js
@@ -6,35 +6,35 @@
* Каждый шаг обёрнут в safe(): сбой одного модуля (например, из-за изменений
* вёрстки DeepSeek) не валит init() и не мешает остальным модулям.
*/
-console.log('[Cookie Code] Preload script 开始执行');
+console.log("[Cookie Code] Preload script 开始执行");
// 暴露 electronAPI 到渲染进程(contextBridge + window 兜底)
-require('./api');
-
-const ui = require('./overlay/ui');
-const projectDir = require('./overlay/project-dir');
-const bindEvents = require('./overlay/events');
-const observer = require('./dom/observer');
-const chatExport = require('./dom/chat-export');
-const chatInput = require('./dom/chat-input');
-const askUserQuestion = require('./dom/ask-user-question');
-const exitPlanMode = require('./dom/exit-plan-mode');
-const planModeToggle = require('./dom/plan-mode-toggle');
-const stealth = require('./dom/stealth');
-const settingsTab = require('./dom/settings-tab');
-const commands = require('./dom/commands');
-const background = require('./dom/background');
-const reasoningGlass = require('./dom/reasoning-glass');
-const inputGlass = require('./dom/input-glass');
-const forceDarkTheme = require('./dom/force-dark-theme');
-const qrOverride = require('./dom/qr-override');
-const fileChip = require('./dom/file-chip');
-const whatsNew = require('./dom/whats-new');
-const i18n = require('./i18n/i18n');
-const state = require('./dom/state');
-const tokenInterceptor = require('./dom/token-interceptor');
-const { getProviderByUrl } = require('../providers');
-const { safe } = require('./dom/safe');
+require("./api");
+
+const ui = require("./overlay/ui");
+const projectDir = require("./overlay/project-dir");
+const bindEvents = require("./overlay/events");
+const observer = require("./dom/observer");
+const chatExport = require("./dom/chat-export");
+const chatInput = require("./dom/chat-input");
+const askUserQuestion = require("./dom/ask-user-question");
+const exitPlanMode = require("./dom/exit-plan-mode");
+const planModeToggle = require("./dom/plan-mode-toggle");
+const stealth = require("./dom/stealth");
+const settingsTab = require("./dom/settings-tab");
+const commands = require("./dom/commands");
+const background = require("./dom/background");
+const reasoningGlass = require("./dom/reasoning-glass");
+const inputGlass = require("./dom/input-glass");
+const forceDarkTheme = require("./dom/force-dark-theme");
+const qrOverride = require("./dom/qr-override");
+const fileChip = require("./dom/file-chip");
+const whatsNew = require("./dom/whats-new");
+const i18n = require("./i18n/i18n");
+const state = require("./dom/state");
+const tokenInterceptor = require("./dom/token-interceptor");
+const { getProviderByUrl } = require("../providers");
+const { safe } = require("./dom/safe");
// ========== 初始化 ==========
@@ -45,22 +45,28 @@ const { safe } = require('./dom/safe');
async function init() {
try {
// Загружаем язык до инъекции HTML — тексты в template.js строятся через t()
- await safe('init.loadLanguage', () => i18n.loadLanguage());
+ await safe("init.loadLanguage", () => i18n.loadLanguage());
let customizationEnabled = true;
try {
const settings = await window.electronAPI.getCuckooSettings();
- customizationEnabled = !settings || settings.customizationEnabled !== false;
+ customizationEnabled =
+ !settings || settings.customizationEnabled !== false;
// Approval gate: режим подтверждения tool-вызовов ('off' | 'risky' | 'all')
- state.toolApprovalMode = (settings && settings.toolApprovalMode) || 'off';
+ state.toolApprovalMode = (settings && settings.toolApprovalMode) || "off";
// Скрытие служебных сообщений (по умолчанию выключено)
- state.hideSystemMessages = Boolean(settings && settings.hideSystemMessages === true);
+ state.hideSystemMessages = Boolean(
+ settings && settings.hideSystemMessages === true,
+ );
// Чипы файловых путей (по умолчанию включено)
state.fileChipEnabled = !settings || settings.fileChipEnabled !== false;
// Блок «Затронуто» под ответом (по умолчанию включено)
- state.showProducedFiles = !settings || settings.showProducedFiles !== false;
+ state.showProducedFiles =
+ !settings || settings.showProducedFiles !== false;
// Блок «Токены диалога» в панели (по умолчанию выключено)
- state.showConvTokens = Boolean(settings && settings.showConvTokens === true);
+ state.showConvTokens = Boolean(
+ settings && settings.showConvTokens === true,
+ );
} catch (_) {}
// Прокидываем флаг в shared state: парсинг работает всегда,
@@ -69,119 +75,167 @@ async function init() {
// Перехват серверных токенов DeepSeek: ставим как можно раньше,
// чтобы поймать первый же запрос completion. Работает через safe().
- safe('init.tokenInterceptor', () => tokenInterceptor.install());
+ safe("init.tokenInterceptor", () => tokenInterceptor.install());
// Регистрируем IPC-листенеры всегда — от них зависит ввод и парсинг tool-блоков
- safe('init.registerIpcListeners', () => chatInput.registerIpcListeners());
- safe('init.registerAskUserQuestionListener', () => askUserQuestion.registerAskUserQuestionListener());
- safe('init.registerExitPlanModeListener', () => exitPlanMode.registerExitPlanModeListener());
- safe('init.registerWhatsNewListener', () => whatsNew.registerWhatsNewListener());
- safe('init.planModeToggleStart', () => planModeToggle.startWatch());
+ safe("init.registerIpcListeners", () => chatInput.registerIpcListeners());
+ safe("init.registerAskUserQuestionListener", () =>
+ askUserQuestion.registerAskUserQuestionListener(),
+ );
+ safe("init.registerExitPlanModeListener", () =>
+ exitPlanMode.registerExitPlanModeListener(),
+ );
+ safe("init.registerWhatsNewListener", () =>
+ whatsNew.registerWhatsNewListener(),
+ );
+ safe("init.planModeToggleStart", () => planModeToggle.startWatch());
// Базовая UI-инфраструктура нужна всегда: оверлей (кнопка), стили, события
- safe('init.injectCSS', () => ui.injectCSS());
- safe('init.injectOverlay', () => ui.injectOverlay());
+ safe("init.injectCSS", () => ui.injectCSS());
+ safe("init.injectOverlay", () => ui.injectOverlay());
// Скрыть блок «Токены диалога», если он выключен в настройках (по умолчанию).
- safe('init.applyConvTokensVisibility', () => {
+ safe("init.applyConvTokensVisibility", () => {
if (state.showConvTokens === true) return;
- const section = document.querySelector('.cuckoo-token-section');
+ const section = document.querySelector(".cuckoo-token-section");
if (section) {
- section.style.display = 'none';
+ section.style.display = "none";
const prev = section.previousElementSibling;
- if (prev && prev.classList && prev.classList.contains('cuckoo-divider')) prev.style.display = 'none';
+ if (prev && prev.classList && prev.classList.contains("cuckoo-divider"))
+ prev.style.display = "none";
}
});
- safe('init.initProjectDirSection', () => projectDir.initProjectDirSection());
- safe('init.bindEvents', () => bindEvents());
- safe('init.updateHomeMode', () => ui.updateHomeMode());
+ safe("init.initProjectDirSection", () =>
+ projectDir.initProjectDirSection(),
+ );
+ safe("init.bindEvents", () => bindEvents());
+ safe("init.updateHomeMode", () => ui.updateHomeMode());
// Сохранить токены текущего диалога перед уходом/закрытием.
- window.addEventListener('beforeunload', () => {
- safe('init.saveTokensBeforeUnload', () => bindEvents.computeAndSaveConversationTokens());
+ window.addEventListener("beforeunload", () => {
+ safe("init.saveTokensBeforeUnload", () =>
+ bindEvents.computeAndSaveConversationTokens(),
+ );
});
// 监听 URL 变化(SPA 路由)
- window.addEventListener('popstate', () => {
- safe('init.updateHomeModePop', () => ui.updateHomeMode());
- safe('init.refreshTokensPop', () => bindEvents.updateConversationTokenDisplay());
+ window.addEventListener("popstate", () => {
+ safe("init.updateHomeModePop", () => ui.updateHomeMode());
+ safe("init.refreshTokensPop", () =>
+ bindEvents.updateConversationTokenDisplay(),
+ );
});
- window.addEventListener('hashchange', () => {
- safe('init.updateHomeModeHash', () => ui.updateHomeMode());
- safe('init.refreshTokensHash', () => bindEvents.updateConversationTokenDisplay());
+ window.addEventListener("hashchange", () => {
+ safe("init.updateHomeModeHash", () => ui.updateHomeMode());
+ safe("init.refreshTokensHash", () =>
+ bindEvents.updateConversationTokenDisplay(),
+ );
});
- setInterval(() => safe('init.updateHomeModeInterval', () => ui.updateHomeMode()), 5000);
+ setInterval(
+ () => safe("init.updateHomeModeInterval", () => ui.updateHomeMode()),
+ 5000,
+ );
// 首次延迟执行,确保 overlay 已注入
- setTimeout(() => safe('init.updateHomeModeDelayed', () => ui.updateHomeMode()), 500);
+ setTimeout(
+ () => safe("init.updateHomeModeDelayed", () => ui.updateHomeMode()),
+ 500,
+ );
// 默认显示覆盖层 - 兜底强制显示
- safe('init.forceShowOverlay', () => ui.forceShowOverlay());
+ safe("init.forceShowOverlay", () => ui.forceShowOverlay());
// 延迟启动观察器,等待页面框架渲染
- setTimeout(() => safe('init.startObserver', () => observer.startObserver()), 2000);
+ setTimeout(
+ () => safe("init.startObserver", () => observer.startObserver()),
+ 2000,
+ );
// Скрытие служебных сообщений (результаты инструментов, системный промпт).
// Поведенческая фича — работает независимо от customizationEnabled.
- safe('init.startStealthWatcher', () => stealth.startStealthWatcher());
+ safe("init.startStealthWatcher", () => stealth.startStealthWatcher());
// 启动设置面板标签注入
- safe('init.settingsTabStart', () => settingsTab.start());
+ safe("init.settingsTabStart", () => settingsTab.start());
// Slash-команды: автодополнение (review/summarize)
- safe('init.commandsStart', () => commands.start());
+ safe("init.commandsStart", () => commands.start());
// Принудительно держим тёмную тему DeepSeek
- safe('init.forceDarkThemeStart', () => forceDarkTheme.startWatch());
+ safe("init.forceDarkThemeStart", () => forceDarkTheme.startWatch());
// Подмена QR-кода в попапе «Скачать приложение»
- safe('init.qrOverrideStart', () => qrOverride.startWatch());
+ safe("init.qrOverrideStart", () => qrOverride.startWatch());
+
+ // Подписка на изменения настроек из TG-бота / других окон:
+ // фон, блюр и стекло применяются мгновенно без перезахода в настройки.
+ safe("init.backgroundSettingsListener", () =>
+ background.installSettingsListener(),
+ );
// Стилизация абсолютных путей к файлам как чипов с открытием в системе
- safe('init.fileChipSetEnabled', () => fileChip.setEnabled(state.fileChipEnabled !== false));
- safe('init.fileChipStart', () => fileChip.startWatch());
+ safe("init.fileChipSetEnabled", () =>
+ fileChip.setEnabled(state.fileChipEnabled !== false),
+ );
+ safe("init.fileChipStart", () => fileChip.startWatch());
// Блок «Затронуто» под ответом AI — вкл/выкл через настройки
- safe('init.producedFilesSetEnabled', () => {
- const rm = require('./dom/response-meta');
- if (typeof rm.setEnabled === 'function') rm.setEnabled(state.showProducedFiles !== false);
+ safe("init.producedFilesSetEnabled", () => {
+ const rm = require("./dom/response-meta");
+ if (typeof rm.setEnabled === "function")
+ rm.setEnabled(state.showProducedFiles !== false);
});
// Кнопка экспорта ответа в PDF/DOCX под каждым ответом AI
- safe('init.chatExportStart', () => chatExport.startWatch());
+ safe("init.chatExportStart", () => chatExport.startWatch());
// Визуальные эффекты применяем только при включённой кастомизации
if (customizationEnabled) {
// Загружаем настройки и применяем фон
- safe('init.backgroundLoadAndApply', () => background.loadAndApply());
+ safe("init.backgroundLoadAndApply", () => background.loadAndApply());
// Матовое стекло для плашки «Размышление N секунд»
- safe('init.reasoningGlassStart', () => reasoningGlass.startWatch());
+ safe("init.reasoningGlassStart", () => reasoningGlass.startWatch());
// Матовое стекло для поля ввода сообщения
- safe('init.inputGlassStart', () => inputGlass.startWatch());
+ safe("init.inputGlassStart", () => inputGlass.startWatch());
+
+ // Пет (чубрик) на поле ввода + debug-рамка на F9
+ safe("init.petStart", () => require("./dom/pet").start());
} else {
// Сбрасываем возможные визуальные эффекты (фон, блюры, RGB-ник)
- safe('init.backgroundReset', () => background.apply('none'));
- safe('init.backgroundBlurReset', () => background.applyBlur({ backgroundBlur: 0, headerBlur: 0, sidebarBlur: 0, headerOpacity: 0, sidebarOpacity: 0, toolBlockOpacity: 0, toolBlockBlur: 0 }));
- safe('init.backgroundRgbReset', () => background.applyRgbUsername(false));
+ safe("init.backgroundReset", () => background.apply("none"));
+ safe("init.backgroundBlurReset", () =>
+ background.applyBlur({
+ backgroundBlur: 0,
+ headerBlur: 0,
+ sidebarBlur: 0,
+ headerOpacity: 0,
+ sidebarOpacity: 0,
+ toolBlockOpacity: 0,
+ toolBlockBlur: 0,
+ }),
+ );
+ safe("init.backgroundRgbReset", () => background.applyRgbUsername(false));
}
// Снимаем базовый цвет фона/::before-слой при выключенной кастомизации
- safe('init.applyCustomizationEnabled', () => background.applyCustomizationEnabled(customizationEnabled));
+ safe("init.applyCustomizationEnabled", () =>
+ background.applyCustomizationEnabled(customizationEnabled),
+ );
} catch (err) {
- console.error('[Cookie Code] init() 出错:', err);
+ console.error("[Cookie Code] init() 出错:", err);
// 兜底:即使出错也强制显示面板
- safe('init.forceShowOverlayFallback', () => ui.forceShowOverlay());
+ safe("init.forceShowOverlayFallback", () => ui.forceShowOverlay());
}
// 定期巡检:防止面板被意外隐藏
- safe('init.startOverlayWatcher', () => ui.startOverlayWatcher());
+ safe("init.startOverlayWatcher", () => ui.startOverlayWatcher());
// 定期提取当前平台用户信息并更新窗口名
- let lastSentUserName = '';
+ let lastSentUserName = "";
setInterval(() => {
- safe('init.updateWindowName', () => {
+ safe("init.updateWindowName", () => {
const provider = getProviderByUrl(window.location.href);
- if (!provider || typeof provider.extractUserInfo !== 'function') return;
+ if (!provider || typeof provider.extractUserInfo !== "function") return;
const text = provider.extractUserInfo();
if (text && text !== lastSentUserName) {
lastSentUserName = text;
@@ -191,8 +245,10 @@ async function init() {
}, 3000);
}
-if (document.readyState === 'loading') {
- document.addEventListener('DOMContentLoaded', () => { init().catch(err => console.error('[Cookie Code] init error:', err)); });
+if (document.readyState === "loading") {
+ document.addEventListener("DOMContentLoaded", () => {
+ init().catch((err) => console.error("[Cookie Code] init error:", err));
+ });
} else {
- init().catch(err => console.error('[Cookie Code] init error:', err));
+ init().catch((err) => console.error("[Cookie Code] init error:", err));
}
diff --git a/src/ui/pets/20693346_proof.png b/src/ui/pets/20693346_proof.png
new file mode 100644
index 0000000..0575b88
Binary files /dev/null and b/src/ui/pets/20693346_proof.png differ
diff --git a/src/ui/pets/chubrik.png b/src/ui/pets/chubrik.png
new file mode 100644
index 0000000..558b80d
Binary files /dev/null and b/src/ui/pets/chubrik.png differ
diff --git a/tools/JsRunner.js b/tools/JsRunner.js
index 8d20ddb..e4dba2c 100644
--- a/tools/JsRunner.js
+++ b/tools/JsRunner.js
@@ -17,11 +17,34 @@ const { decodeOutput, normalizeCommand } = require("./decodeOutput");
// 同步执行超时(vm timeout,覆盖无 await 的死循环)
const SYNC_TIMEOUT = 30 * 1000;
-// 整体运行截止时间(配合宿主桥接检查,覆盖 async 死循环)
-const RUN_DEADLINE = 60 * 1000;
+// 整体运行截止时间的默认值(мс);реальное значение читается из настроек.
+const RUN_DEADLINE_DEFAULT = 60 * 1000;
+const RUN_DEADLINE_MIN = 10 * 1000;
+const RUN_DEADLINE_MAX = 1000 * 1000;
// 输出长度上限
const OUTPUT_LIMIT = 20000;
+/**
+ * Текущий дедлайн выполнения одного JS-блока (мс).
+ * Читается из settings.json (ключ jsTimeoutSec, в секундах).
+ * При ошибке чтения — дефолт 60 сек.
+ * Клампится в [RUN_DEADLINE_MIN, RUN_DEADLINE_MAX].
+ */
+function getRunDeadlineMs() {
+ try {
+ const settingsStore = require("../src/main/settings-store");
+ const s = settingsStore.readSettings();
+ const sec = Number(s && s.jsTimeoutSec);
+ if (!Number.isFinite(sec) || sec <= 0) return RUN_DEADLINE_DEFAULT;
+ const ms = sec * 1000;
+ if (ms < RUN_DEADLINE_MIN) return RUN_DEADLINE_MIN;
+ if (ms > RUN_DEADLINE_MAX) return RUN_DEADLINE_MAX;
+ return ms;
+ } catch (_) {
+ return RUN_DEADLINE_DEFAULT;
+ }
+}
+
/**
* 沙箱初始化脚本:在沙箱上下文内定义所有工具函数
* 注意:该脚本运行在沙箱 realm 内,其抛出的 Error 也是沙箱 realm 对象,无逃逸风险
@@ -327,7 +350,7 @@ class JsRunner {
}
const startTime = Date.now();
- const deadlineMs = RUN_DEADLINE;
+ const deadlineMs = getRunDeadlineMs();
const collectedStats = [];
// 唯一跨域桥接函数:AI 代码中的每个工具调用都通过它回到主进程执行。
@@ -544,4 +567,4 @@ class JsRunner {
}
}
-module.exports = { JsRunner };
+module.exports = { JsRunner, getRunDeadlineMs, RUN_DEADLINE_DEFAULT };