diff --git a/CHANGELOG.md b/CHANGELOG.md index c969c78..4888db7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,13 +1,25 @@ # Changelog -## [Unreleased] +## [4.0.24] - 2026-09-16 ### Added -- _Нет незарелиженных изменений — всё ниже разнесено по версиям._ + +- **Цветные иконки файлов (Material Icon Theme)** — рядом с именами файлов теперь рисуется иконка по типу файла (расширению/имени): + - в чипах путей внутри ответов AI; + - в блоке «Затронуто» под ответом; + - в меню @-упоминаний файлов. + Иконки берутся из пакета `material-icon-theme` (~1377 расширений, ~2135 спец-имён) через IPC `get-file-icons`; неизвестные типы показывают контурный документ. +- **Регистронезависимый @-поиск файлов** — `@lua` находит `Sample.LUA` и наоборот (в ripgrep для `@`-поиска используется `--iglob` вместо регистрозависимого `--glob`). + +### Changed + +- **Меню slash-команд `/`** оставлено без иконок — иконки показываются только в списке файлов (`@`). +- Список файлов в меню `@` выровнен по левому краю с иконкой 16×16 (папка — SVG, файл — Material-иконка). ## [4.0.20] - 2026-09-16 ### Added + - **Telegram-бот: команды управления** — теперь из Telegram доступны: - `/status` — статус окна, проекта, процессов, задач и polling бота - `/stop` (синоним `/kill`) — прервать текущую задачу и убить активные дочерние процессы @@ -33,6 +45,7 @@ ## [4.0.19] - 2026-09-15 ### Added + - **Учёт токенов диалога**: панель «Токены диалога» с оценкой всего диалога из DOM (CJK 0.6 / ASCII 0.3), сохранение по `sessionId` в localStorage, обновление раз в секунду, на `popstate`/`hashchange` и `beforeunload`; формат K/M вместо китайского «万» - **Токены под ответом AI**: оценка токенов именно этого ответа под каждым сообщением - **Перехват серверных токенов DeepSeek** (`token-interceptor`): точные значения usage из ответов DeepSeek + настройка видимости @@ -43,6 +56,7 @@ - **Диагностика интеграции** (`src/preload/dom/diagnostics.js`) — инструмент и UI проверки связки main/preload/сайт ### Changed + - **Удалён Claude-провайдер** (`src/providers/claude.js`, промпт, лого, тесты) - Удалён мёртвый код network-hook (`src/interceptor/`, `intercept-observer.js`, `serverTokenUsage`, `useIntercept`) - Обновлён UI вкладки настроек (`settings-tab.js`) @@ -51,6 +65,7 @@ ## [4.0.18] - 2026-09-14 ### Added + - **Авто-форматтеры** после `write`/`edit`: файл автоматически приводится к стилю проекта - Встроенный реестр: prettier, biome, gofmt, ruff, rustfmt, shfmt, clang-format - Определение по расширению + наличию конфига + бинарю в `PATH` или `node_modules/.bin` @@ -63,277 +78,19 @@ - **Telegram-бот**: команды `/help` и `/settings` с полным меню настроек ### Fixed + - `meta.produced` label: «Создано» → «Затронуто» / Produced → Affected - i18n overlay: `OVERLAY_HTML` → `buildOverlayHTML()` — пересборка при смене языка ## [4.0.17] - 2026-09-14 ### Added + - **File-path chips**: кликабельные чипы путей (абсолютный + относительный через `projectDir`) с открытием в VS Code / проводнике и настройкой видимости - **Produced files** под ответом AI (базовый вариант) + ru/en i18n - Override QR-кода скачивания приложения DeepSeek — свой QR и ссылка на Telegram - `safe()`-обёртка для защиты от изменений DOM DeepSeek ### Fixed -- `meta.produced` label: Создано → Затронуто / Produced → Affected - -## [4.0.16] - 2026-09-14 - -### Added -- Модуль review context для сбора diff -- Команда `summarize` — краткий итог текущей сессии - -### Fixed -- Остановка задачи работает без дочернего процесса - -## [4.0.15] - 2026-09-13 - -### Added -- Стеклянный эффект (glass) для поля ввода DeepSeek - -### Fixed -- Иконка приложения в окне и уведомлениях - -## [4.0.14] - 2026-09-13 - -### Changed -- Доработаны UI подтверждения (approval) и inline-результатов - -## [4.0.13] - 2026-09-13 - -### Changed -- Технический релиз (bump версии) - -## [4.0.12] - 2026-09-13 - -### Added -- Подтверждение вызовов инструментов через Telegram (approve tool calls) - -## [4.0.11] - 2026-09-13 - -### Changed -- Скрытие системных сообщений отключено по умолчанию (`hideSystemMessages: false`) - -## [4.0.10] - 2026-09-13 - -### Added -- **Approval gate**: подтверждение вызовов инструментов и JS-блоков -- Inline-результаты инструментов внутри карточек вызовов -- Скрытие service-сообщений в чате для чистого UI - -### Fixed -- Stealth-режим пропускал service-сообщения в актуальном UI DeepSeek - -## [4.0.9] - 2026-09-13 - -### Added -- Инструмент `read_photo` — вставка изображений в чат -- Ответы на вопросы AI через inline-клавиатуру Telegram - -## [4.0.8] - 2026-09-13 - -### Added -- Цвета темы, кастомные фоны, перетаскиваемый overlay -- Унифицированы кнопки настроек, добавлена секция Service - -## [4.0.7] - 2026-09-12 - -### Added -- Иконки приложения + папка кастомных фонов - -## [4.0.6] - 2026-09-12 - -### Added -- Принудительная тёмная тема на сайте - -## [4.0.5] - 2026-09-12 - -### Added -- Интерактивный инструмент вопросов пользователю (user question tool) -- Глобальный тумблер кастомизации - -## [4.0.3] - 2026-09-12 - -### Changed -- Язык по умолчанию берётся из системной локали - -## [4.0.2] - 2026-09-12 -### Changed -- CI: bump версии от max(package.json, последний тег) — защита от коллизий тегов -- CI: исправлен парсинг YAML (`build.yml`), релизный пайплайн снова включён -- Синхронизированы CI-воркфлоу, упрощён `release.bat`, скриншоты в README - -### Fixed -- Восстановлен оригинальный скриншот Telegram-бота - -## [4.0.1] - 2026-09-12 - -### Changed -- Лицензия переключена на MIT, новые скриншоты Telegram-бота - -## [4.0.0] - 2026-09-12 - -### Added -- **Панель Todo**: список задач, store и инструменты; фикс всплытия панели/таба при неизменном списке -- **Панель «Изменения»** с git diff + вкладка «История» для просмотра прошлых коммитов -- **@-file mentions**: автодополнение файлов проекта при вводе `@`; результаты-директории в file picker -- **Telegram**: команда `/todos` и уведомление о завершении задач -- i18n overlay: локализованы строки ручного парсинга и выполнения инструментов -- Updater: UI переведён на английский, апдейты указывают на merfiDEV/Cookie-code - -### Changed -- Лицензия переключена на MIT; `release.bat` адаптирован под origin -- Удалены `LICENSE` и `HANDOFF_MCP.md` - -## [0.3.6] - 2026-09-08 - -### Fixed -- 添加标准编辑菜单,修复 macOS 无法在 DeepSeek 输入框复制/粘贴的问题 - -## [0.3.5] - 2026-09-08 - -### Fixed -- glob/grep 工具打包后 ripgrep ENOENT,通过 asarUnpack 解包二进制并修正运行路径 -- 恢复 README 中 Codecov 覆盖率徽章 - -## [0.3.4] - 2026-09-07 - -### Fixed -- pwsh 工具改用 execFile,避免管道符被 cmd 预解析导致命令残缺 -- 主窗口 UA 改为 Chrome 130 普通标识,避免 DeepSeek 提示隐私风险 -- openBrowserWindow 工具窗口同步设置 Chrome 130 UA,不再暴露 Electron 标识 -- XML invoke 检测不再要求必须位于文本开头,并避免提示语自触发循环 -- 渲染进程可通过 --cuckoo-user-data 参数加载自定义 Provider - -### Added -- Provider 发送扩展接口(provider.triggerSend),支持站点原生发送 -- 流式稳定性双通道校验(mutation 快照 + interval 兜底) -- 完成检测兜底轮询(每 2s 主动复查),覆盖后台/最小化漏触发场景 -- MCP 工具调用识别(await mcpXxx / log(await xxx)) -- 无 pre 的 .md-code 代码容器兜底 - -### Changed -- 会话 ID 提取与跳转 URL 改为 Provider 方法,不再硬编码 DeepSeek 格式 - -## [0.3.0] - 2026-09-07 - -### Added -- 多平台 Provider 框架:支持 DeepSeek 与 Claude,按平台区分输入框/发送按钮/用户信息/消息解析 -- 平台选择页:新窗口未指定平台时展示卡片式选择页(含官方品牌 logo) -- 提示词模板化:每平台独立模板,支持 `{{PLATFORM_INFO}}` `{{TOOL_API_TYPES}}` `{{TOOLS_LIST}}` `{{TOOL_SECTIONS}}` `{{PROJECT_DIR}}` `{{PROJECT_INTRO_SECTION}}` `{{MCP_SECTION}}` 占位符 -- 自定义 Provider 功能:用户可通过平台选择页导入 JS 文件,支持复制到 userData、重名替换、删除前窗口占用检查 -- 自定义 Provider 类型声明与模板:`src/providers/custom/provider.d.ts` + `provider.template.js` -- Provider 方法化:平台差异全部下沉到 Provider 方法(findInput/findSendButton/isResponseComplete/getMessageCandidates 等) -- Claude 自动解析:基于停止按钮边沿触发完成检测,跳过空消息,避免重复触发 -- 按平台分文件记录渲染日志到 `wyp/log/{providerId}.log` -- 新增 Provider 单元测试 - -### Changed -- 系统提示词由单文件改为多平台模板,运行时按 providerId 选择并替换占位符 -- 工具 API 类型定义从 `tools/cuckoo-tools.d.ts` 动态读取,避免多处维护 -- 平台 logo 从首字母占位改为官方 SVG - -### Fixed -- Claude 自动解析重复触发/漏触发问题 -- 多个 `{{PROJECT_DIR}}` 占位符只替换第一个的问题 -- 导入自定义 Provider 后源文件被删导致失效的问题 - ---- - -## [0.2.5-beta.1] - 2026-09-02 - -### Added -- 新增 MCP(Model Context Protocol)按需查询能力 - - mcpListServers() 查看已配置的 MCP server(名称/类型/状态/工具数/工具名) - - mcpGetTools(serverName) 查看指定 server 的工具详情(描述+参数) - - mcpCall(server, tool, args) 调用 MCP 工具 -- MCP 启动时自动连接已启用的 server,初始化项目时等待连接(8 秒超时) -- MCP 配置保存前完整 JSON 结构校验(错误时明确提示且不覆盖输入) -- MCP 配置保存后弹确认框,由用户决定是否通知 AI(避免打断 AI 操作) -- showConfirmDialog 扩展支持确认/取消双按钮 + Promise 返回(向后兼容) -- 新增 McpCallTool、McpQueryTools 单元测试 - -### Changed -- MCP 提示词改为按需查看模式,不再全量注入工具列表(节省 token) -- MCP 保存后的自动通知改为简短格式,引导 AI 按需查询 -- 移除 .cuckooCode/CUCKOO.md 作为全局项目介绍(避免干扰其他项目场景) - -### Fixed -- 修复 connectEnabledServers 从未被调用导致 MCP 启动后未连接的问题 -- 修复 initProject 同步逻辑中 MCP 连接时序竞态(改为 async + 等待) - ---- - -## [0.2.0] - 2026-08-27 - -### Breaking Changes -- 主进程重构:main.js 拆分为 src/main/ 模块(应用生命周期、IPC、项目上下文、会话存储分离) -- preload 重构:preload.js 拆分为 src/preload/ 模块(DOM 监测、overlay UI、工具解析分离) -- 移除 MySQLTool.js(已无维护,提示词中不再暴露) -- 移除旧版工具桥接实现(ToolBridge.js、UnifiedToolManager.js、WebContentCapturer.js) -- bash/pwsh 不再强制要求 description 参数(改为可选) - -### Added -- 新增 5 个对标 dsh 的工具实现: - - read(offset/limit 分段读取,替代 readFile) - - write(全量覆盖,替代 writeFile) - - edit(精确替换,替代 editFile) - - glob(ripgrep 引擎,替代旧 GlobTool) - - grep(ripgrep JSON 输出,替代旧 GrepTool) -- 新增 todoWrite 工具(全量任务列表,对标 dsh todo_write) -- 新增 pwsh 工具(PowerShell 执行,对标 dsh tool-pwsh) -- 新增 webFetch 工具(HTML 转 Markdown,对标 dsh web_fetch) -- 新增 openBrowserWindow / injectJS 工具(打开调试窗口并注入 JS) -- 工具系统支持 section 机制(对标 dsh systemPrompt.section),每个工具有独立的 prompt section -- 系统提示词全面改版:中文 dsh 风格 + 动态平台检测(Windows/macOS/Linux) -- 发送延迟可配置(UI 设置 + localStorage 持久化) -- UI 全面改版:悬浮球模式(48px 右下角 FAB + 小面板),不再占用全高侧边栏 -- 跨平台启动脚本 start.js(Windows/macOS/Linux 通用) -- 完整单元测试覆盖(tools / main / preload) - -### Changed -- package.json start 脚本改为 node start.js(修复 macOS chcp: command not found) -- start.js 自动根据平台选择 UTF-8 编码设置 -- 工具返回格式统一为 dsh 风格(纯文本 envelope / marker) -- 错误提示统一为英文 dsh 风格 -- package.json 增加 allowScripts 配置(npm 新版本 electron postinstall 被阻止问题) - -### Fixed -- edit 工具 CRLF 适配(LF 的 old_string 能匹配 CRLF 文件) -- bash/pwsh 非零退出不再报错,改用 [exit code: N] 标记 -- glob 路径去掉 ./ 前缀 -- Windows 平台信息动态生成(不再硬编码) -- Electron postinstall 被 npm allowScripts 阻止导致二进制缺失的问题 -- JS 脚本无输出时提示使用 log() - -### Security -- bash/pwsh 危险命令黑名单保留(Windows + PowerShell 特有命令) -- 命令执行超时限制保持 30 秒 -- 输出缓冲 1MB 限制保持 - -### Dependencies -- 新增 @vscode/ripgrep(glob/grep 底层引擎) -- 新增 turndown + @joplin/turndown-plugin-gfm(webFetch HTML 转 Markdown) - ---- - -## [1.0.0] - 2026-08-11 - -### Added -- 初始版本发布 -- Electron 桌面应用,嵌入 DeepSeek 网页版 -- 自动检测 AI 回复中的 cmd/powershell 代码块,弹窗确认后执行 -- 工具调用系统:支持 file_write、file_read、file_edit、file_glob、file_grep、bash、file_delete 等工具 -- 会话级项目目录绑定,工具操作以项目目录为基础 -- 侧边覆盖层面板,显示命令预览、执行结果和历史记录 -- Ctrl+Shift+C 快捷键切换覆盖层 -- 项目初始化功能,自动生成目录树并注入系统提示词 -- 支持危险命令检测与额外警告 -- 会话持久化(cookies/localStorage 保存到 %APPDATA%/cuckoo-code-session) - -### Security -- 命令执行前必须用户确认 -- 危险命令(rm -rf /、format、shutdown 等)触发额外警告 -- 30 秒命令执行超时限制 -- 1MB 命令输出缓冲区限制 +- `meta.produced` label: Создано → Затронуто / Produced → Affected diff --git a/package-lock.json b/package-lock.json index 211c516..9764803 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,6 +20,7 @@ "docx": "^9.7.1", "electron-log": "^5.4.4", "electron-updater": "^6.8.9", + "material-icon-theme": "^5.38.1", "mysql2": "^3.24.2", "turndown": "^7.2.4" }, @@ -1578,6 +1579,12 @@ "node": ">=10" } }, + "node_modules/chroma-js": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/chroma-js/-/chroma-js-3.2.0.tgz", + "integrity": "sha512-os/OippSlX1RlWWr+QDPcGUZs0uoqr32urfxESG9U93lhUfbnlyckte84Q8P1UQY/qth983AS1JONKmLS4T0nw==", + "license": "(BSD-3-Clause AND Apache-2.0)" + }, "node_modules/chromium-pickle-js": { "version": "0.2.0", "resolved": "https://registry.npmmirror.com/chromium-pickle-js/-/chromium-pickle-js-0.2.0.tgz", @@ -3735,6 +3742,21 @@ "node": ">=10" } }, + "node_modules/material-icon-theme": { + "version": "5.38.1", + "resolved": "https://registry.npmjs.org/material-icon-theme/-/material-icon-theme-5.38.1.tgz", + "integrity": "sha512-14cFM4NJGdbuo68rIZTq9TSX0f5BtA6VF+eNX3zq23Z5NoEeVtM4Zn4PpZ0ERl++50jCBrywKFWXmOjbxf0xTA==", + "license": "MIT", + "dependencies": { + "chroma-js": "^3.2.0" + }, + "engines": { + "vscode": "^1.55.0" + }, + "funding": { + "url": "https://github.com/sponsors/material-extensions" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz", diff --git a/package.json b/package.json index b29de3b..8be694a 100644 --- a/package.json +++ b/package.json @@ -134,6 +134,7 @@ "docx": "^9.7.1", "electron-log": "^5.4.4", "electron-updater": "^6.8.9", + "material-icon-theme": "^5.38.1", "mysql2": "^3.24.2", "turndown": "^7.2.4" }, diff --git a/src/main/ipc.js b/src/main/ipc.js index 883f480..6782d7c 100644 --- a/src/main/ipc.js +++ b/src/main/ipc.js @@ -2,21 +2,21 @@ * IPC 处理器注册(渲染进程 → 主进程) * 多窗口版:按 event.sender 路由到对应窗口的 profile 上下文。 */ -const { app, dialog, ipcMain, Notification, shell } = require('electron'); -const { exec } = require('child_process'); -const path = require('path'); - -const windowState = require('./window'); -const profileManager = require('./profile-manager'); -const { toolRegistry, jsRunner } = require('./tool-registry'); -const { initProject } = require('./project-context'); -const { isDangerous } = require('./dangerous-commands'); -const settingsStore = require('./settings-store'); -const chatExport = require('./chat-export'); -const { decodeOutput, normalizeCommand } = require('../../tools/decodeOutput'); -const gitDiff = require('./git-diff'); -const todoStore = require('./todo-store'); -const planMode = require('./plan-mode'); +const { app, dialog, ipcMain, Notification, shell } = require("electron"); +const { exec } = require("child_process"); +const path = require("path"); + +const windowState = require("./window"); +const profileManager = require("./profile-manager"); +const { toolRegistry, jsRunner } = require("./tool-registry"); +const { initProject } = require("./project-context"); +const { isDangerous } = require("./dangerous-commands"); +const settingsStore = require("./settings-store"); +const chatExport = require("./chat-export"); +const { decodeOutput, normalizeCommand } = require("../../tools/decodeOutput"); +const gitDiff = require("./git-diff"); +const todoStore = require("./todo-store"); +const planMode = require("./plan-mode"); /** * Если у окна все задачи выполнены (и список непустой) — пингуем Telegram один раз. @@ -38,7 +38,8 @@ function isTaskCanceled(senderId, token) { } function finishTask(senderId, token) { - if (activeTaskTokens.get(senderId) === token) activeTaskTokens.delete(senderId); + if (activeTaskTokens.get(senderId) === token) + activeTaskTokens.delete(senderId); } function requestUserQuestion(sender, questions) { @@ -47,12 +48,14 @@ function requestUserQuestion(sender, questions) { return new Promise((resolve, reject) => { pendingUserQuestions.set(key, { resolve, reject }); // Показываем диалог в окне (как раньше). - try { sender.send('ask-user-question', { requestId, questions }); } catch (_) {} + try { + sender.send("ask-user-question", { requestId, questions }); + } catch (_) {} // Дублируем вопрос в Telegram с inline-клавиатурой (если бот включён). // Кто ответит первым — окно или TG — тот и резолвит promise. try { - const bot = require('../../botsrc'); + const bot = require("../../botsrc"); bot.askQuestion(requestId, questions).catch(() => {}); } catch (_) {} }); @@ -64,13 +67,14 @@ function requestUserQuestion(sender, questions) { */ function resolveUserQuestionFromTelegram(requestId, answers) { for (const [key, pending] of pendingUserQuestions) { - if (key.endsWith(':' + requestId)) { + if (key.endsWith(":" + requestId)) { pendingUserQuestions.delete(key); // Просим окно закрыть диалог, если он ещё открыт. try { - const senderId = Number(key.split(':')[0]); - const wc = require('electron').webContents.fromId(senderId); - if (wc && !wc.isDestroyed()) wc.send('ask-user-question-resolved', { requestId }); + const senderId = Number(key.split(":")[0]); + const wc = require("electron").webContents.fromId(senderId); + if (wc && !wc.isDestroyed()) + wc.send("ask-user-question-resolved", { requestId }); } catch (_) {} pending.resolve(Array.isArray(answers) ? answers : []); return true; @@ -90,11 +94,13 @@ let planApprovalCounter = 0; * @returns {Promise<{approved: boolean}>} */ function requestExitPlanMode(sender, plan) { - const requestId = 'plan_' + Date.now() + '_' + (++planApprovalCounter); - const key = sender.id + ':' + requestId; + const requestId = "plan_" + Date.now() + "_" + ++planApprovalCounter; + const key = sender.id + ":" + requestId; return new Promise((resolve, reject) => { pendingPlanApprovals.set(key, { resolve, reject }); - try { sender.send('exit-plan-mode', { requestId, plan: String(plan || '') }); } catch (_) {} + try { + sender.send("exit-plan-mode", { requestId, plan: String(plan || "") }); + } catch (_) {} }); } @@ -105,18 +111,25 @@ function requestExitPlanMode(sender, plan) { */ function sessionIdOf(event) { const ctx = windowState.getContextByWebContents(event.sender); - return ctx && ctx.sessionStore ? (ctx.sessionStore.state.currentSessionId || null) : null; + return ctx && ctx.sessionStore + ? ctx.sessionStore.state.currentSessionId || null + : null; } function maybeNotifyAllDone(senderId) { try { const todos = todoStore.getList(senderId); - if (todos.length === 0) { _todoAllDoneNotified.delete(senderId); return; } - const done = todos.filter((t) => t.status === 'completed').length; + if (todos.length === 0) { + _todoAllDoneNotified.delete(senderId); + return; + } + const done = todos.filter((t) => t.status === "completed").length; if (done === todos.length) { if (_todoAllDoneNotified.has(senderId)) return; _todoAllDoneNotified.add(senderId); - try { require('../../botsrc').notifyAllDone(todos); } catch (_) {} + try { + require("../../botsrc").notifyAllDone(todos); + } catch (_) {} } else { _todoAllDoneNotified.delete(senderId); } @@ -133,8 +146,8 @@ function maybeNotifyAllDone(senderId) { */ async function insertImageToChat(sender, filePath, caption, send) { try { - const { clipboard, nativeImage } = require('electron'); - const fs = require('fs'); + const { clipboard, nativeImage } = require("electron"); + const fs = require("fs"); if (!filePath || !fs.existsSync(filePath)) { return { success: false, error: `Файл не найден: ${filePath}` }; @@ -143,7 +156,10 @@ async function insertImageToChat(sender, filePath, caption, send) { // Загружаем изображение в nativeImage и кладём в clipboard const img = nativeImage.createFromPath(filePath); if (img.isEmpty()) { - return { success: false, error: `Не удалось загрузить изображение: ${filePath}` }; + return { + success: false, + error: `Не удалось загрузить изображение: ${filePath}`, + }; } clipboard.writeImage(img); @@ -173,22 +189,34 @@ async function insertImageToChat(sender, filePath, caption, send) { } // Вставляем изображение из clipboard через Ctrl+V - sender.sendInputEvent({ type: 'keyDown', keyCode: 'V', modifiers: ['control'] }); - sender.sendInputEvent({ type: 'keyUp', keyCode: 'V', modifiers: ['control'] }); + sender.sendInputEvent({ + type: "keyDown", + keyCode: "V", + modifiers: ["control"], + }); + sender.sendInputEvent({ + type: "keyUp", + keyCode: "V", + modifiers: ["control"], + }); // Даём React обработать вставку await new Promise((r) => setTimeout(r, 300)); // Если нужно отправить — жмём Enter if (send) { await new Promise((r) => setTimeout(r, 200)); - sender.sendInputEvent({ type: 'keyDown', keyCode: 'Return', key: 'Enter' }); - sender.sendInputEvent({ type: 'char', keyCode: 'Return', key: '\r' }); - sender.sendInputEvent({ type: 'keyUp', keyCode: 'Return', key: 'Enter' }); + sender.sendInputEvent({ + type: "keyDown", + keyCode: "Return", + key: "Enter", + }); + sender.sendInputEvent({ type: "char", keyCode: "Return", key: "\r" }); + sender.sendInputEvent({ type: "keyUp", keyCode: "Return", key: "Enter" }); } return { success: true }; } catch (err) { - console.error('[Cookie Code] insertImageToChat error:', err.message); + console.error("[Cookie Code] insertImageToChat error:", err.message); return { success: false, error: err.message }; } } @@ -200,17 +228,21 @@ async function insertImageToChat(sender, filePath, caption, send) { */ function tryOpenInVSCode(filePath) { return new Promise((resolve) => { - const { exec } = require('child_process'); + const { exec } = require("child_process"); // Обёртка: code --goto "путь" — откроет файл в VS Code. // На Windows 'code' — это code.cmd, доступный через cmd.exe. const safePath = String(filePath).replace(/"/g, '""'); - const cmd = process.platform === 'win32' - ? `cmd /c code --goto "${safePath}"` - : `code --goto "${safePath}"`; + const cmd = + process.platform === "win32" + ? `cmd /c code --goto "${safePath}"` + : `code --goto "${safePath}"`; exec(cmd, { timeout: 5000 }, (error, _stdout, stderr) => { if (error) { - resolve({ success: false, error: (stderr || error.message || 'code не найден').trim() }); + resolve({ + success: false, + error: (stderr || error.message || "code не найден").trim(), + }); return; } resolve({ success: true }); @@ -221,8 +253,8 @@ function tryOpenInVSCode(filePath) { function registerIpcHandlers() { // Мост Telegram → окно: ответ на вопрос из TG резолвит тот же promise. try { - const bot = require('../../botsrc'); - if (typeof bot.setOnQuestionAnswered === 'function') { + const bot = require("../../botsrc"); + if (typeof bot.setOnQuestionAnswered === "function") { bot.setOnQuestionAnswered((requestId, answers) => { resolveUserQuestionFromTelegram(requestId, answers); }); @@ -230,84 +262,94 @@ function registerIpcHandlers() { } catch (_) {} // Решение пользователя по плану (согласие/отказ из окна). - ipcMain.on('exit-plan-mode-response', (event, { requestId, approved } = {}) => { - if (!requestId) return; - const key = event.sender.id + ':' + requestId; - const pending = pendingPlanApprovals.get(key); - if (!pending) return; - pendingPlanApprovals.delete(key); - if (approved) planMode.clearPlanMode(event.sender.id, sessionIdOf(event)); - pending.resolve({ approved: !!approved }); - }); + ipcMain.on( + "exit-plan-mode-response", + (event, { requestId, approved } = {}) => { + if (!requestId) return; + const key = event.sender.id + ":" + requestId; + const pending = pendingPlanApprovals.get(key); + if (!pending) return; + pendingPlanApprovals.delete(key); + if (approved) planMode.clearPlanMode(event.sender.id, sessionIdOf(event)); + pending.resolve({ approved: !!approved }); + }, + ); // Включение/выключение режима плана для текущей сессии окна. - ipcMain.handle('set-plan-mode', async (event, { enabled } = {}) => { + ipcMain.handle("set-plan-mode", async (event, { enabled } = {}) => { const sessionId = sessionIdOf(event); planMode.setPlanMode(event.sender.id, sessionId, !!enabled); - return { success: true, planMode: planMode.isPlanMode(event.sender.id, sessionId), sessionId }; + return { + success: true, + planMode: planMode.isPlanMode(event.sender.id, sessionId), + sessionId, + }; }); - ipcMain.on('ask-user-question-response', (event, { requestId, answers, canceled } = {}) => { - if (!requestId) return; - const key = `${event.sender.id}:${requestId}`; - const pending = pendingUserQuestions.get(key); - if (!pending) return; - pendingUserQuestions.delete(key); - if (canceled) { - pending.reject(new Error('Пользователь отменил вопрос')); - return; - } - pending.resolve(Array.isArray(answers) ? answers : []); - }); + ipcMain.on( + "ask-user-question-response", + (event, { requestId, answers, canceled } = {}) => { + if (!requestId) return; + const key = `${event.sender.id}:${requestId}`; + const pending = pendingUserQuestions.get(key); + if (!pending) return; + pendingUserQuestions.delete(key); + if (canceled) { + pending.reject(new Error("Пользователь отменил вопрос")); + return; + } + pending.resolve(Array.isArray(answers) ? answers : []); + }, + ); // Whats-new: открыть CHANGELOG.md в системном редакторе (по кнопке в модалке) - ipcMain.handle('whats-new-open-changelog', async () => { + ipcMain.handle("whats-new-open-changelog", async () => { try { - const { shell } = require('electron'); - const path = require('path'); + const { shell } = require("electron"); + const path = require("path"); const candidates = [ - path.join(__dirname, '..', '..', 'CHANGELOG.md'), - path.join(process.resourcesPath || '', 'CHANGELOG.md'), + path.join(__dirname, "..", "..", "CHANGELOG.md"), + path.join(process.resourcesPath || "", "CHANGELOG.md"), ]; for (const p of candidates) { try { - if (require('fs').existsSync(p)) { + if (require("fs").existsSync(p)) { const err = await shell.openPath(p); return err ? { success: false, error: err } : { success: true }; } } catch (_) {} } - return { success: false, error: 'CHANGELOG.md не найден' }; + return { success: false, error: "CHANGELOG.md не найден" }; } catch (err) { return { success: false, error: err.message }; } }); // Открыть файл/папку в системе (из file-chip в ответе AI) - ipcMain.handle('open-path', async (_event, payload) => { + ipcMain.handle("open-path", async (_event, payload) => { const targetPath = payload && payload.path; - if (!targetPath || typeof targetPath !== 'string') { - return { success: false, error: 'Путь не указан' }; + if (!targetPath || typeof targetPath !== "string") { + return { success: false, error: "Путь не указан" }; } try { - const fs = require('fs'); + const fs = require("fs"); if (!fs.existsSync(targetPath)) { - return { success: false, error: 'Файл не найден: ' + targetPath }; + return { success: false, error: "Файл не найден: " + targetPath }; } const ext = path.extname(targetPath).toLowerCase(); - const CODE_EXTS = new Set(['.js', '.ts', '.jsx', '.tsx', '.mjs', '.cjs']); + const CODE_EXTS = new Set([".js", ".ts", ".jsx", ".tsx", ".mjs", ".cjs"]); // Скриптовые файлы Windows ассоциирует с Windows Script Host (WSH), // который пытается их ВЫПОЛНИТЬ и падает с синтаксической ошибкой. // Поэтому для них приоритетно открываем VS Code, при неудаче — проводник. if (CODE_EXTS.has(ext)) { const opened = await tryOpenInVSCode(targetPath); - if (opened.success) return { success: true, openedWith: 'vscode' }; + if (opened.success) return { success: true, openedWith: "vscode" }; // VS Code недоступен — показываем файл в проводнике (безопасно). try { shell.showItemInFolder(targetPath); - return { success: true, openedWith: 'explorer', note: opened.error }; + return { success: true, openedWith: "explorer", note: opened.error }; } catch (err) { return { success: false, error: err.message }; } @@ -325,66 +367,95 @@ function registerIpcHandlers() { }); // 初始化项目 - ipcMain.handle('init-project', async (event, { skipPrompt = false } = {}) => { + ipcMain.handle("init-project", async (event, { skipPrompt = false } = {}) => { const ctx = windowState.getContextByWebContents(event.sender); return initProject(skipPrompt, ctx); }); // 列出会话 - ipcMain.handle('list-sessions', async (event) => { + ipcMain.handle("list-sessions", async (event) => { const ctx = windowState.getContextByWebContents(event.sender); const store = ctx ? ctx.sessionStore : null; if (!store || !store.state.selectedProjectDir) { return { success: true, sessions: [] }; } const all = store.readSessionStore(); - const sessions = Object.keys(all).filter(id => all[id] === store.state.selectedProjectDir); + const sessions = Object.keys(all).filter( + (id) => all[id] === store.state.selectedProjectDir, + ); return { success: true, sessions }; }); // 列出项目文件(用于 @ 文件提及自动补全) // 返回 { success, files: [{ rel, abs }] },rel 为相对 projectDir 的路径,abs 为绝对路径。 - ipcMain.handle('list-project-files', async (event, { query = '' } = {}) => { + ipcMain.handle("list-project-files", async (event, { query = "" } = {}) => { const ctx = windowState.getContextByWebContents(event.sender); const store = ctx ? ctx.sessionStore : null; const projectDir = store ? store.state.selectedProjectDir : null; if (!projectDir) return { success: true, files: [] }; try { - const path = require('path'); - const { buildGlobArgs, runRipgrep } = require('../../tools/GlobToolNew'); - const safeQuery = String(query || '').replace(/[\\*?\[\]]/g, ''); - const pattern = safeQuery ? '**/*' + safeQuery + '*' : '**/*'; - const args = buildGlobArgs({ pattern }); + const path = require("path"); + const { buildGlobArgs, runRipgrep } = require("../../tools/GlobToolNew"); + const safeQuery = String(query || "").replace(/[\\*?\[\]]/g, ""); + const pattern = safeQuery ? "**/*" + safeQuery + "*" : "**/*"; + // @-поиск должен игнорировать регистр: --glob в ripgrep регистрозависим, + // а --iglob — нет. buildGlobArgs не трогаем (glob-инструмент остаётся как был). + const args = buildGlobArgs({ pattern }).map((a) => + a.startsWith("--glob=") ? "--iglob=" + a.slice("--glob=".length) : a, + ); const { stdout } = await runRipgrep(args, projectDir); const files = stdout .split(/\r?\n/) - .map((p) => p.replace(/\\/g, '/').replace(/^\.\//, '')) + .map((p) => p.replace(/\\/g, "/").replace(/^\.\//, "")) .filter((p) => p.length > 0) .map((rel) => ({ rel, abs: path.join(projectDir, rel), isDir: false })); // Дополнительно собираем директории проекта (ripgrep их не отдаёт). - const IGNORE_DIRS = new Set(['node_modules', '.git', '.svn', '.hg', 'dist', 'build', 'out', 'coverage', '.next', '.cache', '.vscode', '.idea']); + const IGNORE_DIRS = new Set([ + "node_modules", + ".git", + ".svn", + ".hg", + "dist", + "build", + "out", + "coverage", + ".next", + ".cache", + ".vscode", + ".idea", + ]); const dirs = []; (function walk(absDir, relDir) { let entries; - try { entries = require('fs').readdirSync(absDir, { withFileTypes: true }); } catch (_) { return; } + try { + entries = require("fs").readdirSync(absDir, { withFileTypes: true }); + } catch (_) { + return; + } for (const e of entries) { if (!e.isDirectory()) continue; if (IGNORE_DIRS.has(e.name)) continue; - const rel = relDir ? relDir + '/' + e.name : e.name; - if (safeQuery && !rel.includes(safeQuery)) { /* всё равно заходим глубже */ } + const rel = relDir ? relDir + "/" + e.name : e.name; + if (safeQuery && !rel.includes(safeQuery)) { + /* всё равно заходим глубже */ + } dirs.push({ rel, abs: path.join(projectDir, rel), isDir: true }); walk(path.join(absDir, e.name), rel); } - })(projectDir, ''); + })(projectDir, ""); const q = safeQuery.toLowerCase(); const matchDir = (d) => !q || d.rel.toLowerCase().includes(q); const matchFile = (f) => !q || f.rel.toLowerCase().includes(q); - const dirList = dirs.filter(matchDir).sort((a, b) => a.rel.localeCompare(b.rel)); - const fileList = files.filter(matchFile).sort((a, b) => a.rel.localeCompare(b.rel)); + const dirList = dirs + .filter(matchDir) + .sort((a, b) => a.rel.localeCompare(b.rel)); + const fileList = files + .filter(matchFile) + .sort((a, b) => a.rel.localeCompare(b.rel)); const merged = dirList.concat(fileList).slice(0, 100); return { success: true, files: merged }; @@ -394,21 +465,28 @@ function registerIpcHandlers() { }); // 导航到会话 - ipcMain.handle('navigate-session', async (event, { sessionId }) => { - if (!sessionId) return { success: false, error: '缺少会话ID' }; + ipcMain.handle("navigate-session", async (event, { sessionId }) => { + if (!sessionId) return { success: false, error: "缺少会话ID" }; const ctx = windowState.getContextByWebContents(event.sender); const win = ctx ? ctx.win : null; - if (!win || win.isDestroyed()) return { success: false, error: '窗口已关闭' }; + if (!win || win.isDestroyed()) + return { success: false, error: "窗口已关闭" }; // 按当前 provider 拼会话 URL(DeepSeek /chat/s/ 等) let url = null; try { - const { getProviderByUrl } = require('../providers'); + const { getProviderByUrl } = require("../providers"); const provider = getProviderByUrl(win.webContents.getURL()); - if (provider && typeof provider.sessionUrlBase === 'string' && provider.sessionUrlBase) { + if ( + provider && + typeof provider.sessionUrlBase === "string" && + provider.sessionUrlBase + ) { url = provider.sessionUrlBase + sessionId; } - } catch (_) { /* 回退 DeepSeek */ } - if (!url) url = 'https://chat.deepseek.com/a/chat/s/' + sessionId; + } catch (_) { + /* 回退 DeepSeek */ + } + if (!url) url = "https://chat.deepseek.com/a/chat/s/" + sessionId; try { await win.webContents.loadURL(url); return { success: true }; @@ -418,17 +496,18 @@ function registerIpcHandlers() { }); // Открыть новый чат (переход на домашнюю страницу провайдера). - ipcMain.handle('new-chat', async (event) => { + ipcMain.handle("new-chat", async (event) => { const ctx = windowState.getContextByWebContents(event.sender); const win = ctx ? ctx.win : null; - if (!win || win.isDestroyed()) return { success: false, error: '窗口已关闭' }; + if (!win || win.isDestroyed()) + return { success: false, error: "窗口已关闭" }; let url = null; try { - const { getProviderByUrl } = require('../providers'); + const { getProviderByUrl } = require("../providers"); const provider = getProviderByUrl(win.webContents.getURL()); if (provider && provider.homeUrl) url = provider.homeUrl; } catch (_) {} - if (!url) url = 'https://chat.deepseek.com/'; + if (!url) url = "https://chat.deepseek.com/"; try { await win.webContents.loadURL(url); return { success: true }; @@ -438,40 +517,42 @@ function registerIpcHandlers() { }); // 执行命令 - ipcMain.handle('execute-command', async (event, { command, id }) => { - if (!command || typeof command !== 'string') { - return { id, success: false, error: '无效的命令' }; + ipcMain.handle("execute-command", async (event, { command, id }) => { + if (!command || typeof command !== "string") { + return { id, success: false, error: "无效的命令" }; } const trimmed = normalizeCommand(command.trim()); - if (!trimmed) return { id, success: false, error: '命令为空' }; + if (!trimmed) return { id, success: false, error: "命令为空" }; const ctx = windowState.getContextByWebContents(event.sender); const win = ctx ? ctx.win : windowState.getMainWindow(); const store = ctx ? ctx.sessionStore : null; const selectedDir = store ? store.state.selectedProjectDir : null; - const dangerWarning = isDangerous(trimmed) ? '\n\n⚠️ 警告:此命令可能存在风险,请谨慎确认!' : ''; + const dangerWarning = isDangerous(trimmed) + ? "\n\n⚠️ 警告:此命令可能存在风险,请谨慎确认!" + : ""; const result = await dialog.showMessageBox(win, { - type: isDangerous(trimmed) ? 'warning' : 'question', - buttons: ['取消', '确认执行'], + type: isDangerous(trimmed) ? "warning" : "question", + buttons: ["取消", "确认执行"], defaultId: 0, cancelId: 0, - title: '确认执行命令', - message: '将执行以下命令:', + title: "确认执行命令", + message: "将执行以下命令:", detail: trimmed + dangerWarning, }); if (result.response !== 1) { - return { id, success: false, error: '用户取消了执行', canceled: true }; + return { id, success: false, error: "用户取消了执行", canceled: true }; } return new Promise((resolve) => { - const { processManager } = require('./process-manager'); + const { processManager } = require("./process-manager"); const child = exec( trimmed, { - cwd: selectedDir || process.env.USERPROFILE || app.getPath('home'), + cwd: selectedDir || process.env.USERPROFILE || app.getPath("home"), timeout: 30000, maxBuffer: 1024 * 1024, - encoding: 'buffer', + encoding: "buffer", }, (error, stdout, stderr) => { const wasKilledByUser = processManager.wasKilled(child.pid); @@ -481,106 +562,160 @@ function registerIpcHandlers() { success: !error && !wasKilledByUser, stdout: decodeOutput(stdout), stderr: decodeOutput(stderr), - error: wasKilledByUser ? '执行已被用户停止' : (error ? error.message : null), + error: wasKilledByUser + ? "执行已被用户停止" + : error + ? error.message + : null, canceled: wasKilledByUser, }); - } + }, ); processManager.track(child); }); }); // 执行工具 - ipcMain.handle('execute-tool', async (event, { toolName, params, callId }) => { - const ctx = windowState.getContextByWebContents(event.sender); - const store = ctx ? ctx.sessionStore : null; - const selectedDir = store ? store.state.selectedProjectDir : null; - - // Режим плана: блокируем изменяющие инструменты (кроме записи plan.md). - if (planMode.isPlanMode(event.sender.id, sessionIdOf(event))) { - const verdict = planMode.checkBlocked(toolName, params || {}); - if (verdict.blocked) { - return { callId, success: false, error: verdict.error }; + ipcMain.handle( + "execute-tool", + async (event, { toolName, params, callId }) => { + const ctx = windowState.getContextByWebContents(event.sender); + const store = ctx ? ctx.sessionStore : null; + const selectedDir = store ? store.state.selectedProjectDir : null; + + // Режим плана: блокируем изменяющие инструменты (кроме записи plan.md). + if (planMode.isPlanMode(event.sender.id, sessionIdOf(event))) { + const verdict = planMode.checkBlocked(toolName, params || {}); + if (verdict.blocked) { + return { callId, success: false, error: verdict.error }; + } } - } - const taskToken = beginTask(event.sender.id); - try { - const result = await toolRegistry.execute(toolName, { - ...params, - projectDir: selectedDir, - senderId: event.sender.id, - askUserQuestion: (questions) => requestUserQuestion(event.sender, questions), - pasteImage: (filePath, caption, send) => insertImageToChat(event.sender, filePath, caption, send), - exitPlanMode: (plan) => requestExitPlanMode(event.sender, plan), - }); - if (isTaskCanceled(event.sender.id, taskToken)) { - return { callId, success: false, canceled: true, error: '执行已被用户停止' }; - } - // Если менялся todo-список — пушим обновление в окно - if (toolName === 'todo_write' || toolName === 'todo_edit' || toolName === 'todo_delete') { - try { event.sender.send('todo-updated', { todos: todoStore.getList(event.sender.id) }); } catch (_) {} - maybeNotifyAllDone(event.sender.id); - } - // Уведомление в Telegram (не блокирует ответ). + const taskToken = beginTask(event.sender.id); try { - const preview = result.success - ? (typeof result.data === 'string' ? result.data : '') - : (result.error || ''); - const exitMatch = String(preview).match(/\[exit code:\s*(-?\d+)\]/); - const hasBadExit = !!(exitMatch && exitMatch[1] !== '0'); - const ok = !!result.success && !hasBadExit; - require('../../botsrc').notifyToolResult(toolName, ok, { args: params, preview }); - } catch (_) {} - return { callId, success: result.success, data: result.data, error: result.error }; - } catch (err) { - try { - require('../../botsrc').notifyToolResult(toolName, false, err.message); - } catch (_) {} - return { callId, success: false, error: err.message }; - } finally { - finishTask(event.sender.id, taskToken); - } - }); + const result = await toolRegistry.execute(toolName, { + ...params, + projectDir: selectedDir, + senderId: event.sender.id, + askUserQuestion: (questions) => + requestUserQuestion(event.sender, questions), + pasteImage: (filePath, caption, send) => + insertImageToChat(event.sender, filePath, caption, send), + exitPlanMode: (plan) => requestExitPlanMode(event.sender, plan), + }); + if (isTaskCanceled(event.sender.id, taskToken)) { + return { + callId, + success: false, + canceled: true, + error: "执行已被用户停止", + }; + } + // Если менялся todo-список — пушим обновление в окно + if ( + toolName === "todo_write" || + toolName === "todo_edit" || + toolName === "todo_delete" + ) { + try { + event.sender.send("todo-updated", { + todos: todoStore.getList(event.sender.id), + }); + } catch (_) {} + maybeNotifyAllDone(event.sender.id); + } + // Уведомление в Telegram (не блокирует ответ). + try { + const preview = result.success + ? typeof result.data === "string" + ? result.data + : "" + : result.error || ""; + const exitMatch = String(preview).match(/\[exit code:\s*(-?\d+)\]/); + const hasBadExit = !!(exitMatch && exitMatch[1] !== "0"); + const ok = !!result.success && !hasBadExit; + require("../../botsrc").notifyToolResult(toolName, ok, { + args: params, + preview, + }); + } catch (_) {} + return { + callId, + success: result.success, + data: result.data, + error: result.error, + }; + } catch (err) { + try { + require("../../botsrc").notifyToolResult( + toolName, + false, + err.message, + ); + } catch (_) {} + return { callId, success: false, error: err.message }; + } finally { + finishTask(event.sender.id, taskToken); + } + }, + ); // ========== Вставка изображения в поле чата (read_photo) ========== // IPC-хэндлер, вызываемый рендер-процессом (для будущего использования из overlay). - ipcMain.handle('chat-insert-image', async (event, { filePath, caption, send } = {}) => { - return insertImageToChat(event.sender, filePath, String(caption || ''), send !== false); - }); + ipcMain.handle( + "chat-insert-image", + async (event, { filePath, caption, send } = {}) => { + return insertImageToChat( + event.sender, + filePath, + String(caption || ""), + send !== false, + ); + }, + ); // AI 回复完成时:窗口已聚焦则不打扰;否则弹通知并让任务栏/Dock 闪烁 - ipcMain.handle('show-ai-notification', async (event) => { + ipcMain.handle("show-ai-notification", async (event) => { try { const ctx = windowState.getContextByWebContents(event.sender); const win = ctx ? ctx.win : windowState.getMainWindow(); if (win && !win.isDestroyed() && win.isFocused()) { // 用户正在查看该窗口,不弹通知、不闪烁 - return { success: true, skipped: true, reason: 'window-focused' }; + return { success: true, skipped: true, reason: "window-focused" }; } if (win && !win.isDestroyed()) { - let windowName = 'Cookie Code'; + let windowName = "Cookie Code"; if (ctx && ctx.profileId) { const profile = profileManager.getProfileById(ctx.profileId); if (profile && profile.name) windowName = profile.name; } - const lang = settingsStore.getSetting('language') === 'en' ? 'en' : 'ru'; + const lang = + settingsStore.getSetting("language") === "en" ? "en" : "ru"; const notifText = { - ru: { title: 'AI завершил задачу', body: 'AI завершил ответ' }, - en: { title: 'AI task completed', body: 'AI has finished responding' }, + ru: { title: "AI завершил задачу", body: "AI завершил ответ" }, + en: { + title: "AI task completed", + body: "AI has finished responding", + }, }[lang]; const notification = new Notification({ - title: windowName + ' - ' + notifText.title, + title: windowName + " - " + notifText.title, body: notifText.body, - icon: require('path').join(__dirname, '..', '..', 'build', process.platform === 'win32' ? 'icon.ico' : 'icon.png'), + icon: require("path").join( + __dirname, + "..", + "..", + "build", + process.platform === "win32" ? "icon.ico" : "icon.png", + ), }); // Клик по уведомлению — сфокусировать окно (восстановить из свёрнутого/трея). - notification.on('click', () => { + notification.on("click", () => { try { if (!win || win.isDestroyed()) return; if (win.isMinimized()) win.restore(); @@ -588,14 +723,17 @@ function registerIpcHandlers() { win.focus(); win.flashFrame(false); } catch (err) { - console.error('[Cookie Code] notification click focus error:', err.message); + console.error( + "[Cookie Code] notification click focus error:", + err.message, + ); } }); notification.show(); win.flashFrame(true); - win.once('focus', () => { + win.once("focus", () => { if (!win.isDestroyed()) win.flashFrame(false); }); } @@ -608,16 +746,23 @@ function registerIpcHandlers() { // Остановить текущую задачу окна и активные дочерние процессы. // AI-ответ может выполняться без child process, поэтому одного killAll() недостаточно. - ipcMain.handle('kill-process', async (event) => { + ipcMain.handle("kill-process", async (event) => { try { const taskToken = activeTaskTokens.get(event.sender.id); if (taskToken) taskToken.canceled = true; - const { processManager } = require('./process-manager'); + const { processManager } = require("./process-manager"); const result = await processManager.killAll(); const ctx = windowState.getContextByWebContents(event.sender); const win = ctx && ctx.win; - if (win && !win.isDestroyed() && win.webContents && !win.webContents.isDestroyed()) { - try { win.webContents.stop(); } catch (_) {} + if ( + win && + !win.isDestroyed() && + win.webContents && + !win.webContents.isDestroyed() + ) { + try { + win.webContents.stop(); + } catch (_) {} } return { success: true, stopped: true, count: result.count }; } catch (err) { @@ -626,9 +771,9 @@ function registerIpcHandlers() { }); // 执行 JS 脚本 - ipcMain.handle('execute-js', async (event, { code, callId }) => { - if (!code || typeof code !== 'string') { - return { callId, success: false, error: '无效的 JS 代码' }; + ipcMain.handle("execute-js", async (event, { code, callId }) => { + if (!code || typeof code !== "string") { + return { callId, success: false, error: "无效的 JS 代码" }; } const ctx = windowState.getContextByWebContents(event.sender); const store = ctx ? ctx.sessionStore : null; @@ -641,16 +786,26 @@ function registerIpcHandlers() { selectedDir, event.sender.id, (questions) => requestUserQuestion(event.sender, questions), - (filePath, caption, send) => insertImageToChat(event.sender, filePath, caption, send), + (filePath, caption, send) => + insertImageToChat(event.sender, filePath, caption, send), (plan) => requestExitPlanMode(event.sender, plan), - sessionIdOf(event) + sessionIdOf(event), ); if (isTaskCanceled(event.sender.id, taskToken)) { - return { callId, success: false, canceled: true, error: '执行已被用户停止' }; + return { + callId, + success: false, + canceled: true, + error: "执行已被用户停止", + }; } const after = JSON.stringify(todoStore.getList(event.sender.id)); if (before !== after) { - try { event.sender.send('todo-updated', { todos: todoStore.getList(event.sender.id) }); } catch (_) {} + try { + event.sender.send("todo-updated", { + todos: todoStore.getList(event.sender.id), + }); + } catch (_) {} maybeNotifyAllDone(event.sender.id); } return { callId, ...result }; @@ -662,39 +817,39 @@ function registerIpcHandlers() { }); // 站点原生发送:向聚焦输入框注入真实级 Enter(智谱只响应 isTrusted=true 的输入,合成事件免疫) - ipcMain.handle('chat-send-enter', async (event) => { + ipcMain.handle("chat-send-enter", async (event) => { const sender = event.sender; if (!sender || sender.isDestroyed()) return false; try { - sender.sendInputEvent({ type: 'keyDown', keyCode: 'Enter' }); - sender.sendInputEvent({ type: 'char', keyCode: 'Enter', key: '\r' }); - sender.sendInputEvent({ type: 'keyUp', keyCode: 'Enter' }); - console.log('[Cookie Code] native Enter отправлен в окно'); + sender.sendInputEvent({ type: "keyDown", keyCode: "Enter" }); + sender.sendInputEvent({ type: "char", keyCode: "Enter", key: "\r" }); + sender.sendInputEvent({ type: "keyUp", keyCode: "Enter" }); + console.log("[Cookie Code] native Enter отправлен в окно"); return true; } catch (err) { - console.error('[Cookie Code] ❌ 原生 Enter 发送失败:', err.message); + console.error("[Cookie Code] ❌ 原生 Enter 发送失败:", err.message); return false; } }); // ========== Cookie Code 用户设置 (settings.json) ========== - ipcMain.handle('cuckoo-settings-get-all', async () => { + ipcMain.handle("cuckoo-settings-get-all", async () => { return settingsStore.readSettings(); }); - ipcMain.handle('cuckoo-settings-set', async (_event, { key, value }) => { - if (!key || typeof key !== 'string') { - return { success: false, error: '无效的 key' }; + ipcMain.handle("cuckoo-settings-set", async (_event, { key, value }) => { + if (!key || typeof key !== "string") { + return { success: false, error: "无效的 key" }; } const result = settingsStore.setSetting(key, value); - if (!result) return { success: false, error: '写入失败' }; + if (!result) return { success: false, error: "写入失败" }; return { success: true, settings: result }; }); // ========== Telegram-бот (botsrc/) ========== - ipcMain.handle('telegram-apply', async () => { + ipcMain.handle("telegram-apply", async () => { try { - const bot = require('../../botsrc'); + const bot = require("../../botsrc"); const status = await bot.applySettings(); return { success: true, status }; } catch (err) { @@ -702,27 +857,27 @@ function registerIpcHandlers() { } }); - ipcMain.handle('telegram-status', async () => { + ipcMain.handle("telegram-status", async () => { try { - const bot = require('../../botsrc'); + const bot = require("../../botsrc"); return { success: true, status: bot.getStatus() }; } catch (err) { return { success: false, error: err.message }; } }); - ipcMain.handle('telegram-ping', async () => { + ipcMain.handle("telegram-ping", async () => { try { - const bot = require('../../botsrc'); + const bot = require("../../botsrc"); return await bot.ping(); } catch (err) { return { success: false, error: err.message }; } }); - ipcMain.handle('telegram-test', async () => { + ipcMain.handle("telegram-test", async () => { try { - const bot = require('../../botsrc'); + const bot = require("../../botsrc"); return await bot.testSend(); } catch (err) { return { success: false, error: err.message }; @@ -730,48 +885,62 @@ function registerIpcHandlers() { }); // Ответ AI → в Telegram (для просмотра с телефона). - ipcMain.handle('telegram-notify-ai', async (_event, { text } = {}) => { + ipcMain.handle("telegram-notify-ai", async (_event, { text } = {}) => { try { - const bot = require('../../botsrc'); + const bot = require("../../botsrc"); return await bot.notifyAIResponse(text); } catch (err) { return { success: false, error: err.message }; } }); - ipcMain.handle('telegram-approval-request', async (_event, { requestId, info } = {}) => { - try { - const bot = require('../../botsrc'); - return await bot.requestApproval(requestId, info || {}); - } catch (err) { - return { success: false, error: err.message }; - } - }); + ipcMain.handle( + "telegram-approval-request", + async (_event, { requestId, info } = {}) => { + try { + const bot = require("../../botsrc"); + return await bot.requestApproval(requestId, info || {}); + } catch (err) { + return { success: false, error: err.message }; + } + }, + ); - ipcMain.handle('telegram-approval-cancel', async (_event, { requestId } = {}) => { - try { - const bot = require('../../botsrc'); - return bot.cancelApproval(requestId); - } catch (err) { - return { success: false, error: err.message }; - } - }); + ipcMain.handle( + "telegram-approval-cancel", + async (_event, { requestId } = {}) => { + try { + const bot = require("../../botsrc"); + return bot.cancelApproval(requestId); + } catch (err) { + return { success: false, error: err.message }; + } + }, + ); // ========== Экспорт ответа AI в PDF / DOCX ========== - ipcMain.handle('cuckoo-chat-export', async (_event, payload) => { + ipcMain.handle("cuckoo-chat-export", async (_event, payload) => { try { - console.log('[Cookie Code] chat-export: получен запрос, format =', payload && payload.format, ', html.length =', payload && payload.html && payload.html.length); + console.log( + "[Cookie Code] chat-export: получен запрос, format =", + payload && payload.format, + ", html.length =", + payload && payload.html && payload.html.length, + ); const result = await chatExport.exportChat(payload || {}); - console.log('[Cookie Code] chat-export: результат =', JSON.stringify(result)); + console.log( + "[Cookie Code] chat-export: результат =", + JSON.stringify(result), + ); return result; } catch (err) { - console.error('[Cookie Code] chat-export error:', err.message, err.stack); + console.error("[Cookie Code] chat-export error:", err.message, err.stack); return { success: false, error: err.message }; } }); // ========== Todo-задачи окна ========== - ipcMain.handle('todo-get', async (event) => { + ipcMain.handle("todo-get", async (event) => { try { return { success: true, todos: todoStore.getList(event.sender.id) }; } catch (err) { @@ -779,10 +948,12 @@ function registerIpcHandlers() { } }); - ipcMain.handle('todo-clear', async (event) => { + ipcMain.handle("todo-clear", async (event) => { try { todoStore.clear(event.sender.id); - try { event.sender.send('todo-updated', { todos: [] }); } catch (_) {} + try { + event.sender.send("todo-updated", { todos: [] }); + } catch (_) {} return { success: true }; } catch (err) { return { success: false, error: err.message }; @@ -791,11 +962,16 @@ function registerIpcHandlers() { // ========== Git diff (панель «Изменения») ========== // Список изменённых файлов в текущем проекте (git status). - ipcMain.handle('git-status', async (event) => { + ipcMain.handle("git-status", async (event) => { try { const ctx = windowState.getContextByWebContents(event.sender); - const projectDir = ctx && ctx.sessionStore && ctx.sessionStore.state.selectedProjectDir; - if (!projectDir) return { success: false, reason: 'git не найден: проект не инициализирован' }; + const projectDir = + ctx && ctx.sessionStore && ctx.sessionStore.state.selectedProjectDir; + if (!projectDir) + return { + success: false, + reason: "git не найден: проект не инициализирован", + }; return await gitDiff.getStatus(projectDir); } catch (err) { return { success: false, reason: err.message }; @@ -803,12 +979,17 @@ function registerIpcHandlers() { }); // Unified diff одного файла. - ipcMain.handle('git-diff-file', async (event, { filePath, status } = {}) => { + ipcMain.handle("git-diff-file", async (event, { filePath, status } = {}) => { try { const ctx = windowState.getContextByWebContents(event.sender); - const projectDir = ctx && ctx.sessionStore && ctx.sessionStore.state.selectedProjectDir; - if (!projectDir) return { success: false, reason: 'git не найден: проект не инициализирован' }; - if (!filePath) return { success: false, reason: 'filePath is required' }; + const projectDir = + ctx && ctx.sessionStore && ctx.sessionStore.state.selectedProjectDir; + if (!projectDir) + return { + success: false, + reason: "git не найден: проект не инициализирован", + }; + if (!filePath) return { success: false, reason: "filePath is required" }; return await gitDiff.getFileDiff(projectDir, filePath, status); } catch (err) { return { success: false, reason: err.message }; @@ -816,44 +997,67 @@ function registerIpcHandlers() { }); // ========== Git history (вкладка «История») ========== - ipcMain.handle('git-log', async (event, { limit } = {}) => { + ipcMain.handle("git-log", async (event, { limit } = {}) => { try { const ctx = windowState.getContextByWebContents(event.sender); - const projectDir = ctx && ctx.sessionStore && ctx.sessionStore.state.selectedProjectDir; - if (!projectDir) return { success: false, reason: 'git не найден: проект не инициализирован' }; + const projectDir = + ctx && ctx.sessionStore && ctx.sessionStore.state.selectedProjectDir; + if (!projectDir) + return { + success: false, + reason: "git не найден: проект не инициализирован", + }; return await gitDiff.getLog(projectDir, limit || 20); } catch (err) { return { success: false, reason: err.message }; } }); - ipcMain.handle('git-commit-files', async (event, { hash } = {}) => { + ipcMain.handle("git-commit-files", async (event, { hash } = {}) => { try { const ctx = windowState.getContextByWebContents(event.sender); - const projectDir = ctx && ctx.sessionStore && ctx.sessionStore.state.selectedProjectDir; - if (!projectDir) return { success: false, reason: 'git не найден: проект не инициализирован' }; + const projectDir = + ctx && ctx.sessionStore && ctx.sessionStore.state.selectedProjectDir; + if (!projectDir) + return { + success: false, + reason: "git не найден: проект не инициализирован", + }; return await gitDiff.getCommitFiles(projectDir, hash); } catch (err) { return { success: false, reason: err.message }; } }); - ipcMain.handle('git-commit-file-diff', async (event, { hash, filePath } = {}) => { - try { - const ctx = windowState.getContextByWebContents(event.sender); - const projectDir = ctx && ctx.sessionStore && ctx.sessionStore.state.selectedProjectDir; - if (!projectDir) return { success: false, reason: 'git не найден: проект не инициализирован' }; - return await gitDiff.getCommitFileDiff(projectDir, hash, filePath); - } catch (err) { - return { success: false, reason: err.message }; - } - }); + ipcMain.handle( + "git-commit-file-diff", + async (event, { hash, filePath } = {}) => { + try { + const ctx = windowState.getContextByWebContents(event.sender); + const projectDir = + ctx && ctx.sessionStore && ctx.sessionStore.state.selectedProjectDir; + if (!projectDir) + return { + success: false, + reason: "git не найден: проект не инициализирован", + }; + return await gitDiff.getCommitFileDiff(projectDir, hash, filePath); + } catch (err) { + return { success: false, reason: err.message }; + } + }, + ); - ipcMain.handle('git-commit-diff', async (event, { hash } = {}) => { + ipcMain.handle("git-commit-diff", async (event, { hash } = {}) => { try { const ctx = windowState.getContextByWebContents(event.sender); - const projectDir = ctx && ctx.sessionStore && ctx.sessionStore.state.selectedProjectDir; - if (!projectDir) return { success: false, reason: 'git не найден: проект не инициализирован' }; + const projectDir = + ctx && ctx.sessionStore && ctx.sessionStore.state.selectedProjectDir; + if (!projectDir) + return { + success: false, + reason: "git не найден: проект не инициализирован", + }; return await gitDiff.getCommitDiff(projectDir, hash); } catch (err) { return { success: false, reason: err.message }; @@ -862,9 +1066,9 @@ function registerIpcHandlers() { // Открыть файл настроек (cuckoo-settings.json) системным редактором. // Если файла ещё нет — создаём его с дефолтами, чтобы редактор не ругался. - ipcMain.handle('cuckoo-settings-open-file', async () => { + ipcMain.handle("cuckoo-settings-open-file", async () => { try { - const fs = require('fs'); + const fs = require("fs"); const file = settingsStore.getSettingsPath(); if (!fs.existsSync(file)) { settingsStore.writeSettings(settingsStore.readSettings()); @@ -873,7 +1077,7 @@ function registerIpcHandlers() { if (errMsg) return { success: false, error: errMsg }; return { success: true, path: file }; } catch (err) { - console.error('[Cookie Code] 打开 settings.json 失败:', err.message); + console.error("[Cookie Code] 打开 settings.json 失败:", err.message); return { success: false, error: err.message }; } }); @@ -881,12 +1085,13 @@ function registerIpcHandlers() { // ========== Пользовательские фоны (userData/backgrounds) ========== // Папка: /backgrounds — пользователь кладёт туда картинки, // они автоматически появляются в выборе фонов в настройках. - const CUSTOM_BG_EXT = ['.webp', '.jpg', '.jpeg', '.png', '.gif']; - const getCustomBackgroundsDir = () => path.join(app.getPath('userData'), 'backgrounds'); + const CUSTOM_BG_EXT = [".webp", ".jpg", ".jpeg", ".png", ".gif"]; + const getCustomBackgroundsDir = () => + path.join(app.getPath("userData"), "backgrounds"); - ipcMain.handle('cuckoo-backgrounds-list', async () => { + ipcMain.handle("cuckoo-backgrounds-list", async () => { try { - const fs = require('fs'); + const fs = require("fs"); const dir = getCustomBackgroundsDir(); fs.mkdirSync(dir, { recursive: true }); const files = fs.readdirSync(dir).filter((f) => { @@ -896,7 +1101,7 @@ function registerIpcHandlers() { const ext = path.extname(f); const base = f.slice(0, -ext.length); return { - id: 'custom:' + base, + id: "custom:" + base, label: base, file: path.join(dir, f), custom: true, @@ -904,22 +1109,80 @@ function registerIpcHandlers() { }); return { success: true, dir, backgrounds: list }; } catch (err) { - console.error('[Cookie Code] 读取 пользовательских фонов失败:', err.message); - return { success: false, error: err.message, dir: getCustomBackgroundsDir(), backgrounds: [] }; + console.error( + "[Cookie Code] 读取 пользовательских фонов失败:", + err.message, + ); + return { + success: false, + error: err.message, + dir: getCustomBackgroundsDir(), + backgrounds: [], + }; } }); // Открыть папку с пользовательскими фонами в системном проводнике. - ipcMain.handle('cuckoo-backgrounds-open-folder', async () => { + ipcMain.handle("cuckoo-backgrounds-open-folder", async () => { try { - const fs = require('fs'); + const fs = require("fs"); const dir = getCustomBackgroundsDir(); fs.mkdirSync(dir, { recursive: true }); const errMsg = await shell.openPath(dir); if (errMsg) return { success: false, error: errMsg }; return { success: true, path: dir }; } catch (err) { - console.error('[Cookie Code] 打开 папку фонов失败:', err.message); + console.error("[Cookie Code] 打开 папку фонов失败:", err.message); + return { success: false, error: err.message }; + } + }); + + // Карта иконок Material Icon Theme. + // Отдаёт уникальные SVG + два маппинга (имя файла иконки → ext/имя файла), + // чтобы не дублировать одинаковые SVG. Preload кеширует результат. + ipcMain.handle("get-file-icons", async () => { + try { + const fs = require("fs"); + const pkgDir = path.dirname( + require.resolve("material-icon-theme/package.json"), + ); + const { generateManifest } = require("material-icon-theme"); + const manifest = generateManifest(); + const iconsDir = path.join(pkgDir, "icons"); + const icons = {}; // fileName -> svg + const getIconName = (defName) => { + const def = + manifest.iconDefinitions && manifest.iconDefinitions[defName]; + if (!def || !def.iconPath) return null; + const fileName = path.basename(def.iconPath); + if (icons[fileName] === undefined) { + try { + let svg = fs.readFileSync(path.join(iconsDir, fileName), "utf8"); + // Класс — чтобы отличить Material-иконку от fallback в CSS. + svg = svg.replace( + " { - return ipcRenderer.invoke('execute-command', { command, id }); + return ipcRenderer.invoke("execute-command", { command, id }); }, initProject: () => { - return ipcRenderer.invoke('init-project', { skipPrompt: false }); + return ipcRenderer.invoke("init-project", { skipPrompt: false }); }, openPath: (targetPath) => { - return ipcRenderer.invoke('open-path', { path: targetPath }); + return ipcRenderer.invoke("open-path", { path: targetPath }); + }, + getFileIcons: () => { + return ipcRenderer.invoke("get-file-icons"); }, updateProjectDir: () => { - return ipcRenderer.invoke('init-project', { skipPrompt: true }); + return ipcRenderer.invoke("init-project", { skipPrompt: true }); }, executeTool: (toolName, params, callId) => { - return ipcRenderer.invoke('execute-tool', { toolName, params, callId }); + return ipcRenderer.invoke("execute-tool", { toolName, params, callId }); }, executeJs: (code, callId) => { - return ipcRenderer.invoke('execute-js', { code, callId }); + return ipcRenderer.invoke("execute-js", { code, callId }); }, setPlanMode: (enabled) => { - return ipcRenderer.invoke('set-plan-mode', { enabled }); + return ipcRenderer.invoke("set-plan-mode", { enabled }); }, exitPlanModeResponse: (requestId, approved) => { - ipcRenderer.send('exit-plan-mode-response', { requestId, approved }); + ipcRenderer.send("exit-plan-mode-response", { requestId, approved }); }, killProcess: () => { - return ipcRenderer.invoke('kill-process'); + return ipcRenderer.invoke("kill-process"); }, // ===== Telegram-бот ===== - telegramApply: () => ipcRenderer.invoke('telegram-apply'), - telegramStatus: () => ipcRenderer.invoke('telegram-status'), - telegramPing: () => ipcRenderer.invoke('telegram-ping'), - telegramTest: () => ipcRenderer.invoke('telegram-test'), - telegramNotifyAI: (text) => ipcRenderer.invoke('telegram-notify-ai', { text }), - telegramApprovalRequest: (requestId, info) => ipcRenderer.invoke('telegram-approval-request', { requestId, info }), - telegramApprovalCancel: (requestId) => ipcRenderer.invoke('telegram-approval-cancel', { requestId }), + telegramApply: () => ipcRenderer.invoke("telegram-apply"), + telegramStatus: () => ipcRenderer.invoke("telegram-status"), + telegramPing: () => ipcRenderer.invoke("telegram-ping"), + telegramTest: () => ipcRenderer.invoke("telegram-test"), + telegramNotifyAI: (text) => + ipcRenderer.invoke("telegram-notify-ai", { text }), + telegramApprovalRequest: (requestId, info) => + ipcRenderer.invoke("telegram-approval-request", { requestId, info }), + telegramApprovalCancel: (requestId) => + ipcRenderer.invoke("telegram-approval-cancel", { requestId }), sendEnterToChat: () => { - return ipcRenderer.invoke('chat-send-enter'); + return ipcRenderer.invoke("chat-send-enter"); }, listSessions: () => { - return ipcRenderer.invoke('list-sessions'); + return ipcRenderer.invoke("list-sessions"); }, listProjectFiles: (query) => { - return ipcRenderer.invoke('list-project-files', { query }); + return ipcRenderer.invoke("list-project-files", { query }); }, navigateSession: (sessionId) => { - return ipcRenderer.invoke('navigate-session', { sessionId }); + return ipcRenderer.invoke("navigate-session", { sessionId }); }, newChat: () => { - return ipcRenderer.invoke('new-chat'); + return ipcRenderer.invoke("new-chat"); }, createProfileWindow: () => { - return ipcRenderer.invoke('create-profile-window'); + return ipcRenderer.invoke("create-profile-window"); }, listProfiles: () => { - return ipcRenderer.invoke('list-profiles'); + return ipcRenderer.invoke("list-profiles"); }, openProfileWindow: (profileId) => { - return ipcRenderer.invoke('open-profile-window', { profileId }); + return ipcRenderer.invoke("open-profile-window", { profileId }); }, deleteProfileWindow: (profileId) => { - return ipcRenderer.invoke('delete-profile', { profileId }); + return ipcRenderer.invoke("delete-profile", { profileId }); }, updateWindowName: (displayName) => { - return ipcRenderer.invoke('update-window-name', { displayName }); + return ipcRenderer.invoke("update-window-name", { displayName }); }, showAiNotification: () => { - return ipcRenderer.invoke('show-ai-notification'); + return ipcRenderer.invoke("show-ai-notification"); }, // ========== MCP 相关 API ========== listMcpServers: () => { - return ipcRenderer.invoke('list-mcp-servers'); + return ipcRenderer.invoke("list-mcp-servers"); }, upsertMcpServer: (server) => { - return ipcRenderer.invoke('upsert-mcp-server', { server }); + return ipcRenderer.invoke("upsert-mcp-server", { server }); }, removeMcpServer: (name) => { - return ipcRenderer.invoke('remove-mcp-server', { name }); + return ipcRenderer.invoke("remove-mcp-server", { name }); }, enableMcpServer: (name) => { - return ipcRenderer.invoke('enable-mcp-server', { name }); + return ipcRenderer.invoke("enable-mcp-server", { name }); }, disableMcpServer: (name) => { - return ipcRenderer.invoke('disable-mcp-server', { name }); + return ipcRenderer.invoke("disable-mcp-server", { name }); }, getMcpTools: () => { - return ipcRenderer.invoke('get-mcp-tools'); + return ipcRenderer.invoke("get-mcp-tools"); }, // ========== 平台相关 API ========== listProviders: () => { - return ipcRenderer.invoke('list-providers'); + return ipcRenderer.invoke("list-providers"); }, selectPlatform: (providerId) => { - return ipcRenderer.invoke('select-platform', { providerId }); + return ipcRenderer.invoke("select-platform", { providerId }); }, createProfileWindowWithProvider: (providerId) => { - return ipcRenderer.invoke('create-profile-window', { providerId }); + return ipcRenderer.invoke("create-profile-window", { providerId }); }, importProvider: () => { - return ipcRenderer.invoke('import-provider'); + return ipcRenderer.invoke("import-provider"); }, removeProvider: (filePath, providerId) => { - return ipcRenderer.invoke('remove-provider', { path: filePath, providerId }); + return ipcRenderer.invoke("remove-provider", { + path: filePath, + providerId, + }); }, replaceProvider: (providerId) => { - return ipcRenderer.invoke('replace-provider', { providerId }); + return ipcRenderer.invoke("replace-provider", { providerId }); }, // ========== Cookie Code 用户设置 (settings.json) ========== getCuckooSettings: () => { - return ipcRenderer.invoke('cuckoo-settings-get-all'); + return ipcRenderer.invoke("cuckoo-settings-get-all"); }, setCuckooSetting: (key, value) => { - return ipcRenderer.invoke('cuckoo-settings-set', { key, value }); + return ipcRenderer.invoke("cuckoo-settings-set", { key, value }); }, openCuckooSettingsFile: () => { - return ipcRenderer.invoke('cuckoo-settings-open-file'); + return ipcRenderer.invoke("cuckoo-settings-open-file"); }, // ========== Пользовательские фоны (userData/backgrounds) ========== listCustomBackgrounds: () => { - return ipcRenderer.invoke('cuckoo-backgrounds-list'); + return ipcRenderer.invoke("cuckoo-backgrounds-list"); }, openCustomBackgroundsFolder: () => { - return ipcRenderer.invoke('cuckoo-backgrounds-open-folder'); + return ipcRenderer.invoke("cuckoo-backgrounds-open-folder"); }, // ========== Todo-задачи ========== getTodos: () => { - return ipcRenderer.invoke('todo-get'); + return ipcRenderer.invoke("todo-get"); }, clearTodos: () => { - return ipcRenderer.invoke('todo-clear'); + return ipcRenderer.invoke("todo-clear"); }, onTodoUpdated: (callback) => { const listener = (_event, data) => callback(data); - ipcRenderer.on('todo-updated', listener); - return () => ipcRenderer.removeListener('todo-updated', listener); + ipcRenderer.on("todo-updated", listener); + return () => ipcRenderer.removeListener("todo-updated", listener); }, // ========== Git diff (панель «Изменения») ========== gitStatus: () => { - return ipcRenderer.invoke('git-status'); + return ipcRenderer.invoke("git-status"); }, gitDiffFile: (filePath, status) => { - return ipcRenderer.invoke('git-diff-file', { filePath, status }); + return ipcRenderer.invoke("git-diff-file", { filePath, status }); }, gitLog: (limit) => { - return ipcRenderer.invoke('git-log', { limit }); + return ipcRenderer.invoke("git-log", { limit }); }, gitCommitFiles: (hash) => { - return ipcRenderer.invoke('git-commit-files', { hash }); + return ipcRenderer.invoke("git-commit-files", { hash }); }, gitCommitFileDiff: (hash, filePath) => { - return ipcRenderer.invoke('git-commit-file-diff', { hash, filePath }); + return ipcRenderer.invoke("git-commit-file-diff", { hash, filePath }); }, gitCommitDiff: (hash) => { - return ipcRenderer.invoke('git-commit-diff', { hash }); + return ipcRenderer.invoke("git-commit-diff", { hash }); }, exportChat: (payload) => { - return ipcRenderer.invoke('cuckoo-chat-export', payload); + return ipcRenderer.invoke("cuckoo-chat-export", payload); }, // ========== API Уведомлений-баннеров (cuckoo-first-time-box style) ========== // Публичное API для показа модальных уведомлений сверху страницы @@ -179,34 +188,34 @@ let electronAPI = { // // window.electronAPI.hideBannerNotification(); // принудительно закрыть баннер showBannerNotification: (text, options) => { - const { showBannerNotification } = require('./overlay/ui'); + const { showBannerNotification } = require("./overlay/ui"); return showBannerNotification(text, options); }, hideBannerNotification: () => { - const { hideBannerNotification } = require('./overlay/ui'); + const { hideBannerNotification } = require("./overlay/ui"); return hideBannerNotification(); }, // ========== Whats-new (список нововведений после обновления) ========== onWhatsNewShow: (callback) => { const listener = (_event, payload) => callback(payload); - ipcRenderer.on('whats-new-show', listener); - return () => ipcRenderer.removeListener('whats-new-show', listener); + ipcRenderer.on("whats-new-show", listener); + return () => ipcRenderer.removeListener("whats-new-show", listener); }, openChangelogFile: () => { - return ipcRenderer.invoke('whats-new-open-changelog'); + return ipcRenderer.invoke("whats-new-open-changelog"); }, // ========== Диагностика интеграции (выполняется в контексте страницы) ========== runDiagnosticsText: async () => { - const { runDiagnostics, formatReportText } = require('./dom/diagnostics'); + const { runDiagnostics, formatReportText } = require("./dom/diagnostics"); const report = await runDiagnostics(); return formatReportText(report); }, }; try { - contextBridge.exposeInMainWorld('electronAPI', electronAPI); + contextBridge.exposeInMainWorld("electronAPI", electronAPI); } catch (err) { - console.error('[Cookie Code] contextBridge.exposeInMainWorld 失败:', err); + console.error("[Cookie Code] contextBridge.exposeInMainWorld 失败:", err); } // 无论 contextBridge 是否成功,都直接挂载到 window 作为备选 diff --git a/src/preload/dom/commands/controller.js b/src/preload/dom/commands/controller.js index d82a9c0..e7d21d5 100644 --- a/src/preload/dom/commands/controller.js +++ b/src/preload/dom/commands/controller.js @@ -3,12 +3,12 @@ * показывает меню и по выбору заменяет slash-токен на промпт. */ -const { detectTrigger } = require('./detect'); -const { searchCommands, findCommand, descOf } = require('./registry'); -const { t } = require('../../i18n/i18n'); -const menu = require('./menu'); -const chatInput = require('../chat-input'); -const { collectReviewDiff, buildReviewPrompt } = require('./review-context'); +const { detectTrigger } = require("./detect"); +const { searchCommands, findCommand, descOf } = require("./registry"); +const { t } = require("../../i18n/i18n"); +const menu = require("./menu"); +const chatInput = require("../chat-input"); +const { collectReviewDiff, buildReviewPrompt } = require("./review-context"); /** Текущий активный токен (span в поле). */ let activeHit = null; @@ -17,7 +17,7 @@ let menuOpen = false; /** Текущий список элементов меню (для навигации). */ let currentItems = []; /** Тип активного меню: 'command' | 'file'. */ -let activeKind = 'command'; +let activeKind = "command"; /** Таймер debounce для поиска файлов. */ let fileSearchTimer = null; /** Счётчик запросов файлов (для отбрасывания устаревших ответов). */ @@ -34,7 +34,11 @@ function onFieldUpdate(e) { if (!isTrackedField(field)) return; // keyup после навигации не меняет текст. Перерисовка здесь сбрасывает // подсветку меню на первый пункт, поэтому такие события игнорируем. - if (e.type === 'keyup' && ['ArrowDown', 'ArrowUp', 'Enter', 'Tab', 'Escape'].includes(e.key)) return; + if ( + e.type === "keyup" && + ["ArrowDown", "ArrowUp", "Enter", "Tab", "Escape"].includes(e.key) + ) + return; update(e); } @@ -58,15 +62,15 @@ function update(e) { } // Ветка @-файлов: запрашиваем список файлов проекта через IPC. - if (hit.trigger === '@') { + if (hit.trigger === "@") { activeHit = { hit, field }; - activeKind = 'file'; + activeKind = "file"; scheduleFileSearch(field, hit.query); return; } // Ветка slash-команд. - activeKind = 'command'; + activeKind = "command"; cancelFileSearch(); const candidates = searchCommands(hit.query).map((c) => ({ name: c.name, @@ -106,18 +110,24 @@ function cancelFileSearch() { function scheduleFileSearch(field, query) { // Тот же запрос уже показан — не перезапрашиваем и не перерисовываем меню // (иначе навигация стрелками сбрасывает подсветку на первый элемент). - if (menuOpen && activeKind === 'file' && query === lastFileQuery) return; + if (menuOpen && activeKind === "file" && query === lastFileQuery) return; cancelFileSearch(); const seq = ++fileSearchSeq; fileSearchTimer = setTimeout(async () => { fileSearchTimer = null; if (!activeHit || activeHit.field !== field) return; - if (!window.electronAPI || typeof window.electronAPI.listProjectFiles !== 'function') return; + if ( + !window.electronAPI || + typeof window.electronAPI.listProjectFiles !== "function" + ) + return; let files = []; try { const res = await window.electronAPI.listProjectFiles(query); if (res && res.success && Array.isArray(res.files)) files = res.files; - } catch (_) { files = []; } + } catch (_) { + files = []; + } // Устаревший ответ (пользователь уже набрал другое) — игнорируем. if (seq !== fileSearchSeq) return; if (!activeHit || activeHit.field !== field) return; @@ -128,16 +138,21 @@ function scheduleFileSearch(field, query) { return; } - const wasOpen = menuOpen && activeKind === 'file'; + const wasOpen = menuOpen && activeKind === "file"; const prevHighlight = wasOpen ? menu.getState().highlight : 0; - currentItems = files.map((f) => ({ name: f.rel, abs: f.abs, isDir: !!f.isDir, description: '' })); - activeKind = 'file'; - menu.show( - currentItems, - field, - (idx) => pick(idx), - { prefix: '', title: t('cmd.menu.files'), monospace: false } - ); + currentItems = files.map((f) => ({ + name: f.rel, + abs: f.abs, + isDir: !!f.isDir, + description: "", + })); + activeKind = "file"; + menu.show(currentItems, field, (idx) => pick(idx), { + prefix: "", + title: t("cmd.menu.files"), + monospace: false, + icons: true, + }); if (wasOpen && prevHighlight > 0) { const idx = Math.min(prevHighlight, currentItems.length - 1); menu.highlightItem(idx); @@ -157,17 +172,17 @@ function onKeyDown(e) { if (!menuOpen || !activeHit) return false; switch (e.key) { - case 'ArrowDown': + case "ArrowDown": move(1); return true; - case 'ArrowUp': + case "ArrowUp": move(-1); return true; - case 'Enter': - case 'Tab': + case "Enter": + case "Tab": pick(menu.getState().highlight); return true; - case 'Escape': + case "Escape": closeMenu(); return true; default: @@ -201,7 +216,7 @@ function pick(idx) { // Для файлов вставляем абсолютный путь, для команд — промпт. let replacement; - if (activeKind === 'file') { + if (activeKind === "file") { replacement = item.abs || item.name; } else { const cmd = findCommand(item.name); @@ -209,22 +224,27 @@ function pick(idx) { replacement = cmd.prompt; } - const isReviewCommand = activeKind === 'command' && item.name === 'review'; + const isReviewCommand = activeKind === "command" && item.name === "review"; // Review отправляется после async-сбора diff. Не вставляем промежуточный // prompt в поле: событие input может повторно открыть меню и выбрать команду. if (isReviewCommand) { closeMenu(); activeHit = null; - collectReviewDiff(window.electronAPI).then((result) => { - chatInput.sendToChat(buildReviewPrompt(result), 'review', 0); - }).catch((err) => { - chatInput.sendToChat( - buildReviewPrompt({ diff: '', reason: err.message || 'Не удалось подготовить review' }), - 'review', - 0 - ); - }); + collectReviewDiff(window.electronAPI) + .then((result) => { + chatInput.sendToChat(buildReviewPrompt(result), "review", 0); + }) + .catch((err) => { + chatInput.sendToChat( + buildReviewPrompt({ + diff: "", + reason: err.message || "Не удалось подготовить review", + }), + "review", + 0, + ); + }); return; } @@ -245,7 +265,6 @@ function pick(idx) { // Устанавливаем курсор после вставленного текста const pos = before.length + replacement.length; setCaret(field, pos); - } /** Закрывает меню. */ @@ -253,7 +272,7 @@ function closeMenu() { menu.hide(); menuOpen = false; currentItems = []; - activeKind = 'command'; + activeKind = "command"; lastFileQuery = null; cancelFileSearch(); } @@ -266,18 +285,22 @@ function attachField(field) { if (!field || field.__cuckooCmdBound) return; field.__cuckooCmdBound = true; - field.addEventListener('input', onFieldUpdate); - field.addEventListener('keyup', onFieldUpdate); - field.addEventListener('click', onFieldUpdate); - field.addEventListener('keydown', (e) => { - const handled = onKeyDown(e); - if (handled) { - e.preventDefault(); - e.stopPropagation(); - } - }, true); - - field.addEventListener('blur', () => { + field.addEventListener("input", onFieldUpdate); + field.addEventListener("keyup", onFieldUpdate); + field.addEventListener("click", onFieldUpdate); + field.addEventListener( + "keydown", + (e) => { + const handled = onKeyDown(e); + if (handled) { + e.preventDefault(); + e.stopPropagation(); + } + }, + true, + ); + + field.addEventListener("blur", () => { // небольшая задержка, чтобы mousedown по пункту успел сработать setTimeout(() => { if (menuOpen) closeMenu(); @@ -289,34 +312,39 @@ function attachField(field) { function isTrackedField(el) { if (!el) return false; - if (el.tagName === 'TEXTAREA' || el.tagName === 'INPUT') return true; - if (el.isContentEditable || el.getAttribute('contenteditable') === 'true') return true; + if (el.tagName === "TEXTAREA" || el.tagName === "INPUT") return true; + if (el.isContentEditable || el.getAttribute("contenteditable") === "true") + return true; return false; } function getValue(field) { - if (field.tagName === 'TEXTAREA' || field.tagName === 'INPUT') return field.value || ''; - return field.textContent || ''; + if (field.tagName === "TEXTAREA" || field.tagName === "INPUT") + return field.value || ""; + return field.textContent || ""; } function setValue(field, value) { - if (field.tagName === 'TEXTAREA' || field.tagName === 'INPUT') { - const proto = field.tagName === 'TEXTAREA' - ? window.HTMLTextAreaElement.prototype - : window.HTMLInputElement.prototype; - const nativeSetter = Object.getOwnPropertyDescriptor(proto, 'value').set; + if (field.tagName === "TEXTAREA" || field.tagName === "INPUT") { + const proto = + field.tagName === "TEXTAREA" + ? window.HTMLTextAreaElement.prototype + : window.HTMLInputElement.prototype; + const nativeSetter = Object.getOwnPropertyDescriptor(proto, "value").set; nativeSetter.call(field, value); - field.dispatchEvent(new Event('input', { bubbles: true })); + field.dispatchEvent(new Event("input", { bubbles: true })); } else { // contenteditable field.textContent = value; - field.dispatchEvent(new Event('input', { bubbles: true })); + field.dispatchEvent(new Event("input", { bubbles: true })); } } function getCaret(field) { - if (field.tagName === 'TEXTAREA' || field.tagName === 'INPUT') { - return typeof field.selectionStart === 'number' ? field.selectionStart : null; + if (field.tagName === "TEXTAREA" || field.tagName === "INPUT") { + return typeof field.selectionStart === "number" + ? field.selectionStart + : null; } const sel = window.getSelection(); if (!sel || sel.rangeCount === 0) return null; @@ -329,8 +357,10 @@ function getCaret(field) { } function setCaret(field, pos) { - if (field.tagName === 'TEXTAREA' || field.tagName === 'INPUT') { - try { field.setSelectionRange(pos, pos); } catch (_) {} + if (field.tagName === "TEXTAREA" || field.tagName === "INPUT") { + try { + field.setSelectionRange(pos, pos); + } catch (_) {} return; } // contenteditable — приблизительно: ставим в конец @@ -348,6 +378,10 @@ module.exports = { closeMenu, onKeyDown, update, - get activeHit() { return activeHit; }, - get menuOpen() { return menuOpen; }, + get activeHit() { + return activeHit; + }, + get menuOpen() { + return menuOpen; + }, }; diff --git a/src/preload/dom/commands/menu.js b/src/preload/dom/commands/menu.js index c16e445..db06b92 100644 --- a/src/preload/dom/commands/menu.js +++ b/src/preload/dom/commands/menu.js @@ -4,9 +4,15 @@ * Стиль согласован с вкладкой настроек Cookie Code (тёмный, сине-фиолетовый). */ -const { t } = require('../../i18n/i18n'); +const { t } = require("../../i18n/i18n"); -const MENU_ID = 'cuckoo-command-menu'; +// Иконка файла (Material Icon Theme) для строк меню @-упоминаний. +let getFileIcon = () => ""; +try { + ({ getFileIcon } = require("../file-chip")); +} catch (_) {} + +const MENU_ID = "cuckoo-command-menu"; /** Состояние текущего меню. */ let state = { @@ -14,9 +20,10 @@ let state = { items: [], highlight: 0, anchor: null, // поле ввода, к которому привязано меню - prefix: '/', // префикс токена для отображения ('' — без префикса) + prefix: "/", // префикс токена для отображения ('' — без префикса) title: null, // заголовок меню (null — взять cmd.menu.title по умолчанию) monospace: true, // моноширинный ли шрифт у имени элемента + icons: false, // показывать ли иконки (только для @-файлов) }; /** @@ -27,28 +34,28 @@ function ensureMenuEl() { let el = document.getElementById(MENU_ID); if (el) return el; - el = document.createElement('div'); + el = document.createElement("div"); el.id = MENU_ID; el.style.cssText = [ - 'position: fixed', - 'z-index: 2147483600', - 'display: none', - 'min-width: 280px', - 'max-width: 460px', - 'max-height: 320px', - 'overflow-y: auto', - 'padding: 6px', - 'box-sizing: border-box', - 'border-radius: 12px', - 'border: 1px solid rgba(139,147,255,0.35)', - 'background: rgba(20,23,40,0.96)', - 'box-shadow: 0 12px 32px rgba(0,0,0,0.45)', - 'backdrop-filter: blur(14px)', - '-webkit-backdrop-filter: blur(14px)', + "position: fixed", + "z-index: 2147483600", + "display: none", + "min-width: 280px", + "max-width: 460px", + "max-height: 320px", + "overflow-y: auto", + "padding: 6px", + "box-sizing: border-box", + "border-radius: 12px", + "border: 1px solid rgba(139,147,255,0.35)", + "background: rgba(20,23,40,0.96)", + "box-shadow: 0 12px 32px rgba(0,0,0,0.45)", + "backdrop-filter: blur(14px)", + "-webkit-backdrop-filter: blur(14px)", 'font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif', - 'color: #dde1ff', - 'font-size: 13px', - ].join(';'); + "color: #dde1ff", + "font-size: 13px", + ].join(";"); document.body.appendChild(el); return el; } @@ -60,60 +67,101 @@ function ensureMenuEl() { */ function render(items, highlight) { const el = ensureMenuEl(); - el.innerHTML = ''; + el.innerHTML = ""; if (!items || items.length === 0) { - el.style.display = 'none'; + el.style.display = "none"; state.visible = false; return; } - const title = document.createElement('div'); - title.textContent = state.title != null ? state.title : t('cmd.menu.title'); - title.style.cssText = 'padding:6px 10px;font-size:11px;letter-spacing:0.6px;text-transform:uppercase;color:#8a90b8;'; + const title = document.createElement("div"); + title.textContent = state.title != null ? state.title : t("cmd.menu.title"); + title.style.cssText = + "padding:6px 10px;font-size:11px;letter-spacing:0.6px;text-transform:uppercase;color:#8a90b8;"; el.appendChild(title); items.forEach((item, idx) => { - const row = document.createElement('div'); + const row = document.createElement("div"); const active = idx === highlight; - row.className = 'cuckoo-command-item'; + row.className = "cuckoo-command-item"; row.dataset.index = String(idx); row.style.cssText = [ - 'display:flex', - 'flex-direction:column', - 'gap:2px', - 'padding:8px 10px', - 'border-radius:8px', - 'cursor:pointer', - 'transition: background 0.12s', - 'background:' + (active ? 'rgba(139,147,255,0.22)' : 'transparent'), - ].join(';'); - - const name = document.createElement('div'); - name.textContent = (item.isDir ? '📁 ' : '') + state.prefix + item.name + (item.isDir ? '/' : ''); - name.style.cssText = 'font-weight:600;color:#c8ccff;' + (state.monospace ? 'font-family:Consolas,monospace;' : ''); + "display:flex", + "flex-direction:column", + "gap:2px", + "padding:8px 10px", + "border-radius:8px", + "cursor:pointer", + "transition: background 0.12s", + "background:" + (active ? "rgba(139,147,255,0.22)" : "transparent"), + ].join(";"); + + const name = document.createElement("div"); + name.style.cssText = + "display:flex;align-items:center;gap:6px;font-weight:600;color:#c8ccff;" + + (state.monospace ? "font-family:Consolas,monospace;" : ""); + + // Иконка: только для @-файлов (state.icons). + if (state.icons) { + const icon = document.createElement("span"); + icon.className = "cuckoo-command-item-icon"; + icon.style.cssText = + "display:inline-flex;align-items:center;justify-content:center;width:16px;height:16px;flex:0 0 16px;"; + if (item.isDir) { + icon.innerHTML = + '"; + } else { + const svg = getFileIcon(item.abs || item.name); + if (svg) { + icon.innerHTML = svg; + const s = icon.querySelector("svg"); + if (s) { + s.setAttribute("width", "16"); + s.setAttribute("height", "16"); + // Fallback-иконка (монохромный документ) — контурная, + // иначе браузер заливает её чёрным. + if (s.classList.contains("cuckoo-file-chip__fallback")) { + s.setAttribute("fill", "none"); + s.setAttribute("stroke", "currentColor"); + s.setAttribute("stroke-width", "2"); + s.setAttribute("stroke-linecap", "round"); + s.setAttribute("stroke-linejoin", "round"); + } + } + } + } + name.appendChild(icon); + } + + const label = document.createElement("span"); + label.textContent = state.prefix + item.name + (item.isDir ? "/" : ""); + name.appendChild(label); + row.appendChild(name); - const desc = document.createElement('div'); - desc.textContent = item.description || ''; - desc.style.cssText = 'font-size:12px;color:#8a90b8;'; + const desc = document.createElement("div"); + desc.textContent = item.description || ""; + desc.style.cssText = "font-size:12px;color:#8a90b8;"; row.appendChild(desc); - row.addEventListener('mouseenter', () => { + row.addEventListener("mouseenter", () => { state.highlight = idx; highlightItem(idx); }); - row.addEventListener('mousedown', (e) => { + row.addEventListener("mousedown", (e) => { // mousedown, чтобы не потерять фокус поля ввода e.preventDefault(); e.stopPropagation(); - if (typeof state.onPick === 'function') state.onPick(idx); + if (typeof state.onPick === "function") state.onPick(idx); }); el.appendChild(row); }); - el.style.display = 'block'; + el.style.display = "block"; state.visible = true; } @@ -124,9 +172,10 @@ function render(items, highlight) { function highlightItem(index) { const el = document.getElementById(MENU_ID); if (!el) return; - const rows = el.querySelectorAll('.cuckoo-command-item'); + const rows = el.querySelectorAll(".cuckoo-command-item"); rows.forEach((row, i) => { - row.style.background = i === index ? 'rgba(139,147,255,0.22)' : 'transparent'; + row.style.background = + i === index ? "rgba(139,147,255,0.22)" : "transparent"; }); state.highlight = index; } @@ -138,7 +187,7 @@ function highlightItem(index) { function scrollIntoView(index) { const el = document.getElementById(MENU_ID); if (!el) return; - const rows = el.querySelectorAll('.cuckoo-command-item'); + const rows = el.querySelectorAll(".cuckoo-command-item"); const row = rows[index]; if (!row) return; const rowTop = row.offsetTop; @@ -172,8 +221,8 @@ function position(anchor) { let top = rect.top - menuRect.height - gap; if (top < 8) top = rect.bottom + gap; - el.style.left = Math.round(left) + 'px'; - el.style.top = Math.round(top) + 'px'; + el.style.left = Math.round(left) + "px"; + el.style.top = Math.round(top) + "px"; } /** @@ -188,9 +237,10 @@ function show(items, anchor, onPick, options) { state.anchor = anchor; state.onPick = onPick; const opts = options || {}; - state.prefix = opts.prefix != null ? opts.prefix : '/'; + state.prefix = opts.prefix != null ? opts.prefix : "/"; state.title = opts.title != null ? opts.title : null; state.monospace = opts.monospace != null ? opts.monospace : true; + state.icons = opts.icons === true; render(items, 0); position(anchor); } @@ -198,13 +248,14 @@ function show(items, anchor, onPick, options) { /** Скрывает меню. */ function hide() { const el = document.getElementById(MENU_ID); - if (el) el.style.display = 'none'; + if (el) el.style.display = "none"; state.visible = false; state.items = []; state.onPick = undefined; - state.prefix = '/'; + state.prefix = "/"; state.title = null; state.monospace = true; + state.icons = false; } /** diff --git a/src/preload/dom/file-chip.js b/src/preload/dom/file-chip.js index 2a1175c..fa9bff6 100644 --- a/src/preload/dom/file-chip.js +++ b/src/preload/dom/file-chip.js @@ -13,50 +13,162 @@ * модуль просто пропускает шаг и пишет предупреждение в консоль. */ -const { safe } = require('./safe'); +const { safe } = require("./safe"); /** Метка — «элемент уже превращён в чип». */ -const PATCHED_ATTR = 'data-cuckoo-file-chip'; +const PATCHED_ATTR = "data-cuckoo-file-chip"; /** Класс чипа. */ -const CHIP_CLASS = 'cuckoo-file-chip'; +const CHIP_CLASS = "cuckoo-file-chip"; /** Стили чипа. Инжектятся один раз. */ -const STYLE_ID = 'cuckoo-file-chip-style'; +const STYLE_ID = "cuckoo-file-chip-style"; const STYLE_CSS = - '.' + CHIP_CLASS + ' {' + - 'display: inline-flex; align-items: center; gap: 4px;' + - 'padding: 1px 6px 1px 4px;' + - 'background: rgb(44, 44, 46);' + - 'border: 1px solid rgba(128,128,160,.18);' + - 'border-radius: 6px;' + - 'font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;' + - 'font-size: 13px; line-height: 20px;' + - 'color: inherit; cursor: pointer;' + - 'transition: background .12s ease, border-color .12s ease;' + - 'vertical-align: baseline;' + - '}' + - '.' + CHIP_CLASS + ':hover {' + - 'background: rgba(96, 165, 250, .18);' + - 'border-color: rgba(96, 165, 250, .45);' + - '}' + - '.' + CHIP_CLASS + ' svg {' + - 'width: 12px; height: 12px; flex: 0 0 12px;' + - 'stroke: currentColor; fill: none;' + - 'stroke-width: 2; stroke-linecap: round; stroke-linejoin: round;' + - 'opacity: .75;' + - '}' + - '.' + CHIP_CLASS + ':hover svg { opacity: 1; }' + - '.' + CHIP_CLASS + ' .' + CHIP_CLASS + '__path {' + - 'white-space: nowrap;' + - '}'; - -/** SVG-иконка «файл». */ -const FILE_ICON_SVG = - ''; + "." + + CHIP_CLASS + + " {" + + "display: inline-flex; align-items: center; gap: 4px;" + + "padding: 1px 6px 1px 4px;" + + "background: rgb(44, 44, 46);" + + "border: 1px solid rgba(128,128,160,.18);" + + "border-radius: 6px;" + + "font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;" + + "font-size: 13px; line-height: 20px;" + + "color: inherit; cursor: pointer;" + + "transition: background .12s ease, border-color .12s ease;" + + "vertical-align: baseline;" + + "}" + + "." + + CHIP_CLASS + + ":hover {" + + "background: rgba(96, 165, 250, .18);" + + "border-color: rgba(96, 165, 250, .45);" + + "}" + + "." + + CHIP_CLASS + + " svg {" + + "width: 14px; height: 14px; flex: 0 0 14px;" + + "opacity: .95;" + + "}" + + "." + + CHIP_CLASS + + " svg." + + CHIP_CLASS + + "__fallback {" + + "stroke: currentColor; fill: none;" + + "stroke-width: 2; stroke-linecap: round; stroke-linejoin: round;" + + "opacity: .75;" + + "}" + + "." + + CHIP_CLASS + + ":hover svg { opacity: 1; }" + + "." + + CHIP_CLASS + + " ." + + CHIP_CLASS + + "__path {" + + "white-space: nowrap;" + + "}"; + +/** Обёртка fallback-иконки чипа. */ +function svgWrap(inner) { + return ( + '" + ); +} + +/** Базовая иконка «файл» (fallback, если Material-иконка не найдена). */ +const FILE_ICON_SVG = svgWrap( + '' + + '', +); + +/** + * Кэш Material-иконок из главного процесса. + * icons: { fileName: '' } + * fileExtensions: { lua: 'lua', … } (ext → имя SVG-файла) + * fileNames: { dockerfile: 'docker', … } + */ +let MATERIAL_ICONS = { icons: {}, fileExtensions: {}, fileNames: {} }; + +/** + * Загрузить карту иконок Material Icon Theme из main-процесса. + * Вызывается один раз при старте. При ошибке — остаются fallback-иконки. + */ +async function loadMaterialIcons() { + try { + if ( + !window.electronAPI || + typeof window.electronAPI.getFileIcons !== "function" + ) { + console.warn( + "[Cookie Code] file-chip: electronAPI.getFileIcons недоступен — только fallback", + ); + return; + } + const res = await window.electronAPI.getFileIcons(); + if (res && res.success && res.data) { + MATERIAL_ICONS = { + icons: res.data.icons || {}, + fileExtensions: res.data.fileExtensions || {}, + fileNames: res.data.fileNames || {}, + }; + console.log( + "[Cookie Code] file-chip: Material-иконок загружено", + Object.keys(MATERIAL_ICONS.icons).length, + "уникальных, расширений:", + Object.keys(MATERIAL_ICONS.fileExtensions).length, + "имён:", + Object.keys(MATERIAL_ICONS.fileNames).length, + ); + // Пересканируем уже отрендеренные ответы, чтобы применить иконки. + try { + scanAll(); + } catch (_) {} + } + } catch (err) { + console.warn( + "[Cookie Code] file-chip: loadMaterialIcons error:", + err.message, + ); + } +} + +/** + * Определить SVG-иконку по пути/имени файла. + * Сначала ищет по полному имени (fileNames), затем по расширению (fileExtensions). + * Если ничего не найдено — возвращает fallback. + * @param {string} filePath + * @returns {string} SVG-разметка + */ +function getFileIcon(filePath) { + if (!filePath || typeof filePath !== "string") return FILE_ICON_SVG; + const name = filePath.replace(/\\/g, "/").split("/").pop().toLowerCase(); + if (!name) return FILE_ICON_SVG; + + const resolve = (iconName) => + iconName && MATERIAL_ICONS.icons[iconName] + ? MATERIAL_ICONS.icons[iconName] + : null; + + // 1) Точное имя файла (dockerfile, makefile, .gitignore, license и т.п.) + let svg = resolve(MATERIAL_ICONS.fileNames[name]); + if (svg) return svg; + + // 2) По расширению + const dot = name.lastIndexOf("."); + if (dot > 0) { + const ext = name.slice(dot + 1); + svg = resolve(MATERIAL_ICONS.fileExtensions[ext]); + if (svg) return svg; + } + + return FILE_ICON_SVG; +} /** * Проверка: текст — путь к файлу, пригодный для чипа. @@ -79,16 +191,21 @@ const FILE_ICON_SVG = * @returns {boolean} */ function isFilePath(text) { - if (!text || typeof text !== 'string') return false; + if (!text || typeof text !== "string") return false; const t = text.trim(); if (t.length < 5 || t.length > 500) return false; // Плейсхолдер «...» — не открываем. - if (t.includes('...')) return false; + if (t.includes("...")) return false; // URL / email — не файл. if (/^[a-z]+:\/\//i.test(t)) return false; - if (t.includes('@') && !t.startsWith('.') && !t.startsWith('/') && !/[\\/]/.test(t.split('@')[0])) { + if ( + t.includes("@") && + !t.startsWith(".") && + !t.startsWith("/") && + !/[\\/]/.test(t.split("@")[0]) + ) { // Похоже на email/ссылку — не считаем // Но если есть слеш до @ — маловероятно. Оставим простую эвристику. } @@ -101,8 +218,8 @@ function isFilePath(text) { // Абсолютный Windows: C:\folder\file.ext if (/^[A-Za-z]:[\\/]/.test(t)) { - const norm = t.replace(/\\/g, '/'); - const parts = norm.split('/').filter(Boolean); + const norm = t.replace(/\\/g, "/"); + const parts = norm.split("/").filter(Boolean); // диск + минимум 2 сегмента (папка + файл) if (parts.length < 3) return false; const last = parts[parts.length - 1]; @@ -112,9 +229,9 @@ function isFilePath(text) { // Относительный путь: содержит / или \, минимум один сегмент, последний — файл с расширением if (!/[\\/]/.test(t)) return false; // Исключим строки, начинающиеся с одиночного слеша (не Windows-путь) - const cleaned = t.replace(/^\.\//, '').replace(/^\.\.\//, ''); - const norm = cleaned.replace(/\\/g, '/'); - const parts = norm.split('/').filter(Boolean); + const cleaned = t.replace(/^\.\//, "").replace(/^\.\.\//, ""); + const norm = cleaned.replace(/\\/g, "/"); + const parts = norm.split("/").filter(Boolean); if (parts.length < 2) return false; // относительный должен быть «папка/файл» const last = parts[parts.length - 1]; if (!/^[^\\/:*?"<>|]+\.[A-Za-z0-9]{1,12}$/.test(last)) return false; @@ -132,7 +249,7 @@ const isAbsoluteWindowsPath = isFilePath; */ function normalizePath(p) { if (!p) return p; - return p.replace(/\//g, '\\'); + return p.replace(/\//g, "\\"); } /** @@ -152,17 +269,22 @@ function resolveToAbsolute(p) { // Относительный — нужен projectDir let projectDir = null; try { - const state = require('./state'); + const state = require("./state"); projectDir = state.currentProjectDir || null; } catch (_) {} if (!projectDir) { - return { abs: null, error: 'Не выбран проект (currentProjectDir пуст) — нельзя разрешить относительный путь' }; + return { + abs: null, + error: + "Не выбран проект (currentProjectDir пуст) — нельзя разрешить относительный путь", + }; } // Уберём .\ и ..\ при склейке — просто соединим - const sep = projectDir.includes('\\') ? '\\' : '/'; - const rel = norm.replace(/^\.\//, '').replace(/^\.\.\//, ''); - const abs = projectDir.replace(/[\\/]+$/, '') + sep + rel.replace(/^[\\/]+/, ''); + const sep = projectDir.includes("\\") ? "\\" : "/"; + const rel = norm.replace(/^\.\//, "").replace(/^\.\.\//, ""); + const abs = + projectDir.replace(/[\\/]+$/, "") + sep + rel.replace(/^[\\/]+/, ""); return { abs }; } @@ -173,72 +295,93 @@ function resolveToAbsolute(p) { */ function patchCodeEl(codeEl) { if (!enabled) return; - if (!codeEl || codeEl.getAttribute(PATCHED_ATTR) === '1') return; - const text = (codeEl.textContent || '').trim(); + if (!codeEl || codeEl.getAttribute(PATCHED_ATTR) === "1") return; + const text = (codeEl.textContent || "").trim(); if (!isFilePath(text)) return; const normPath = normalizePath(text); // Строим чип - const chip = document.createElement('span'); + const chip = document.createElement("span"); chip.className = CHIP_CLASS; - chip.setAttribute('data-path', normPath); - chip.setAttribute('title', normPath); - chip.setAttribute('role', 'button'); - chip.setAttribute('tabindex', '0'); - chip.innerHTML = FILE_ICON_SVG + '' + escapeHtml(text) + ''; + chip.setAttribute("data-path", normPath); + chip.setAttribute("title", normPath); + chip.setAttribute("role", "button"); + chip.setAttribute("tabindex", "0"); + chip.innerHTML = + getFileIcon(normPath) + + '' + + escapeHtml(text) + + ""; // Клик — открыть в системе const open = (e) => { e.preventDefault(); e.stopPropagation(); try { - if (!window.electronAPI || typeof window.electronAPI.openPath !== 'function') { - console.warn('[Cookie Code] open-path: electronAPI.openPath не доступен (обновите preload)'); + if ( + !window.electronAPI || + typeof window.electronAPI.openPath !== "function" + ) { + console.warn( + "[Cookie Code] open-path: electronAPI.openPath не доступен (обновите preload)", + ); return; } // Резолвим относительный путь через projectDir const resolved = resolveToAbsolute(normPath); if (!resolved.abs) { - console.warn('[Cookie Code] open-path: ' + (resolved.error || 'не удалось разрешить путь')); + console.warn( + "[Cookie Code] open-path: " + + (resolved.error || "не удалось разрешить путь"), + ); return; } - window.electronAPI.openPath(resolved.abs).then((res) => { - if (res && !res.success) { - console.warn('[Cookie Code] open-path: ' + (res.error || 'не удалось открыть')); - } - }).catch((err) => { - console.warn('[Cookie Code] open-path: ' + err.message); - }); + window.electronAPI + .openPath(resolved.abs) + .then((res) => { + if (res && !res.success) { + console.warn( + "[Cookie Code] open-path: " + (res.error || "не удалось открыть"), + ); + } + }) + .catch((err) => { + console.warn("[Cookie Code] open-path: " + err.message); + }); } catch (err) { - console.warn('[Cookie Code] open-path: ' + err.message); + console.warn("[Cookie Code] open-path: " + err.message); } }; - chip.addEventListener('click', open); - chip.addEventListener('keydown', (e) => { if (e.key === 'Enter' || e.key === ' ') open(e); }); + chip.addEventListener("click", open); + chip.addEventListener("keydown", (e) => { + if (e.key === "Enter" || e.key === " ") open(e); + }); // Заменяем содержимое на чип - codeEl.setAttribute(PATCHED_ATTR, '1'); - codeEl.textContent = ''; + codeEl.setAttribute(PATCHED_ATTR, "1"); + codeEl.textContent = ""; codeEl.appendChild(chip); - codeEl.style.background = 'transparent'; - codeEl.style.padding = '0'; - codeEl.style.border = 'none'; + codeEl.style.background = "transparent"; + codeEl.style.padding = "0"; + codeEl.style.border = "none"; } function escapeHtml(s) { return String(s) - .replace(/&/g, '&') - .replace(//g, '>') - .replace(/"/g, '"'); + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """); } /** Инжект стилей один раз. */ function ensureStyles() { - if (typeof document === 'undefined') return; + if (typeof document === "undefined") return; if (document.getElementById(STYLE_ID)) return; - const style = document.createElement('style'); + const style = document.createElement("style"); style.id = STYLE_ID; style.textContent = STYLE_CSS; document.head.appendChild(style); @@ -250,13 +393,15 @@ function ensureStyles() { */ function scanAll(root) { if (!enabled) return; - safe('file-chip.scanAll', () => { + safe("file-chip.scanAll", () => { const doc = root || document; const scope = doc.querySelectorAll ? doc : document; - const codes = scope.querySelectorAll('.ds-markdown code, .ds-markdown-paragraph code'); + const codes = scope.querySelectorAll( + ".ds-markdown code, .ds-markdown-paragraph code", + ); for (const code of codes) { // Пропускаем код-блоки (многострочные): только inline без
-родителя.
-      if (code.closest('pre')) continue;
+      if (code.closest("pre")) continue;
       // Пропускаем пользовательские сообщения
       if (isInsideUserMessage(code)) continue;
       patchCodeEl(code);
@@ -271,9 +416,9 @@ function scanAll(root) {
  */
 function isInsideUserMessage(node) {
   try {
-    const { getProviderByUrl } = require('../../providers');
+    const { getProviderByUrl } = require("../../providers");
     const provider = getProviderByUrl(window.location.href);
-    if (provider && typeof provider.isUserMessage === 'function') {
+    if (provider && typeof provider.isUserMessage === "function") {
       return provider.isUserMessage(node);
     }
   } catch (_) {}
@@ -290,25 +435,25 @@ let enabled = true;
 function setEnabled(on) {
   enabled = on !== false;
   if (!enabled) {
-    safe('file-chip.setEnabled.remove', () => removeAllChips());
+    safe("file-chip.setEnabled.remove", () => removeAllChips());
   } else {
     // Включили — применим чипы к уже существующим .
-    safe('file-chip.setEnabled.rescan', () => scanAll());
+    safe("file-chip.setEnabled.rescan", () => scanAll());
   }
 }
 
 /** Снять все чипы: вернуть оригинальный текст в . */
 function removeAllChips() {
-  const chips = document.querySelectorAll('.' + CHIP_CLASS);
+  const chips = document.querySelectorAll("." + CHIP_CLASS);
   for (const chip of chips) {
-    const code = chip.closest('code');
-    const text = chip.getAttribute('data-path') || chip.textContent || '';
+    const code = chip.closest("code");
+    const text = chip.getAttribute("data-path") || chip.textContent || "";
     if (code) {
       code.removeAttribute(PATCHED_ATTR);
       code.textContent = text;
-      code.style.background = '';
-      code.style.padding = '';
-      code.style.border = '';
+      code.style.background = "";
+      code.style.padding = "";
+      code.style.border = "";
     } else {
       // Чип не в  — просто подменяем на текстовый узел
       chip.replaceWith(document.createTextNode(text));
@@ -323,25 +468,28 @@ function startWatch() {
   if (started) return;
   started = true;
 
-  safe('file-chip.ensureStyles', () => ensureStyles());
+  safe("file-chip.ensureStyles", () => ensureStyles());
+
+  // Загружаем карту Material-иконок (асинхронно, не блокирует старт).
+  loadMaterialIcons();
 
   // Стартовый прогон (вдруг уже есть отрендеренные ответы)
   scanAll();
 
   // MutationObserver на новые 
-  safe('file-chip.observe', () => {
+  safe("file-chip.observe", () => {
     const mo = new MutationObserver((mutations) => {
       if (!enabled) return;
       for (const m of mutations) {
         for (const n of m.addedNodes) {
           if (n.nodeType !== 1) continue;
-          if (n.tagName === 'CODE') {
+          if (n.tagName === "CODE") {
             patchCodeEl(n);
             continue;
           }
           if (n.querySelectorAll) {
-            for (const code of n.querySelectorAll('code')) {
-              if (code.closest('pre')) continue;
+            for (const code of n.querySelectorAll("code")) {
+              if (code.closest("pre")) continue;
               patchCodeEl(code);
             }
           }
@@ -355,7 +503,7 @@ function startWatch() {
   // Периодический доскан (на случай поздних рендеров и React-перерисовок)
   setInterval(() => scanAll(), 3000);
 
-  console.log('[Cookie Code] file-chip: наблюдение запущено');
+  console.log("[Cookie Code] file-chip: наблюдение запущено");
 }
 
 module.exports = {
@@ -368,6 +516,7 @@ module.exports = {
   isAbsoluteWindowsPath,
   normalizePath,
   resolveToAbsolute,
+  getFileIcon,
   CHIP_CLASS,
   PATCHED_ATTR,
 };
diff --git a/src/preload/dom/response-meta.js b/src/preload/dom/response-meta.js
index 23862de..9f3686d 100644
--- a/src/preload/dom/response-meta.js
+++ b/src/preload/dom/response-meta.js
@@ -9,18 +9,32 @@
  * Данные сохраняются в localStorage (переживают Ctrl+R).
  */
 
-const META_CLASS = 'cuckoo-response-meta';
-const ATTR_MARKED = 'data-cuckoo-meta-marked';
-const STORAGE_KEY = 'cuckoo-response-meta';
+const META_CLASS = "cuckoo-response-meta";
+const ATTR_MARKED = "data-cuckoo-meta-marked";
+const STORAGE_KEY = "cuckoo-response-meta";
 
 let t = (k) => k;
-try { ({ t } = require('../i18n/i18n')); } catch (_) {}
+try {
+  ({ t } = require("../i18n/i18n"));
+} catch (_) {}
 
 let state = null;
-try { state = require('./state'); } catch (_) { state = { showProducedFiles: true }; }
+try {
+  state = require("./state");
+} catch (_) {
+  state = { showProducedFiles: true };
+}
 
 let estimateTokens = (s) => (s ? Math.ceil(s.length / 4) : 0);
-try { ({ estimateTokens } = require('./token-estimator')); } catch (_) {}
+try {
+  ({ estimateTokens } = require("./token-estimator"));
+} catch (_) {}
+
+// Иконка файла (Material Icon Theme) для чипов «Затронуто».
+let getFileIcon = () => "";
+try {
+  ({ getFileIcon } = require("./file-chip"));
+} catch (_) {}
 
 // Активные замеры: messageEl -> { start }
 let activeTimers = new WeakMap();
@@ -37,10 +51,10 @@ function bindServerDeltaListener() {
   if (serverDeltaListenerBound) return;
   serverDeltaListenerBound = true;
   try {
-    window.addEventListener('cuckoo:token-update', (e) => {
+    window.addEventListener("cuckoo:token-update", (e) => {
       try {
         const d = e && e.detail ? e.detail : {};
-        if (typeof d.delta === 'number' && d.delta > 0) {
+        if (typeof d.delta === "number" && d.delta > 0) {
           lastServerDelta = { value: d.delta, at: performance.now() };
         }
       } catch (_) {}
@@ -54,7 +68,8 @@ let confirmedMap = new WeakMap();
 // Глобальный флаг вкл/выкл блока «Затронуто»
 let producedEnabled = true;
 try {
-  if (state && typeof state.showProducedFiles === 'boolean') producedEnabled = state.showProducedFiles !== false;
+  if (state && typeof state.showProducedFiles === "boolean")
+    producedEnabled = state.showProducedFiles !== false;
 } catch (_) {}
 
 /**
@@ -64,30 +79,37 @@ try {
  */
 function setEnabled(on) {
   producedEnabled = on !== false;
-  try { if (state) state.showProducedFiles = producedEnabled; } catch (_) {}
-  if (typeof document === 'undefined') return;
-  const metas = document.querySelectorAll('.' + META_CLASS);
+  try {
+    if (state) state.showProducedFiles = producedEnabled;
+  } catch (_) {}
+  if (typeof document === "undefined") return;
+  const metas = document.querySelectorAll("." + META_CLASS);
   for (const meta of metas) {
-    const produced = meta.querySelector('.cuckoo-response-meta-produced');
-    const sep = meta.querySelector('.cuckoo-response-meta-sep-files');
+    const produced = meta.querySelector(".cuckoo-response-meta-produced");
+    const sep = meta.querySelector(".cuckoo-response-meta-sep-files");
     if (!produced) continue;
     if (producedEnabled) {
-      produced.style.display = '';
-      if (sep) sep.style.display = '';
+      produced.style.display = "";
+      if (sep) sep.style.display = "";
     } else {
-      produced.style.display = 'none';
-      if (sep) sep.style.display = 'none';
+      produced.style.display = "none";
+      if (sep) sep.style.display = "none";
     }
   }
   // При включении — попробуем восстановить из стораджа для сообщений без чипов
   if (producedEnabled) {
-    try { restoreFromStorage(); } catch (_) {}
+    try {
+      restoreFromStorage();
+    } catch (_) {}
   }
 }
 
 function isProducedEnabled() {
-  if (typeof producedEnabled === 'boolean') return producedEnabled;
-  try { if (state && typeof state.showProducedFiles === 'boolean') return state.showProducedFiles !== false; } catch (_) {}
+  if (typeof producedEnabled === "boolean") return producedEnabled;
+  try {
+    if (state && typeof state.showProducedFiles === "boolean")
+      return state.showProducedFiles !== false;
+  } catch (_) {}
   return true;
 }
 
@@ -96,10 +118,13 @@ function isProducedEnabled() {
  */
 function getMessageKey(messageEl) {
   try {
-    const md = messageEl.querySelector('.ds-markdown');
+    const md = messageEl.querySelector(".ds-markdown");
     if (!md) return null;
-    const norm = String(md.textContent || '').replace(/\s+/g, ' ').trim().slice(0, 200);
-    return norm + '|' + norm.length;
+    const norm = String(md.textContent || "")
+      .replace(/\s+/g, " ")
+      .trim()
+      .slice(0, 200);
+    return norm + "|" + norm.length;
   } catch (_) {
     return null;
   }
@@ -109,7 +134,9 @@ function readMetaStore() {
   try {
     const raw = localStorage.getItem(STORAGE_KEY);
     return raw ? JSON.parse(raw) : {};
-  } catch (_) { return {}; }
+  } catch (_) {
+    return {};
+  }
 }
 
 function writeMetaStore(store) {
@@ -133,22 +160,25 @@ function startTimer(messageEl) {
 
 function escapeHtml(s) {
   return String(s)
-    .replace(/&/g, '&')
-    .replace(//g, '>')
-    .replace(/"/g, '"');
+    .replace(/&/g, "&")
+    .replace(//g, ">")
+    .replace(/"/g, """);
 }
 
 function shortFileName(filePath) {
   try {
-    const norm = String(filePath).replace(/\\/g, '/');
-    const parts = norm.split('/').filter(Boolean);
+    const norm = String(filePath).replace(/\\/g, "/");
+    const parts = norm.split("/").filter(Boolean);
     // Убираем букву диска (D:) из отображения
-    const filtered = parts.length > 0 && /^[A-Za-z]:$/.test(parts[0]) ? parts.slice(1) : parts;
+    const filtered =
+      parts.length > 0 && /^[A-Za-z]:$/.test(parts[0]) ? parts.slice(1) : parts;
     if (filtered.length === 0) return filePath;
-    if (filtered.length <= 2) return filtered.join('/');
-    return filtered.slice(-2).join('/');
-  } catch (_) { return filePath; }
+    if (filtered.length <= 2) return filtered.join("/");
+    return filtered.slice(-2).join("/");
+  } catch (_) {
+    return filePath;
+  }
 }
 
 /**
@@ -158,14 +188,14 @@ function shortFileName(filePath) {
  * @returns {{path:string, op:string, status:string}}
  */
 function normalizeFileEntry(entry) {
-  if (typeof entry === 'string') {
-    return { path: entry, op: 'unknown', status: 'pending' };
+  if (typeof entry === "string") {
+    return { path: entry, op: "unknown", status: "pending" };
   }
-  if (entry && typeof entry.path === 'string') {
+  if (entry && typeof entry.path === "string") {
     return {
       path: String(entry.path),
-      op: entry.op || 'unknown',
-      status: entry.status || 'pending'
+      op: entry.op || "unknown",
+      status: entry.status || "pending",
     };
   }
   return null;
@@ -184,13 +214,14 @@ function denormalizeForStore(files) {
 function extractFilesFromCode(code) {
   const out = [];
   const seen = new Set();
-  if (!code || typeof code !== 'string') return out;
-  const re1 = /await\s+(write|edit|writeFile|editFile|deleteFile)\s*\(\s*["'`]([^"'`]+)["'`]/g;
+  if (!code || typeof code !== "string") return out;
+  const re1 =
+    /await\s+(write|edit|writeFile|editFile|deleteFile)\s*\(\s*["'`]([^"'`]+)["'`]/g;
   let m;
   while ((m = re1.exec(code)) !== null) {
-    const op = (m[1] || 'unknown').trim();
-    const raw = (m[2] || '').trim();
-    if (!raw || raw.length > 500 || raw.includes('...')) continue;
+    const op = (m[1] || "unknown").trim();
+    const raw = (m[2] || "").trim();
+    if (!raw || raw.length > 500 || raw.includes("...")) continue;
     if (seen.has(raw)) continue;
     seen.add(raw);
     out.push({ path: raw, op });
@@ -199,11 +230,11 @@ function extractFilesFromCode(code) {
   if (/await\s+(write|edit|writeFile|editFile|deleteFile)\s*\(/.test(code)) {
     const re2 = /["']file_path["']\s*:\s*["'`]([^"'`]+)["'`]/g;
     while ((m = re2.exec(code)) !== null) {
-      const raw = (m[1] || '').trim();
-      if (!raw || raw.length > 500 || raw.includes('...')) continue;
+      const raw = (m[1] || "").trim();
+      if (!raw || raw.length > 500 || raw.includes("...")) continue;
       if (seen.has(raw)) continue;
       seen.add(raw);
-      out.push({ path: raw, op: 'write' });
+      out.push({ path: raw, op: "write" });
     }
   }
   return out;
@@ -222,32 +253,33 @@ function extractFileEntries(messageEl) {
   const files = [];
   const seen = new Set();
   try {
-    const pres = messageEl.querySelectorAll('pre');
+    const pres = messageEl.querySelectorAll("pre");
     for (const pre of pres) {
-      const code = pre.textContent || '';
+      const code = pre.textContent || "";
       if (!code) continue;
       const entries = extractFilesFromCode(code);
       for (const e of entries) {
         if (seen.has(e.path)) continue;
         seen.add(e.path);
-        files.push({ path: e.path, op: e.op, status: 'pending' });
+        files.push({ path: e.path, op: e.op, status: "pending" });
       }
     }
     // Fallback: если 
 ещё не отрендерился, ищем прямо в тексте сообщения
     if (files.length === 0) {
       try {
-        const md = messageEl.querySelector('.ds-markdown');
-        const text = md ? (md.textContent || '') : (messageEl.textContent || '');
+        const md = messageEl.querySelector(".ds-markdown");
+        const text = md ? md.textContent || "" : messageEl.textContent || "";
         if (text) {
-          const reText = /await\s+(write|edit|writeFile|editFile|deleteFile)\s*\(\s*["'`]([^"'`]+)["'`]/g;
+          const reText =
+            /await\s+(write|edit|writeFile|editFile|deleteFile)\s*\(\s*["'`]([^"'`]+)["'`]/g;
           let m;
           while ((m = reText.exec(text)) !== null) {
-            const op = (m[1] || 'unknown').trim();
-            const raw = (m[2] || '').trim();
-            if (!raw || raw.length > 500 || raw.includes('...')) continue;
+            const op = (m[1] || "unknown").trim();
+            const raw = (m[2] || "").trim();
+            if (!raw || raw.length > 500 || raw.includes("...")) continue;
             if (seen.has(raw)) continue;
             seen.add(raw);
-            files.push({ path: raw, op, status: 'pending' });
+            files.push({ path: raw, op, status: "pending" });
             if (files.length >= 20) break;
           }
         }
@@ -256,38 +288,44 @@ function extractFileEntries(messageEl) {
     // Fallback: JSON toolCall формат вне 
 (если AI использует JSON вместо cuckoo)
     if (files.length === 0) {
       try {
-        const md = messageEl.querySelector('.ds-markdown');
-        const text = md ? (md.textContent || '') : (messageEl.textContent || '');
-        if (text && /"(write|edit|file_write|file_edit)"/.test(text) && text.includes('file_path')) {
-          const reJson1 = /"toolName"\s*:\s*"(write|edit|file_write|file_edit|writeFile|editFile)"[^}]*?"file_path"\s*:\s*"([^"]+)"/g;
-          const reJson2 = /"file_path"\s*:\s*"([^"]+)"[^}]*?"toolName"\s*:\s*"(write|edit|file_write|file_edit)"/g;
+        const md = messageEl.querySelector(".ds-markdown");
+        const text = md ? md.textContent || "" : messageEl.textContent || "";
+        if (
+          text &&
+          /"(write|edit|file_write|file_edit)"/.test(text) &&
+          text.includes("file_path")
+        ) {
+          const reJson1 =
+            /"toolName"\s*:\s*"(write|edit|file_write|file_edit|writeFile|editFile)"[^}]*?"file_path"\s*:\s*"([^"]+)"/g;
+          const reJson2 =
+            /"file_path"\s*:\s*"([^"]+)"[^}]*?"toolName"\s*:\s*"(write|edit|file_write|file_edit)"/g;
           let m;
           while ((m = reJson1.exec(text)) !== null) {
-            const op = (m[1] || 'write').trim();
-            const raw = (m[2] || '').trim();
-            if (!raw || raw.length > 500 || raw.includes('...')) continue;
+            const op = (m[1] || "write").trim();
+            const raw = (m[2] || "").trim();
+            if (!raw || raw.length > 500 || raw.includes("...")) continue;
             if (seen.has(raw)) continue;
             seen.add(raw);
-            files.push({ path: raw, op, status: 'pending' });
+            files.push({ path: raw, op, status: "pending" });
           }
           while ((m = reJson2.exec(text)) !== null) {
-            const raw = (m[1] || '').trim();
-            const op = (m[2] || 'write').trim();
-            if (!raw || raw.length > 500 || raw.includes('...')) continue;
+            const raw = (m[1] || "").trim();
+            const op = (m[2] || "write").trim();
+            if (!raw || raw.length > 500 || raw.includes("...")) continue;
             if (seen.has(raw)) continue;
             seen.add(raw);
-            files.push({ path: raw, op, status: 'pending' });
+            files.push({ path: raw, op, status: "pending" });
           }
           if (files.length === 0) {
             const reAny = /"file_path"\s*:\s*"([^"]+)"/g;
             while ((m = reAny.exec(text)) !== null) {
-              const raw = (m[1] || '').trim();
-              if (!raw || raw.length > 500 || raw.includes('...')) continue;
+              const raw = (m[1] || "").trim();
+              if (!raw || raw.length > 500 || raw.includes("...")) continue;
               if (seen.has(raw)) continue;
               const ctx = text.slice(Math.max(0, m.index - 200), m.index + 200);
               if (!/(write|edit|file_write|file_edit)/.test(ctx)) continue;
               seen.add(raw);
-              files.push({ path: raw, op: 'write', status: 'pending' });
+              files.push({ path: raw, op: "write", status: "pending" });
             }
           }
         }
@@ -305,7 +343,7 @@ function extractFileEntries(messageEl) {
  */
 function extractAffectedFiles(messageEl) {
   const entries = extractFileEntries(messageEl);
-  return entries.map(e => e.path);
+  return entries.map((e) => e.path);
 }
 
 /**
@@ -316,16 +354,19 @@ function extractAffectedFiles(messageEl) {
  */
 function setProducedFiles(messageEl, files) {
   if (!messageEl) return;
-  const normalized = (Array.isArray(files) ? files : []).map(normalizeFileEntry).filter(Boolean).slice(0, 20);
+  const normalized = (Array.isArray(files) ? files : [])
+    .map(normalizeFileEntry)
+    .filter(Boolean)
+    .slice(0, 20);
   confirmedMap.set(messageEl, normalized);
   // Обновляем UI если мета уже отрендерена
   try {
-    const meta = messageEl.querySelector('.' + META_CLASS);
+    const meta = messageEl.querySelector("." + META_CLASS);
     if (meta) {
       // Удаляем старую мету и перерисуем с подтверждёнными данными
-      const secondsEl = meta.querySelector('.cuckoo-response-meta-item span');
+      const secondsEl = meta.querySelector(".cuckoo-response-meta-item span");
       // Пытаемся достать seconds/tokens из текущей меты или из стора
-      let seconds = '0.0';
+      let seconds = "0.0";
       let tokens = 0;
       try {
         const key = getMessageKey(messageEl);
@@ -370,11 +411,16 @@ function recordExecutionResults(messageEl, results) {
   const toConfirm = [];
   const seen = new Set();
   for (const item of results) {
-    if (!item || typeof item.code !== 'string') continue;
+    if (!item || typeof item.code !== "string") continue;
     const entries = extractFilesFromCode(item.code);
     // Если в коде нет файлового вызова, но результат success — пропускаем
     if (entries.length === 0) continue;
-    const status = item.result && item.result.denied ? 'denied' : (item.result && item.result.success ? 'success' : 'error');
+    const status =
+      item.result && item.result.denied
+        ? "denied"
+        : item.result && item.result.success
+          ? "success"
+          : "error";
     // Только успешные считаем «затронутыми» — по требованию фильтровать success:true
     // Но храним и error для отображения красным (опционально). Фильтруем denied/error как не успешные для основного списка,
     // но сохраняем чтобы UI мог показать ошибку если нужно.
@@ -382,20 +428,20 @@ function recordExecutionResults(messageEl, results) {
       if (seen.has(e.path)) continue;
       seen.add(e.path);
       // Если статус не success, не добавляем в «затронуто» (только успешные)
-      if (status !== 'success') continue;
+      if (status !== "success") continue;
       toConfirm.push({ path: e.path, op: e.op, status });
     }
   }
   if (toConfirm.length > 0) {
     // Мерджим с уже подтверждёнными
     const existing = confirmedMap.get(messageEl) || [];
-    const mergedMap = new Map(existing.map(f => [f.path, f]));
+    const mergedMap = new Map(existing.map((f) => [f.path, f]));
     for (const f of toConfirm) {
       if (!mergedMap.has(f.path)) mergedMap.set(f.path, f);
       else {
         // Обновляем статус если был pending
         const prev = mergedMap.get(f.path);
-        if (prev.status === 'pending') mergedMap.set(f.path, f);
+        if (prev.status === "pending") mergedMap.set(f.path, f);
       }
     }
     const merged = Array.from(mergedMap.values()).slice(0, 20);
@@ -411,22 +457,40 @@ function recordExecutionResults(messageEl, results) {
  * @param {{success?:boolean, denied?:boolean}} result
  */
 function recordSingleToolCall(messageEl, toolName, params, result) {
-  if (!messageEl || !params || typeof params.file_path !== 'string') return;
+  if (!messageEl || !params || typeof params.file_path !== "string") return;
   const filePath = String(params.file_path).trim();
-  if (!filePath || filePath.length > 500 || filePath.includes('...')) return;
-  const status = result && result.denied ? 'denied' : (result && result.success ? 'success' : 'error');
-  if (status !== 'success') return; // показываем только успешные
-  const opMap = { file_write: 'write', file_edit: 'edit', file_delete: 'deleteFile' };
+  if (!filePath || filePath.length > 500 || filePath.includes("...")) return;
+  const status =
+    result && result.denied
+      ? "denied"
+      : result && result.success
+        ? "success"
+        : "error";
+  if (status !== "success") return; // показываем только успешные
+  const opMap = {
+    file_write: "write",
+    file_edit: "edit",
+    file_delete: "deleteFile",
+  };
   const op = opMap[toolName] || toolName;
-  const allowedOps = ['write', 'edit', 'writeFile', 'editFile', 'deleteFile', 'file_write', 'file_edit', 'file_delete'];
+  const allowedOps = [
+    "write",
+    "edit",
+    "writeFile",
+    "editFile",
+    "deleteFile",
+    "file_write",
+    "file_edit",
+    "file_delete",
+  ];
   if (!allowedOps.includes(toolName) && !allowedOps.includes(op)) return;
   const entry = { path: filePath, op, status };
   const existing = confirmedMap.get(messageEl) || [];
-  const mergedMap = new Map(existing.map(f => [f.path, f]));
+  const mergedMap = new Map(existing.map((f) => [f.path, f]));
   if (!mergedMap.has(entry.path)) mergedMap.set(entry.path, entry);
   else {
     const prev = mergedMap.get(entry.path);
-    if (prev.status === 'pending') mergedMap.set(entry.path, entry);
+    if (prev.status === "pending") mergedMap.set(entry.path, entry);
   }
   setProducedFiles(messageEl, Array.from(mergedMap.values()).slice(0, 20));
 }
@@ -436,12 +500,12 @@ function recordSingleToolCall(messageEl, toolName, params, result) {
  */
 function renderMetaPanel(messageEl, seconds, tokensEstimate, files) {
   if (!messageEl) return;
-  if (messageEl.getAttribute(ATTR_MARKED) === '1') return;
-  const markdown = messageEl.querySelector('.ds-markdown');
+  if (messageEl.getAttribute(ATTR_MARKED) === "1") return;
+  const markdown = messageEl.querySelector(".ds-markdown");
   const anchor = markdown || messageEl;
   if (!anchor.parentElement) return;
-  if (messageEl.querySelector('.' + META_CLASS)) {
-    messageEl.setAttribute(ATTR_MARKED, '1');
+  if (messageEl.querySelector("." + META_CLASS)) {
+    messageEl.setAttribute(ATTR_MARKED, "1");
     return;
   }
 
@@ -455,105 +519,158 @@ function renderMetaPanel(messageEl, seconds, tokensEstimate, files) {
   } catch (_) {}
 
   // Нормализуем к объектам
-  const normalized = (Array.isArray(effectiveFiles) ? effectiveFiles : []).map(normalizeFileEntry).filter(Boolean).slice(0, 20);
+  const normalized = (Array.isArray(effectiveFiles) ? effectiveFiles : [])
+    .map(normalizeFileEntry)
+    .filter(Boolean)
+    .slice(0, 20);
   // Фильтруем только успешные для отображения (pending тоже показываем, т.к. ещё не подтверждено но уже спарсено)
   // Показываем все, но denied/error можно подсветить
   const toRender = normalized;
   const hasFiles = toRender.length > 0 && isProducedEnabled();
 
-  const meta = document.createElement('div');
+  const meta = document.createElement("div");
   meta.className = META_CLASS;
   // i18n с фолбэком на русский, если t() недоступен
-  let timeTitle = 'Время ответа';
-  let tokensTitle = 'Токены ответа (серверные, при недоступности — оценка chars / 4)';
-  let producedLabel = 'Затронуто';
-  let producedTitle = 'Файлы, затронутые за этот ответ';
+  let timeTitle = "Время ответа";
+  let tokensTitle =
+    "Токены ответа (серверные, при недоступности — оценка chars / 4)";
+  let producedLabel = "Затронуто";
+  let producedTitle = "Файлы, затронутые за этот ответ";
   try {
-    const tt = t('meta.time.title'); if (tt && tt !== 'meta.time.title') timeTitle = tt;
-    const tk = t('meta.tokens.title'); if (tk && tk !== 'meta.tokens.title') tokensTitle = tk;
-    const pl = t('meta.produced'); if (pl && pl !== 'meta.produced') producedLabel = pl;
-    const pt = t('meta.produced.title'); if (pt && pt !== 'meta.produced.title') producedTitle = pt;
+    const tt = t("meta.time.title");
+    if (tt && tt !== "meta.time.title") timeTitle = tt;
+    const tk = t("meta.tokens.title");
+    if (tk && tk !== "meta.tokens.title") tokensTitle = tk;
+    const pl = t("meta.produced");
+    if (pl && pl !== "meta.produced") producedLabel = pl;
+    const pt = t("meta.produced.title");
+    if (pt && pt !== "meta.produced.title") producedTitle = pt;
   } catch (_) {}
   let html =
-    '' +
+    '' +
     '  ' +
     '    ' +
     '    ' +
-    '  ' +
-    '  ' + seconds + 's' +
-    '' +
+    "  " +
+    "  " +
+    seconds +
+    "s" +
+    "" +
     '·' +
-    '' +
+    '' +
     '  ' +
     '    ' +
     '    ' +
-    '  ' +
-    '  ~' + tokensEstimate + ' tok' +
-    '';
+    "  " +
+    "  ~" +
+    tokensEstimate +
+    " tok" +
+    "";
 
   if (hasFiles) {
     html +=
       '·' +
-      '' +
-      '  ' + escapeHtml(producedLabel) + '';
+      '' +
+      '  ' +
+      escapeHtml(producedLabel) +
+      "";
     for (const f of toRender) {
       const short = shortFileName(f.path);
-      const status = f.status || 'pending';
-      const opTitle = f.op !== 'unknown' ? f.op + ': ' + f.path : f.path;
-      html += '' +
-              '#' +
-              '' + escapeHtml(short) + '' +
-              '';
+      const status = f.status || "pending";
+      const opTitle = f.op !== "unknown" ? f.op + ": " + f.path : f.path;
+      html +=
+        '' +
+        '' +
+        getFileIcon(f.path) +
+        "" +
+        '' +
+        escapeHtml(short) +
+        "" +
+        "";
     }
-    html += '';
+    html += "";
   }
 
   meta.innerHTML = html;
 
   // Клики по чипам → открыть файл в системе (как file-chip)
   if (hasFiles) {
-    const chips = meta.querySelectorAll('.cuckoo-produced-chip');
+    const chips = meta.querySelectorAll(".cuckoo-produced-chip");
     for (const chip of chips) {
-      const rawPath = chip.getAttribute('data-path') || '';
+      const rawPath = chip.getAttribute("data-path") || "";
       const onActivate = (e) => {
         e.preventDefault();
         e.stopPropagation();
         try {
-          if (!window.electronAPI || typeof window.electronAPI.openPath !== 'function') {
-            console.warn('[Cookie Code] openPath not available');
+          if (
+            !window.electronAPI ||
+            typeof window.electronAPI.openPath !== "function"
+          ) {
+            console.warn("[Cookie Code] openPath not available");
             return;
           }
-          let target = rawPath.replace(/\//g, '\\');
+          let target = rawPath.replace(/\//g, "\\");
           const isAbs = /^[A-Za-z]:[\\/]/.test(target) || /^\\\\/.test(target);
           if (!isAbs) {
             let projectDir = null;
             try {
-              const stateInner = require('./state');
+              const stateInner = require("./state");
               projectDir = stateInner.currentProjectDir || null;
             } catch (_) {}
             if (projectDir) {
-              const sep = projectDir.includes('\\') ? '\\' : '/';
-              const rel = target.replace(/^\.\\/, '').replace(/^\.\.\\/, '');
-              target = projectDir.replace(/[\\/]+$/, '') + sep + rel.replace(/^[\\/]+/, '');
+              const sep = projectDir.includes("\\") ? "\\" : "/";
+              const rel = target.replace(/^\.\\/, "").replace(/^\.\.\\/, "");
+              target =
+                projectDir.replace(/[\\/]+$/, "") +
+                sep +
+                rel.replace(/^[\\/]+/, "");
             } else {
-              console.warn('[Cookie Code] Produced chip: no projectDir to resolve ' + rawPath);
+              console.warn(
+                "[Cookie Code] Produced chip: no projectDir to resolve " +
+                  rawPath,
+              );
               return;
             }
           }
-          window.electronAPI.openPath(target).then((res) => {
-            if (res && !res.success) console.warn('[Cookie Code] Produced chip open failed: ' + (res.error || 'unknown'));
-          }).catch((err) => console.warn('[Cookie Code] Produced chip error: ' + err.message));
+          window.electronAPI
+            .openPath(target)
+            .then((res) => {
+              if (res && !res.success)
+                console.warn(
+                  "[Cookie Code] Produced chip open failed: " +
+                    (res.error || "unknown"),
+                );
+            })
+            .catch((err) =>
+              console.warn("[Cookie Code] Produced chip error: " + err.message),
+            );
         } catch (err) {
-          console.warn('[Cookie Code] Produced chip error: ' + err.message);
+          console.warn("[Cookie Code] Produced chip error: " + err.message);
         }
       };
-      chip.addEventListener('click', onActivate);
-      chip.addEventListener('keydown', (e) => { if (e.key === 'Enter' || e.key === ' ') onActivate(e); });
+      chip.addEventListener("click", onActivate);
+      chip.addEventListener("keydown", (e) => {
+        if (e.key === "Enter" || e.key === " ") onActivate(e);
+      });
     }
   }
 
   anchor.parentElement.insertBefore(meta, anchor.nextSibling);
-  messageEl.setAttribute(ATTR_MARKED, '1');
+  messageEl.setAttribute(ATTR_MARKED, "1");
 }
 
 /**
@@ -561,20 +678,23 @@ function renderMetaPanel(messageEl, seconds, tokensEstimate, files) {
  */
 function finishTimer(messageEl) {
   if (!messageEl) return;
-  if (messageEl.getAttribute(ATTR_MARKED) === '1') return;
+  if (messageEl.getAttribute(ATTR_MARKED) === "1") return;
 
   const rec = activeTimers.get(messageEl);
-  const elapsedMs = rec ? (performance.now() - rec.start) : 0;
+  const elapsedMs = rec ? performance.now() - rec.start : 0;
   activeTimers.delete(messageEl);
 
-  const markdown = messageEl.querySelector('.ds-markdown');
-  const text = markdown ? (markdown.textContent || '') : '';
+  const markdown = messageEl.querySelector(".ds-markdown");
+  const text = markdown ? markdown.textContent || "" : "";
   // Токены ответа: приоритет — серверная дельта accumulated_token_usage,
   // если она пришла во время этого замера (после rec.start).
   // Иначе — локальная оценка (CJK 0.6 / ASCII 0.3).
   let tokensEstimate = estimateTokens(text);
   try {
-    if (lastServerDelta.value > 0 && (!rec || lastServerDelta.at >= rec.start)) {
+    if (
+      lastServerDelta.value > 0 &&
+      (!rec || lastServerDelta.at >= rec.start)
+    ) {
       tokensEstimate = lastServerDelta.value;
     }
   } catch (_) {}
@@ -600,7 +720,11 @@ function finishTimer(messageEl) {
     const key = getMessageKey(messageEl);
     if (key) {
       const store = readMetaStore();
-      store[key] = { seconds: parseFloat(seconds), tokens: tokensEstimate, files: files };
+      store[key] = {
+        seconds: parseFloat(seconds),
+        tokens: tokensEstimate,
+        files: files,
+      };
       writeMetaStore(store);
     }
   } catch (_) {}
@@ -611,8 +735,8 @@ function finishTimer(messageEl) {
  */
 function isAIMessage(el) {
   if (!el || !el.classList) return false;
-  if (!el.classList.contains('ds-message')) return false;
-  if (el.classList.contains('d29f3d7d')) return false;
+  if (!el.classList.contains("ds-message")) return false;
+  if (el.classList.contains("d29f3d7d")) return false;
   return true;
 }
 
@@ -620,7 +744,7 @@ function isAIMessage(el) {
  * Найти последнее AI-сообщение.
  */
 function findLatestAIMessage() {
-  const all = document.querySelectorAll('.ds-message');
+  const all = document.querySelectorAll(".ds-message");
   for (let i = all.length - 1; i >= 0; i--) {
     if (isAIMessage(all[i])) return all[i];
   }
@@ -637,7 +761,8 @@ const STORE_CACHE_TTL = 2000;
 
 function getCachedStore() {
   const now = Date.now();
-  if (cachedStore && (now - cachedStoreTime) < STORE_CACHE_TTL) return cachedStore;
+  if (cachedStore && now - cachedStoreTime < STORE_CACHE_TTL)
+    return cachedStore;
   cachedStore = readMetaStore();
   cachedStoreTime = now;
   return cachedStore;
@@ -651,11 +776,11 @@ function invalidateStoreCache() {
 function restoreFromStorage() {
   const store = getCachedStore();
   if (!store || Object.keys(store).length === 0) return;
-  const all = document.querySelectorAll('.ds-message');
+  const all = document.querySelectorAll(".ds-message");
   for (let i = 0; i < all.length; i++) {
     const el = all[i];
     if (!isAIMessage(el)) continue;
-    if (el.getAttribute(ATTR_MARKED) === '1') continue;
+    if (el.getAttribute(ATTR_MARKED) === "1") continue;
     const key = getMessageKey(el);
     if (!key) continue;
     const rec = store[key];
@@ -669,24 +794,32 @@ function restoreFromStorage() {
         files = null;
       }
       let fresh = null;
-      try { fresh = extractFileEntries(el); } catch (_) { fresh = []; }
+      try {
+        fresh = extractFileEntries(el);
+      } catch (_) {
+        fresh = [];
+      }
       // Если в сторе пусто, но в DOM есть — дополняем
       if (!files || files.length === 0) {
         if (fresh && fresh.length > 0) {
           // Фильтруем только если нет подтверждённых — пока pending
           files = fresh;
           rec.files = fresh;
-          try { writeMetaStore(store); } catch (_) {}
+          try {
+            writeMetaStore(store);
+          } catch (_) {}
         } else {
           files = fresh || [];
         }
       } else if (fresh && fresh.length > files.length) {
         // В DOM появилось больше файлов, чем было сохранено (поздний рендер) — обновляем если нет подтверждённых
-        const hasConfirmed = files.some(f => f.status === 'success');
+        const hasConfirmed = files.some((f) => f.status === "success");
         if (!hasConfirmed) {
           files = fresh;
           rec.files = fresh;
-          try { writeMetaStore(store); } catch (_) {}
+          try {
+            writeMetaStore(store);
+          } catch (_) {}
         }
       }
       // Если есть подтверждённые в confirmedMap — приоритет им
@@ -711,13 +844,13 @@ function startWatch() {
   const run = () => {
     try {
       restoreFromStorage();
-      const all = document.querySelectorAll('.ds-message');
+      const all = document.querySelectorAll(".ds-message");
       for (let i = 0; i < all.length; i++) {
         const el = all[i];
         if (!isAIMessage(el)) continue;
-        if (el.getAttribute(ATTR_MARKED) === '1') continue;
+        if (el.getAttribute(ATTR_MARKED) === "1") continue;
         if (activeTimers.has(el)) continue;
-        if (!el.querySelector('.ds-markdown')) continue;
+        if (!el.querySelector(".ds-markdown")) continue;
         startTimer(el);
       }
     } catch (_) {}
@@ -738,13 +871,17 @@ function startWatch() {
     mo.observe(document.body, { childList: true, subtree: true });
     run();
   } else {
-    document.addEventListener('DOMContentLoaded', () => {
-      const mo = new MutationObserver(runDebounced);
-      mo.observe(document.body, { childList: true, subtree: true });
-      run();
-    }, { once: true });
+    document.addEventListener(
+      "DOMContentLoaded",
+      () => {
+        const mo = new MutationObserver(runDebounced);
+        mo.observe(document.body, { childList: true, subtree: true });
+        run();
+      },
+      { once: true },
+    );
   }
-  console.log('[meta] watch started (debounced mutation)');
+  console.log("[meta] watch started (debounced mutation)");
 }
 
 /**
@@ -753,9 +890,12 @@ function startWatch() {
 function clearStorage() {
   try {
     localStorage.removeItem(STORAGE_KEY);
-    console.log('[Cookie Code] Мета-данные очищены');
+    console.log("[Cookie Code] Мета-данные очищены");
   } catch (err) {
-    console.error('[Cookie Code] Не удалось очистить мета-данные:', err.message);
+    console.error(
+      "[Cookie Code] Не удалось очистить мета-данные:",
+      err.message,
+    );
   }
 }
 
@@ -769,9 +909,15 @@ function resetForTests() {
   cachedStore = null;
   cachedStoreTime = 0;
   producedEnabled = true;
-  try { if (state) state.showProducedFiles = true; } catch (_) {}
-  try { localStorage.removeItem(STORAGE_KEY); } catch (_) {}
-  try { localStorage.removeItem('cuckoo-results'); } catch (_) {}
+  try {
+    if (state) state.showProducedFiles = true;
+  } catch (_) {}
+  try {
+    localStorage.removeItem(STORAGE_KEY);
+  } catch (_) {}
+  try {
+    localStorage.removeItem("cuckoo-results");
+  } catch (_) {}
 }
 
 module.exports = {
diff --git a/src/preload/overlay/template.js b/src/preload/overlay/template.js
index 7fdb60b..228a960 100644
--- a/src/preload/overlay/template.js
+++ b/src/preload/overlay/template.js
@@ -2,1035 +2,1142 @@
  * 覆盖层 UI 模板(HTML 与 CSS)
  * 方向 C:悬浮球模式 —— 右下角悬浮球 + 按需弹出小面板
  */
-const { t } = require('../i18n/i18n');
+const { t } = require("../i18n/i18n");
 // Фон страницы теперь применяется динамически из настроек (см. background.js),
 // через внутренний протокол cuckoo-asset:// — CSS-правило пустое, скрывает
 // только базовый цвет под картинкой.
 
 function buildOverlayHTML() {
   return [
-'
', -'
', -' Cookie Code', -' ', -'
', -'
', -'
', -'
', -' ' + t('overlay.label.currentDir') + '', -' ', -'
', -'
', -' ' + t('overlay.dir.notSelected') + '', -'
', -'
', -'
', -'
', -' ', -'
', -' 0', -' tokens', -'
', -'
', -'
', -'
', -' ', -'
', -'
', -' ', -' ', -' ', -'
', -'
', - '', -'
', -'
', -' ', -'
', -'
', -' ', -'
', -'
', -'
', -'
', -' ' + t('overlay.label.sessions') + '', -' ', -'
', -'
', -'
' + t('overlay.sessions.empty') + '
', -'
', -'
', -'
', -'
', -' ', -'
', -'
', -' ', -' ' + t('overlay.label.delayRange') + '', -' ', -' ms', -'
', -'
', -' ', -'
', -'
', -'
', -' ', -'
' + t('overlay.cmd.none') + '
', -'
', -'
', -' ', -'
', -'
',
-'    
', -'
', -'
', -' ' + t('overlay.label.history') + '', -'
', -' ', -'
', -'
', -'
', -'
', -'
', -'
', -' ' + t('wm.title') + '', -' ', -'
', -'
', -'
', -' ', -'
', -'
', -'
', -'
', -' ' + t('wm.label.list') + '', -' ', -'
', -'
', -'
' + t('wm.empty') + '
', -'
', -'
', -'
', -'
', -'
', -'
', -' ' + t('mcp.title') + '', -' ', -'
', -'
', -'
', -'
', -' ' + t('mcp.label.configured') + '', -' ', -'
', -'
', -'
' + t('mcp.empty') + '
', -'
', -'
', -'
', -' ', -'
', -' ', -'
', -'
', -'
', -'
', -'
', -'
', -' ' + t('diff.title') + '', -'
', -' ', -' ', -'
', -'
', -'
', -' ', -' ', -'
', -'
', -'
', -'
' + t('diff.loading') + '
', -'
', -'
', -'
', -'
', -' ', -' ', -' ', -'
', -'
', -'
', -'
', -'
', -'
', -'
', -' diff', -' ', -'
', -'
', -'
', -'
', -'
', -' Cookie Code', -'
', -' 0/0', -' ', -'
', -'
', -'
', -'
', -'
', -'
', -'', -'
', -' ', -' ', -'
', -'
', -'
', -' ', -'
' + t('firstTime.text') + '
', -'
', -' ', -'
', -'
', -'
', - ].join('\n'); + '
', + '
', + ' Cookie Code', + ' ', + "
", + '
', + '
', + '
', + ' ' + + t("overlay.label.currentDir") + + "", + ' ", + "
", + '
', + ' ' + + t("overlay.dir.notSelected") + + "", + "
", + "
", + '
', + '
', + ' ", + '
', + ' 0', + ' tokens', + "
", + "
", + '
', + '
', + ' ", + "
", + '
', + ' ", + ' ', + ' ', + "
", + '
', + '", + "
", + '
', + ' ", + "
", + '
', + ' ", + "
", + '
', + '
', + '
', + ' ' + + t("overlay.label.sessions") + + "", + ' ", + "
", + '
', + '
' + + t("overlay.sessions.empty") + + "
", + "
", + "
", + '
', + '
', + ' ", + '
', + '
', + ' ', + ' ' + + t("overlay.label.delayRange") + + "", + ' ', + ' ms', + "
", + "
", + ' ", + "
", + '
', + '
', + ' ", + '
' +
+      t("overlay.cmd.none") +
+      "
", + "
", + '
', + ' ", + '
', + '
',
+    "    
", + '
', + '
', + ' ' + + t("overlay.label.history") + + "", + '
', + ' ", + "
", + "
", + "
", + "
", + '
', + '
', + ' ' + t("wm.title") + "", + ' ', + "
", + '
', + '
', + ' ", + "
", + '
', + '
', + '
', + ' ' + t("wm.label.list") + "", + ' ", + "
", + '
', + '
' + t("wm.empty") + "
", + "
", + "
", + "
", + "
", + '
', + '
', + ' ' + t("mcp.title") + "", + ' ', + "
", + '
', + '
', + '
', + ' ' + + t("mcp.label.configured") + + "", + ' ', + "
", + '
', + '
' + t("mcp.empty") + "
", + "
", + "
", + '
', + ' ', + '
', + ' ", + "
", + "
", + "
", + "
", + '
', + '
', + ' ' + t("diff.title") + "", + '
', + ' ', + ' ', + "
", + "
", + '
', + ' ", + ' ", + "
", + '
', + '
', + '
' + t("diff.loading") + "
", + "
", + '
', + '
', + '
', + ' ", + ' ', + ' ", + "
", + '
', + "
", + "
", + "
", + '
', + '
', + ' diff', + ' ', + "
", + '
', + "
", + '
', + '
', + ' Cookie Code', + '
', + ' 0/0', + ' ', + "
", + "
", + '
', + '
', + "
", + "
", + '", + '
', + ' ', + ' ', + "
", + '
', + '
', + ' ', + '
' + t("firstTime.text") + "
", + '
', + ' ", + "
", + "
", + "
", + ].join("\n"); } const OVERLAY_CSS = [ -':root {', -' --ck-bg: rgba(17, 19, 34, 0.97);', -' --ck-surface: rgba(255, 255, 255, 0.04);', -' --ck-surface-hover: rgba(255, 255, 255, 0.08);', -' --ck-border: rgba(255, 255, 255, 0.08);', -' --ck-primary: #8b93ff;', -' --ck-primary-strong: #6d76ff;', -' --ck-text: #dde1ff;', -' --ck-text-dim: #8a90b8;', -' --ck-green: #4ade80;', -' --ck-red: #ff6b7a;', -' --ck-code-bg: rgba(0, 0, 0, 0.35);', -'}', -'.cuckoo-overlay {', -' position: fixed; bottom: 84px; right: 24px; width: var(--cuckoo-overlay-width, 300px); max-height: 60vh;', -' background: var(--cuckoo-overlay-bg, rgba(17, 19, 34, 0.72));', -' backdrop-filter: blur(var(--cuckoo-overlay-blur, 12px)) saturate(140%);', -' -webkit-backdrop-filter: blur(var(--cuckoo-overlay-blur, 12px)) saturate(140%);', -' border: 1px solid var(--ck-border);', -' border-radius: 16px;', -' z-index: 2147483647;', -' will-change: transform;', -' display: flex; flex-direction: column;', -' box-shadow: 0 12px 40px rgba(0, 0, 0, 0.45), 0 0 0 1px rgba(139, 147, 255, 0.06);', -' transition: opacity 0.22s ease, transform 0.22s ease;', -' font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;', -' color: var(--ck-text); font-size: 13px; line-height: 1.55;', -' overflow: hidden;', -'}', -'.cuckoo-overlay.cuckoo-hidden { opacity: 0; pointer-events: none; transform: translateY(8px); }', -'.cuckoo-header {', -' display: flex; align-items: center; justify-content: space-between;', -' padding: 12px 16px 10px; flex-shrink: 0; cursor: grab; user-select: none;', -' border-bottom: 1px solid var(--ck-border);', -' background: rgba(255, 255, 255, 0.04);', -' backdrop-filter: blur(8px);', -' -webkit-backdrop-filter: blur(8px);', -'}', -'.cuckoo-header:active { cursor: grabbing; }', -'.cuckoo-title { font-size: 14px; font-weight: 700; color: #c8ccff; letter-spacing: 0.4px; }', -'.cuckoo-btn-icon {', -' background: var(--ck-surface); border: 1px solid var(--ck-border); color: #8a90b8;', -' cursor: pointer; font-size: 16px; width: 28px; height: 28px; border-radius: 8px;', -' display: flex; align-items: center; justify-content: center; transition: all 0.2s;', -' line-height: 1;', -'}', -'.cuckoo-btn-icon:hover { background: var(--ck-surface-hover); color: #fff; }', -'.cuckoo-body { padding: 12px 14px; overflow-y: auto; flex: 1; display: flex; flex-direction: column; gap: 12px; }', -'.cuckoo-section { display: flex; flex-direction: column; gap: 8px; }', -'.cuckoo-blur-row { display: flex; flex-direction: column; gap: 4px; padding: 10px 12px; background: rgba(255,255,255,0.04); border: 1px solid rgba(255,255,255,0.08); border-radius: 10px; }', -'.cuckoo-blur-label { font-size: 13px; color: #cfd3ff; display: flex; justify-content: space-between; align-items: center; gap: 6px; }', -'.cuckoo-blur-value { font-size: 12px; color: #8b93ff; font-family: "Consolas", monospace; font-weight: 600; }', -'.cuckoo-input { width: 70px; background: rgba(15,18,32,0.6); border: 1px solid rgba(255,255,255,0.1); border-radius: 8px; padding: 5px 8px; color: #dde1ff; font-size: 12px; font-family: "Consolas", monospace; outline: none; transition: border-color 0.18s; }', -'.cuckoo-input:focus { border-color: rgba(139,147,255,0.6); }', + ":root {", + " --ck-bg: rgba(17, 19, 34, 0.97);", + " --ck-surface: rgba(255, 255, 255, 0.04);", + " --ck-surface-hover: rgba(255, 255, 255, 0.08);", + " --ck-border: rgba(255, 255, 255, 0.08);", + " --ck-primary: #8b93ff;", + " --ck-primary-strong: #6d76ff;", + " --ck-text: #dde1ff;", + " --ck-text-dim: #8a90b8;", + " --ck-green: #4ade80;", + " --ck-red: #ff6b7a;", + " --ck-code-bg: rgba(0, 0, 0, 0.35);", + "}", + ".cuckoo-overlay {", + " position: fixed; bottom: 84px; right: 24px; width: var(--cuckoo-overlay-width, 300px); max-height: 60vh;", + " background: var(--cuckoo-overlay-bg, rgba(17, 19, 34, 0.72));", + " backdrop-filter: blur(var(--cuckoo-overlay-blur, 12px)) saturate(140%);", + " -webkit-backdrop-filter: blur(var(--cuckoo-overlay-blur, 12px)) saturate(140%);", + " border: 1px solid var(--ck-border);", + " border-radius: 16px;", + " z-index: 2147483647;", + " will-change: transform;", + " display: flex; flex-direction: column;", + " box-shadow: 0 12px 40px rgba(0, 0, 0, 0.45), 0 0 0 1px rgba(139, 147, 255, 0.06);", + " transition: opacity 0.22s ease, transform 0.22s ease;", + ' font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;', + " color: var(--ck-text); font-size: 13px; line-height: 1.55;", + " overflow: hidden;", + "}", + ".cuckoo-overlay.cuckoo-hidden { opacity: 0; pointer-events: none; transform: translateY(8px); }", + ".cuckoo-header {", + " display: flex; align-items: center; justify-content: space-between;", + " padding: 12px 16px 10px; flex-shrink: 0; cursor: grab; user-select: none;", + " border-bottom: 1px solid var(--ck-border);", + " background: rgba(255, 255, 255, 0.04);", + " backdrop-filter: blur(8px);", + " -webkit-backdrop-filter: blur(8px);", + "}", + ".cuckoo-header:active { cursor: grabbing; }", + ".cuckoo-title { font-size: 14px; font-weight: 700; color: #c8ccff; letter-spacing: 0.4px; }", + ".cuckoo-btn-icon {", + " background: var(--ck-surface); border: 1px solid var(--ck-border); color: #8a90b8;", + " cursor: pointer; font-size: 16px; width: 28px; height: 28px; border-radius: 8px;", + " display: flex; align-items: center; justify-content: center; transition: all 0.2s;", + " line-height: 1;", + "}", + ".cuckoo-btn-icon:hover { background: var(--ck-surface-hover); color: #fff; }", + ".cuckoo-body { padding: 12px 14px; overflow-y: auto; flex: 1; display: flex; flex-direction: column; gap: 12px; }", + ".cuckoo-section { display: flex; flex-direction: column; gap: 8px; }", + ".cuckoo-blur-row { display: flex; flex-direction: column; gap: 4px; padding: 10px 12px; background: rgba(255,255,255,0.04); border: 1px solid rgba(255,255,255,0.08); border-radius: 10px; }", + ".cuckoo-blur-label { font-size: 13px; color: #cfd3ff; display: flex; justify-content: space-between; align-items: center; gap: 6px; }", + '.cuckoo-blur-value { font-size: 12px; color: #8b93ff; font-family: "Consolas", monospace; font-weight: 600; }', + '.cuckoo-input { width: 70px; background: rgba(15,18,32,0.6); border: 1px solid rgba(255,255,255,0.1); border-radius: 8px; padding: 5px 8px; color: #dde1ff; font-size: 12px; font-family: "Consolas", monospace; outline: none; transition: border-color 0.18s; }', + ".cuckoo-input:focus { border-color: rgba(139,147,255,0.6); }", -'.cuckoo-label { font-size: 12px; font-weight: 600; color: #c8ccff; text-transform: uppercase; letter-spacing: 0.6px; cursor: pointer; margin-bottom: 2px; }', -'.cuckoo-cmd-preview {', -' background: rgba(15,18,32,0.6); border: 1px solid rgba(255,255,255,0.1);', -' border-radius: 10px; padding: 10px 12px;', -' font-family: "Cascadia Code", "Fira Code", "Consolas", monospace;', -' font-size: 12px; color: #a7f3c0; line-height: 1.5;', -' max-height: 140px; overflow: auto; white-space: pre-wrap; word-break: break-all; margin: 0;', -'}', -'.cuckoo-actions { display: flex; gap: 8px; width: 100%; flex-wrap: wrap; }', -'.cuckoo-actions .cuckoo-btn { flex: 1 1 auto; min-width: 0; white-space: nowrap; }', -'.cuckoo-btn {', -' flex: 1; padding: 9px 10px; border: 1px solid transparent; border-radius: var(--cuckoo-overlay-btn-radius, 10px);', -' font-size: 12.5px; font-weight: 600; cursor: pointer;', -' transition: all 0.22s ease; letter-spacing: 0.3px;', -' white-space: nowrap; text-overflow: ellipsis; overflow: hidden;', -'}', -'.cuckoo-btn:active { transform: scale(0.98); }', -'.cuckoo-btn-primary {', -' background: var(--cuckoo-overlay-primary-bg, linear-gradient(135deg, #8b93ff, #6d76ff)); color: #fff;', -' box-shadow: 0 6px 18px var(--cuckoo-overlay-primary-shadow, rgba(109, 118, 255, 0.25));', -'}', -'.cuckoo-btn-primary:hover { box-shadow: 0 8px 22px var(--cuckoo-overlay-primary-shadow-hover, rgba(109, 118, 255, 0.4)); transform: translateY(-1px); }', -'.cuckoo-btn-primary:disabled { opacity: 0.5; cursor: not-allowed; transform: none; }', -'.cuckoo-btn-secondary {', -' background: var(--cuckoo-overlay-secondary-bg, rgba(139,147,255,0.12)); color: var(--cuckoo-overlay-secondary-text, #a8afff);', -' border-color: var(--cuckoo-overlay-secondary-border, rgba(139, 147, 255, 0.5));', -'}', -'.cuckoo-btn-secondary:hover { background: var(--cuckoo-overlay-secondary-hover-bg, rgba(139, 147, 255, 0.28)); color: #fff; border-color: var(--cuckoo-overlay-secondary-hover-border, rgba(139, 147, 255, 0.75)); transform: translateY(-1px); }', -'.cuckoo-btn-text {', -' background: none; border: none; color: var(--ck-text-dim); padding: 4px 0;', -' text-align: left; font-size: 11px; cursor: pointer; transition: color 0.2s;', -'}', -'.cuckoo-btn-text:hover { color: var(--ck-red); }', -'.cuckoo-result-status { font-size: 12px; font-weight: 600; padding: 2px 0; }', -'#cuckoo-result-section { background: rgba(255,255,255,0.04); border: 1px solid rgba(255,255,255,0.08); border-radius: 10px; padding: 10px 12px; }', -'.cuckoo-result-status.success { color: var(--ck-green); }', -'.cuckoo-result-status.error { color: var(--ck-red); }', -'.cuckoo-result-output {', -' background: rgba(15,18,32,0.6); border: 1px solid rgba(255,255,255,0.1);', -' border-radius: 10px; padding: 10px 12px;', -' font-family: "Cascadia Code", "Fira Code", "Consolas", monospace;', -' font-size: 11.5px; color: #c8ccff; line-height: 1.5;', -' max-height: 200px; overflow: auto; white-space: pre-wrap; word-break: break-all; margin: 0;', -'}', -'.cuckoo-history-list { max-height: 140px; overflow-y: auto; display: flex; flex-direction: column; gap: 6px; }', -'.cuckoo-history-item {', -' background: rgba(255,255,255,0.04); border: 1px solid rgba(255,255,255,0.08); border-radius: 10px; padding: 8px 10px;', -' font-size: 12px; cursor: pointer; transition: background 0.2s, border-color 0.2s;', -'}', -'.cuckoo-history-item:hover { background: rgba(139, 147, 255, 0.18); border-color: rgba(139,147,255,0.45); }', -'.cuckoo-history-item .cuckoo-cmd-text {', -' font-family: "Consolas", monospace; color: #a7f3c0; font-size: 12px;', -' display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;', -'}', -'.cuckoo-history-item .cuckoo-cmd-status { font-size: 11px; margin-top: 2px; display: block; }', -'.cuckoo-history-item .cuckoo-cmd-status.success { color: var(--ck-green); }', -'.cuckoo-history-item .cuckoo-cmd-status.error { color: var(--ck-red); }', -'.cuckoo-history-item .cuckoo-cmd-time { font-size: 10px; color: #5d6280; margin-top: 2px; display: block; }', -'#cuckoo-mcp-manager { width: 780px; max-width: 90vw; }', -'#cuckoo-mcp-manager .cuckoo-mcp-body { flex-direction: row; gap: 14px; align-items: stretch; overflow: hidden; }', -'.cuckoo-mcp-left { width: 220px; flex-shrink: 0; display: flex; flex-direction: column; }', -'.cuckoo-mcp-left .cuckoo-session-list { flex: 1; max-height: none; overflow-y: auto; }', -'.cuckoo-mcp-right { flex: 1; display: flex; flex-direction: column; gap: 10px; }', -'.cuckoo-mcp-json { flex: 1; min-height: 55vh; resize: vertical; }', -'.cuckoo-mcp-right .cuckoo-btn { flex: 0 0 auto; align-self: flex-start; }', -'.cuckoo-window-manager {', -' position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%);', -' z-index: 2147483647; width: 520px; height: 80vh; max-height: 85vh;', -' background: rgba(17, 19, 34, 0.98);', -' border: 1px solid var(--ck-border); border-radius: 16px;', -' display: flex; flex-direction: column; overflow: hidden;', -' box-shadow: 0 16px 50px rgba(0, 0, 0, 0.55), 0 0 0 1px rgba(139, 147, 255, 0.08);', -' transition: opacity 0.22s ease, transform 0.22s ease;', -'}', -'.cuckoo-window-manager.cuckoo-hidden { opacity: 0; pointer-events: none; transform: translate(-50%, -50%) scale(0.95); }', -'.cuckoo-wm-header {', -' display: flex; align-items: center; justify-content: space-between;', -' padding: 12px 16px 10px; flex-shrink: 0;', -' border-bottom: 1px solid var(--ck-border);', -' background: rgba(255, 255, 255, 0.02);', -'}', -'.cuckoo-wm-body { padding: 12px 14px; overflow: hidden; flex: 1; display: flex; flex-direction: column; gap: 12px; }', -'.cuckoo-window-item {', -' display: flex; align-items: center; justify-content: space-between;', -' background: var(--ck-surface); border: 1px solid var(--ck-border);', -' border-radius: 10px; padding: 10px 12px; cursor: pointer;', -' transition: background 0.2s; font-size: 12px;', -' gap: 8px;', -'}', -'.cuckoo-window-item:hover { background: rgba(139, 147, 255, 0.18); }', -'.cuckoo-window-left { display: flex; align-items: center; gap: 6px; flex: 1; min-width: 0; }', -'.cuckoo-window-item .cuckoo-window-name { color: #c8ccff; font-weight: 600; flex-shrink: 0; }', -'.cuckoo-window-item .cuckoo-window-sep { color: #5d6280; flex-shrink: 0; }', -'.cuckoo-window-item .cuckoo-window-status { color: #8a90b8; font-size: 11px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }', -'.cuckoo-window-del {', -' color: #ff6b7a; cursor: pointer; font-size: 11px; padding: 2px 6px;', -' border-radius: 6px; flex-shrink: 0; transition: all 0.2s;', -'}', -'.cuckoo-window-del:hover { background: rgba(255, 107, 122, 0.18); color: #fff; }', -'.cuckoo-window-item.active { border-color: rgba(139, 147, 255, 0.5); background: rgba(139, 147, 255, 0.12); }', -'#cuckoo-window-list { max-height: none; flex: 1; min-height: 200px; overflow-y: auto; }', -'#cuckoo-window-list .cuckoo-window-item { flex-shrink: 0; }', -'#cuckoo-status-badge {', -' position: fixed; bottom: 24px; right: 24px; z-index: 2147483647;', -' width: 48px; height: 48px; border-radius: 50%;', -' background: rgba(139, 147, 255, 0.22); border: 1px solid rgba(139, 147, 255, 0.4);', -' will-change: transform;', -' display: flex; align-items: center; justify-content: center; gap: 0;', -' cursor: pointer;', -' transition: background 0.2s, border-color 0.2s, transform 0.15s;', -'}', -'#cuckoo-status-badge:hover { background: rgba(139, 147, 255, 0.35); border-color: rgba(139, 147, 255, 0.65); transform: scale(1.05); }', -'#cuckoo-status-dot {', -' position: absolute; top: 6px; right: 6px;', -' width: 9px; height: 9px; border-radius: 50%;', -' background: var(--ck-green); animation: cuckoo-pulse 2s infinite;', -'}', -'.cuckoo-fab-icon {', -' display: flex; align-items: center; justify-content: center;', -' width: 26px; height: 22px;', -' color: #a8afff;', -' user-select: none;', -'}', -'.cuckoo-fab-icon svg {', -' width: 100%; height: 100%;', -' display: block;', -'}', -'.cuckoo-toast {', -' position: fixed; top: 18px; left: 50%; transform: translateX(-50%) translateY(-8px);', -' z-index: 2147483648; min-width: 200px; max-width: 380px; text-align: center;', -' background: rgba(22, 24, 44, 0.92); backdrop-filter: blur(18px);', -' -webkit-backdrop-filter: blur(18px);', -' border: 1px solid rgba(139, 147, 255, 0.28); border-radius: 12px;', -' padding: 10px 16px; font-size: 13px; color: #dde1ff;', -' box-shadow: 0 10px 30px rgba(0, 0, 0, 0.35);', -' opacity: 0; pointer-events: none; transition: opacity 0.22s ease, transform 0.22s ease;', -'}', -'.cuckoo-toast.show { opacity: 1; transform: translateX(-50%) translateY(0); }', -'.cuckoo-task-status {', -' display: inline-flex; align-items: center; gap: 5px;', -' color: #a8afff; font-size: 11px; font-weight: 600;', -'}', -'.cuckoo-btn-kill {', -' margin-left: 6px; padding: 2px 8px;', -' border: 1px solid rgba(255, 107, 122, 0.55); border-radius: 7px;', -' background: rgba(255, 107, 122, 0.16); color: #ff8a95;', -' font-size: 10.5px; font-weight: 700; letter-spacing: 0.3px;', -' cursor: pointer; transition: all 0.2s ease; line-height: 1.4;', -'}', -'.cuckoo-btn-kill:hover { background: rgba(255, 107, 122, 0.32); color: #fff; border-color: rgba(255, 107, 122, 0.9); }', -'.cuckoo-btn-kill:active { transform: scale(0.96); }', -'.cuckoo-btn-kill:disabled { opacity: 0.5; cursor: not-allowed; }', -'.cuckoo-spinner {', -' width: 11px; height: 11px; border-radius: 50%;', -' border: 2px solid rgba(139,147,255,0.25); border-top-color: #8b93ff;', -' animation: cuckoo-spin 0.8s linear infinite;', -'}', -'@keyframes cuckoo-spin { to { transform: rotate(360deg); } }', -'@keyframes cuckoo-pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.35; } }', -'@keyframes cuckoo-attention {', -' 0%, 100% { transform: scale(1); }', -' 10%, 30%, 50%, 70%, 90% { transform: scale(1.08); }', -' 20%, 40%, 60%, 80% { transform: scale(0.98); }', -'}', -'.cuckoo-btn-attention {', -' animation: cuckoo-attention 1s ease-in-out 3;', -' background: rgba(139, 147, 255, 0.35) !important;', -' color: #fff !important;', -' border-color: rgba(139, 147, 255, 0.6) !important;', -'}', -'.cuckoo-hidden { display: none !important; }', -'.cuckoo-home-mode .cuckoo-header .cuckoo-btn-icon,', -'.cuckoo-home-mode .cuckoo-body .cuckoo-section,', -'.cuckoo-home-mode .cuckoo-body .cuckoo-actions:not(:has(#cuckoo-btn-init)),', -'.cuckoo-home-mode .cuckoo-divider {', -' display: none !important;', -'}', -'.cuckoo-overlay ::-webkit-scrollbar { width: 6px; }', -'.cuckoo-overlay ::-webkit-scrollbar-track { background: transparent; }', -'.cuckoo-overlay ::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.15); border-radius: 3px; }', -'.cuckoo-overlay ::-webkit-scrollbar-thumb:hover { background: rgba(255,255,255,0.25); }', -'.cuckoo-project-dir-section {', -' background: rgba(255,255,255,0.04); border-radius: 10px;', -' padding: 10px 12px; border: 1px solid rgba(255,255,255,0.08);', -'}', -'.cuckoo-project-dir-row { display: flex; justify-content: space-between; align-items: center; }', -'.cuckoo-btn-change-dir {', -' background: rgba(139,147,255,0.12); border: 1px solid rgba(139,147,255,0.5);', -' color: #a8afff; padding: 3px 10px; border-radius: 8px;', -' cursor: pointer; font-size: 11px; font-weight: 600; transition: all 0.18s;', -'}', -'.cuckoo-btn-change-dir:hover { background: rgba(139,147,255,0.28); color: #fff; border-color: rgba(139,147,255,0.75); }', -'.cuckoo-project-dir-display { margin-top: 4px; font-size: 12px; font-family: "Consolas", monospace; color: #8a90b8; word-break: break-all; }', -'.cuckoo-dir-path { color: #a7f3c0; }', -'.cuckoo-token-section { background: var(--ck-surface); border-radius: 10px; padding: 8px 10px; border: 1px solid var(--ck-border); }', -'.cuckoo-token-display { display: flex; align-items: baseline; gap: 6px; margin-top: 2px; }', -'.cuckoo-token-count { font-size: 20px; font-weight: 700; color: #a7f3c0; font-family: "Consolas", monospace; }', -'.cuckoo-token-unit { font-size: 11px; color: var(--ck-text-dim); }', -'.cuckoo-divider { border-top: 1px solid var(--ck-border); margin: 2px 0; }', -'.cuckoo-session-section {', -' background: rgba(255,255,255,0.04); border-radius: 10px;', -' padding: 10px 12px; border: 1px solid rgba(255,255,255,0.08);', -'}', -'.cuckoo-session-header { display: flex; justify-content: space-between; align-items: center; }', -'.cuckoo-btn-refresh-sessions {', -' background: rgba(139,147,255,0.12); border: 1px solid rgba(139,147,255,0.5);', -' color: #a8afff; padding: 3px 10px; border-radius: 8px;', -' cursor: pointer; font-size: 11px; font-weight: 600; transition: all 0.18s;', -'}', -'.cuckoo-btn-refresh-sessions:hover { background: rgba(139,147,255,0.28); color: #fff; border-color: rgba(139,147,255,0.75); }', -'.cuckoo-session-list { max-height: 100px; overflow-y: auto; margin-top: 4px; display: flex; flex-direction: column; gap: 4px; }', -'.cuckoo-session-item { background: rgba(255,255,255,0.05); border-radius: 8px; padding: 6px 10px; font-size: 12px; font-family: "Consolas", monospace; color: #ccc; cursor: pointer; transition: background 0.2s; }', -'.cuckoo-session-item:hover { background: rgba(139,147,255,0.2); }', -'.cuckoo-session-item .session-id { color: #a8afff; font-size: 11px; }', -'.cuckoo-session-item .session-action { color: #a7f3c0; font-size: 11px; font-weight: 600; }', -'.cuckoo-session-empty { color: #5d6280; font-size: 12px; font-style: italic; padding: 8px 0; text-align: center; }', -'.cuckoo-first-time-dialog {', -' position: fixed; top: 0; left: 0; right: 0; z-index: 2147483647;', -' display: flex; align-items: flex-start; justify-content: center;', -' padding-top: 24px;', -' pointer-events: none;', -'}', -'.cuckoo-first-time-dialog.cuckoo-hidden { display: none !important; }', -'.cuckoo-first-time-box {', -' position: relative; max-width: 420px; width: 90%;', -' background: rgba(17, 19, 34, 0.98);', -' border: 1px solid rgba(139, 147, 255, 0.35);', -' border-radius: 18px;', -' padding: 28px 24px 22px;', -' box-shadow: 0 20px 60px rgba(0, 0, 0, 0.6), 0 0 0 1px rgba(139, 147, 255, 0.08);', -' color: var(--ck-text); font-size: 14px; line-height: 1.6;', -' text-align: center;', -' pointer-events: auto;', -'}', -'.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; }', -'.cuckoo-first-time-box .cuckoo-btn { flex: 0 0 auto; min-width: 150px; }', -// ========== CSS-переменные (управляются из настроек Cookie Code) ========== -':root {', -' --cuckoo-bg-blur: 0px;', // размытие самой картинки фона -' --cuckoo-header-blur: 12px;', // стекло шапки -' --cuckoo-sidebar-blur: 12px;', // стекло сайдбара -' --cuckoo-header-opacity: 45%;', // плотность фона шапки (0 = прозрачно) -' --cuckoo-sidebar-opacity: 45%;', // плотность фона сайдбара (0 = прозрачно) -' --cuckoo-toolblock-opacity: 55%;', // плотность фона tool-блоков -' --cuckoo-toolblock-blur: 0px;', // стекло tool-блоков -' --cuckoo-input-glass-opacity: 55%;', // плотность фона поля ввода -' --cuckoo-input-glass-blur: 12px;', // стекло поля ввода -'}', -// ========== 页面背景:базовый цвет + стили (картинка ставится через JS) ========== -// При выключенной кастомизации (класс cuckoo-customization-off на ) -// правила не применяются, и фон остаётся родным фоном страницы DeepSeek. -'html:not(.cuckoo-customization-off), body:not(.cuckoo-customization-off) {', -' background-color: #0f1220 !important;', -' background-size: cover !important;', -' background-position: center center !important;', -' background-repeat: no-repeat !important;', -' background-attachment: fixed !important;', -'}', -// ========== Размытие самой фоновой картинки (--cuckoo-bg-blur) ========== -// Отдельный fixed-слой поверх body, чтобы фильтр не затрагивал контент. -'html:not(.cuckoo-customization-off)::before {', -' content: "";', -' position: fixed;', -' inset: 0;', -' background: inherit;', -' background-image: inherit;', -' background-size: cover;', -' background-position: center center;', -' background-attachment: fixed;', -' filter: blur(var(--cuckoo-bg-blur));', -' transform: scale(1.05);', // скрываем размытые края по периметру -' pointer-events: none;', -' z-index: -1;', -'}', -// ========== 页面顶部标题栏毛玻璃(DeepSeek) ========== -// Прозрачность управляется переменной --cuckoo-header-opacity (0% — полностью прозрачно). -'.the-header {', -' backdrop-filter: blur(var(--cuckoo-header-blur)) saturate(140%) !important;', -' -webkit-backdrop-filter: blur(var(--cuckoo-header-blur)) saturate(140%) !important;', -' background: rgba(15, 18, 32, var(--cuckoo-header-opacity)) !important;', -'}', -// ========== 去掉输入框底部的深色渐变,让背景图透出 ========== -// DeepSeek 在 _871cbca 与其空的子层 d72636e2 上画了从上到下的 -// 线性渐变(底部是不透明的 rgb(21,21,23)),会挡住页面背景图。 -// 用简写 background: transparent !important 一次清掉颜色+渐变。 -'._871cbca,', -'._871cbca > .d72636e2 {', -' background: transparent !important;', -'}', -// ========== 左侧边栏毛玻璃(DeepSeek) ========== -// .b8812f16.a2f3d50e — корень сайдбара. Внутри много слоёв, каждый со своим -// фоном (шапка _262baab, список _3586175/_6d215eb/_77cdc67, низ _7b40dad). -// Делаем прозрачными все промежуточные слои, чтобы стекло работало насквозь. -'.b8812f16.a2f3d50e,', -'.b8812f16.a2f3d50e > div,', -'.b8812f16.a2f3d50e > div > div,', -'.b8812f16.a2f3d50e ._262baab,', -'.b8812f16.a2f3d50e ._3586175,', -'.b8812f16.a2f3d50e ._6d215eb,', -'.b8812f16.a2f3d50e ._77cdc67,', -'.b8812f16.a2f3d50e ._7b40dad,', -'.b8812f16.a2f3d50e ._8a693f3,', -'.b8812f16.a2f3d50e ._1d72f01,', -'.b8812f16.a2f3d50e .f3d18f6a,', -'.b8812f16.a2f3d50e ._3098d02 {', -' background: transparent !important;', -' backdrop-filter: none !important;', -' -webkit-backdrop-filter: none !important;', -'}', -// Стекло применяем один раз — на корень сайдбара. -// Прозрачность — переменная --cuckoo-sidebar-opacity (0% — полностью прозрачно). -'.b8812f16.a2f3d50e {', -' background: rgba(15, 18, 32, var(--cuckoo-sidebar-opacity)) !important;', -' backdrop-filter: blur(var(--cuckoo-sidebar-blur)) saturate(140%) !important;', -' -webkit-backdrop-filter: blur(var(--cuckoo-sidebar-blur)) saturate(140%) !important;', -'}', -// ========== 隐藏底部「Сгенерировано ИИ」免责声明 ========== -'._0fcaa63 {', -' display: none !important;', -'}', -// ========== Ошибка «Слишком частые сообщения» (DeepSeek rate limit) — красный текст ========== -// Хэш _1ce76f5 нестабилен, поэтому цепляемся к нему + страховка через :has() -// и любые элементы с этим текстом внутри сообщений. -'._1ce76f5,', -'[class*="_1ce76f5"] {', -' color: #ff6b7a !important;', -' font-weight: 600 !important;', -'}', -// Страховка: любой span внутри сообщения, содержащий «Слишком частые сообщения» -'p:has(> span):has(._1ce76f5),', -'div:has(> ._1ce76f5) {', -' color: #ff6b7a !important;', -'}', -// ========== Модалка «Настройки» (и другие ds-modal) — матовое стекло ========== -'.ds-modal-content {', -' background: rgba(15, 18, 32, 0.55) !important;', -' backdrop-filter: blur(16px) saturate(140%) !important;', -' -webkit-backdrop-filter: blur(16px) saturate(140%) !important;', -'}', -// Приглушаем фон-маску, чтобы под модалкой просвечивала страница -'.ds-modal-mask {', -' background: rgba(0, 0, 0, 0.35) !important;', -'}', -// ========== Матовое стекло для поля ввода сообщения DeepSeek ========== -// Приём тот же, что у плашки «Размышление»: backdrop-filter не работает, -// потому что контейнер инпута лежит внутри виртуализированного списка с -// transform. Поэтому стекло рисуем сами: ::before показывает тот же фон -// страницы, размытый и спозиционированный под вьюпорт; ::after добавляет -// лёгкую затемняющую плёнку. Координаты считает input-glass.js. -// Селекторы из DOM: внешняя обёртка блока ввода _77cefa5 (у неё непрозрачный -// фон), внутренний контейнер _020ab5b, textarea[name="search"]. -// Внешняя обёртка блока ввода — именно у неё непрозрачный фон (белый/тёмный) -// и border-radius 24px. Именно её делаем стеклянной. -// Дополнительно правило отключается классом cuckoo-input-glass-off на -// (чекбокс «Стекло поля ввода включено» в настройках). -'html:not(.cuckoo-customization-off):not(.cuckoo-input-glass-off) ._77cefa5 {', -' position: relative !important;', -' overflow: hidden !important;', -' border-radius: 24px !important;', -' border: 1px solid rgba(139, 147, 255, 0.22) !important;', -' background: transparent !important;', -' backdrop-filter: none !important;', -' -webkit-backdrop-filter: none !important;', -'}', -// Псевдоэлемент рисует тот же фон страницы, размытый (backdrop-filter не -// работает: предки внутри виртуализированного списка с transform). -'html:not(.cuckoo-customization-off):not(.cuckoo-input-glass-off) ._77cefa5::before {', -' content: "" !important;', -' position: absolute !important;', -' inset: calc(-1 * var(--cuckoo-input-pad, 40px)) !important;', -' background-image: var(--cuckoo-bg-image, none) !important;', -' background-size: var(--cuckoo-input-bg-size, cover) !important;', -' background-repeat: no-repeat !important;', -' background-position: var(--cuckoo-input-bg-pos, 0 0) !important;', -' filter: blur(var(--cuckoo-input-glass-blur, 12px)) !important;', -' -webkit-filter: blur(var(--cuckoo-input-glass-blur, 12px)) !important;', -' pointer-events: none !important;', -' z-index: 0 !important;', -'}', -'html:not(.cuckoo-customization-off):not(.cuckoo-input-glass-off) ._77cefa5::after {', -' content: "" !important;', -' position: absolute !important;', -' inset: 0 !important;', -' background: rgba(15, 18, 32, var(--cuckoo-input-glass-opacity, 55%)) !important;', -' pointer-events: none !important;', -' z-index: 0 !important;', -'}', -// Внутренний контейнер ввода — прозрачный, контент поверх стекла. -'html:not(.cuckoo-customization-off):not(.cuckoo-input-glass-off) ._020ab5b {', -' position: relative !important;', -' background: transparent !important;', -' z-index: 1 !important;', -'}', -'html:not(.cuckoo-customization-off):not(.cuckoo-input-glass-off) ._020ab5b textarea[name="search"],', -'html:not(.cuckoo-customization-off):not(.cuckoo-input-glass-off) ._020ab5b ._24fad49 {', -' background: transparent !important;', -'}', -// ========== Кнопка «Новый чат» в сайдбаре — матовое стекло ========== -'._5a8ac7a.a084f19e {', -' background: rgba(15, 18, 32, 0.5) !important;', -' backdrop-filter: blur(10px) saturate(140%) !important;', -' -webkit-backdrop-filter: blur(10px) saturate(140%) !important;', -' border: 1px solid rgba(139, 147, 255, 0.2) !important;', -' border-radius: 12px !important;', -'}', -// ========== Рендер tool-блоков (cuckoo) — как в dsh-web-dev ========== -'.cuckoo-tool-block {', -' margin: 8px 0;', -' border: 1px solid rgba(139, 147, 255, 0.22);', -' border-radius: 10px;', -' background: rgba(15, 18, 32, var(--cuckoo-toolblock-opacity)) !important;', -' backdrop-filter: blur(var(--cuckoo-toolblock-blur)) !important;', -' -webkit-backdrop-filter: blur(var(--cuckoo-toolblock-blur)) !important;', -' overflow: hidden;', - ' min-height: 0 !important;', -' font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;', -'}', -'.cuckoo-tool-block[data-expanded="false"] > .md-code-block {', -' display: none !important;', -' height: 0 !important;', -' min-height: 0 !important;', -'}', -'.cuckoo-tool-header {', -' display: flex; align-items: center; gap: 8px;', -' padding: 8px 12px;', -' cursor: pointer;', -' user-select: none;', -' font-size: 13px;', -' color: #dde1ff;', -' transition: background 0.15s;', -'}', -'.cuckoo-tool-header:hover { background: rgba(139, 147, 255, 0.09); }', -'.cuckoo-tool-chevron {', -' display: inline-block; width: 12px; text-align: center;', -' color: #8a90b8; font-size: 11px;', -'}', -'.cuckoo-tool-icon {', -' display: inline-flex; align-items: center; justify-content: center;', -' width: 16px; height: 16px;', -' color: #a8afff;', -' flex-shrink: 0;', -'}', -'.cuckoo-tool-icon svg {', -' width: 16px !important; height: 16px !important;', -' display: block;', -' stroke: currentColor;', -'}', -'.cuckoo-tool-label { font-weight: 600; color: #a8afff; }', -'.cuckoo-tool-sep { color: #5d6280; }', -'.cuckoo-tool-file {', -' color: #a7f3c0;', -' font-family: "Consolas", "Cascadia Code", monospace;', -' font-size: 12px;', -' text-decoration: underline dashed rgba(167, 243, 192, 0.35);', -' cursor: pointer;', -'}', -'.cuckoo-tool-diff { margin-left: auto; display: inline-flex; align-items: center; gap: 4px; font-family: "Consolas", "Cascadia Code", monospace; font-size: 11px; font-weight: 600; white-space: nowrap; }', -'.cuckoo-diff-add { color: #4ade80; }', -'.cuckoo-diff-del { color: #f87171; }', -'.cuckoo-tool-block[data-expanded="true"] > .md-code-block {', -' display: block !important;', -' border-top: 1px solid rgba(255, 255, 255, 0.06);', -' margin: 0 !important;', -' border-radius: 0 !important;', -'}', -// ========== Стиль ошибки tool-блока ========== -'.cuckoo-tool-block.cuckoo-tool-error {', -' border-color: rgba(255, 107, 122, 0.65) !important;', -' background: rgba(60, 15, 22, 0.7) !important;', -'}', -'.cuckoo-tool-block.cuckoo-tool-error .cuckoo-tool-header {', -' background: rgba(255, 107, 122, 0.1);', -'}', -'.cuckoo-tool-block.cuckoo-tool-error .cuckoo-tool-label {', -' color: #ff8e9b !important;', -'}', -'.cuckoo-tool-block.cuckoo-tool-error .cuckoo-tool-chevron {', -' color: #ff6b7a !important;', -'}', -'.cuckoo-tool-error-icon {', -' color: #ff6b7a;', -' font-size: 14px;', -' margin-left: 4px;', -'}', -'.cuckoo-tool-error-msg {', -' padding: 8px 12px;', -' font-size: 12px;', -' color: #ff9aa5;', -' background: rgba(255, 107, 122, 0.1);', -' border-top: 1px solid rgba(255, 107, 122, 0.3);', -' white-space: pre-wrap;', -' word-break: break-word;', -' font-family: "Consolas", monospace;', -'}', -// Внутренний код-блок внутри tool-блока: фон/стекло по тем же переменным -'.cuckoo-tool-block .md-code-block,', -'.cuckoo-tool-block .md-code-block-banner-wrap,', -'.cuckoo-tool-block .md-code-block-banner,', -'.cuckoo-tool-block pre,', -'.cuckoo-tool-block code {', -' background: rgba(15, 18, 32, var(--cuckoo-toolblock-opacity)) !important;', -' backdrop-filter: blur(var(--cuckoo-toolblock-blur)) !important;', -' -webkit-backdrop-filter: blur(var(--cuckoo-toolblock-blur)) !important;', -'}', -// Сам pre делаем прозрачным поверх фона контейнера — чтобы не дублировать -'.cuckoo-tool-block pre,', -'.cuckoo-tool-block code {', -' background: transparent !important;', -' backdrop-filter: none !important;', -' -webkit-backdrop-filter: none !important;', -'}', -// ========== Мета-информация под ответом AI (время + токены + затронутые файлы) ========== -'.cuckoo-response-meta {', -' display: flex; flex-wrap: wrap; align-items: center; gap: 6px;', -' margin-top: 6px; padding: 3px 8px;', -' font-size: 11px;', -' color: #8a90b8;', -' font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;', -' border-radius: 6px;', -' user-select: none;', -' opacity: 0.85;', -'}', -'.cuckoo-response-meta-item {', -' display: inline-flex; align-items: center; gap: 4px;', -'}', -'.cuckoo-response-meta-item svg { display: block; }', -'.cuckoo-response-meta-sep { color: #5d6280; }', -'.cuckoo-response-meta-produced { display: inline-flex; align-items: center; gap: 6px; flex-wrap: wrap; }', -'.cuckoo-response-meta-produced-label { color: #6b7280; font-size: 11px; font-weight: 400; }', -'.cuckoo-produced-chip {', -' display: inline-flex; align-items: center; gap: 4px;', -' padding: 1px 8px 1px 6px;', -' background: rgb(44, 44, 46);', -' border: 1px solid rgba(255,255,255,0.06);', -' border-radius: 999px;', -' font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;', -' font-size: 11px; line-height: 18px;', -' color: #c8ccff; cursor: pointer;', -' transition: background .12s ease, border-color .12s ease;', -' user-select: none;', -'}', -'.cuckoo-produced-chip:hover { background: rgba(139,147,255,0.18); border-color: rgba(139,147,255,0.45); }', -'.cuckoo-produced-chip:focus-visible { outline: 1px solid rgba(139,147,255,0.6); outline-offset: 1px; }', -'.cuckoo-produced-chip-hash {', -' display: inline-flex; align-items: center; justify-content: center;', -' width: 14px; height: 14px; border-radius: 4px;', -' background: rgba(139,147,255,0.22); color: #a8afff;', -' font-size: 10px; font-weight: 700; line-height: 1;', -'}', -'.cuckoo-produced-chip[data-status="error"] { border-color: rgba(255,107,122,0.45); background: rgba(60,15,22,0.65); }', -'.cuckoo-produced-chip[data-status="error"] .cuckoo-produced-chip-hash { background: rgba(255,107,122,0.28); color: #ff8e9b; }', -'.cuckoo-produced-chip[data-status="denied"] { border-color: rgba(251,191,36,0.45); background: rgba(60,45,15,0.6); }', -'.cuckoo-produced-chip[data-status="denied"] .cuckoo-produced-chip-hash { background: rgba(251,191,36,0.28); color: #fbbf24; }', -'.cuckoo-produced-chip-name { white-space: nowrap; }', -// ========== RGB-переливание ника пользователя ========== -// Переливание включается, если у нет класса `cuckoo-rgb-off`. -// Управляется настройкой rgbUsername из settings.json. -'body:not(.cuckoo-rgb-off) ._9d8da05 {', -' background: linear-gradient(90deg, #ff4d4d, #ffa64d, #ffee4d, #4dff88, #4dd2ff, #8a4dff, #ff4dd2, #ff4d4d);', -' background-size: 400% 100%;', -' -webkit-background-clip: text;', -' background-clip: text;', -' -webkit-text-fill-color: transparent;', -' color: transparent;', -' animation: cuckoo-rgb-username 5s linear infinite;', -' font-weight: 600;', -'}', -'@keyframes cuckoo-rgb-username {', -' 0% { background-position: 0% 50%; }', -' 100% { background-position: 400% 50%; }', -'}', -// ========== RGB-переливание приветственного текста на стартовой странице ========== -// Приветствие — без класса, всегда идёт сразу после SVG с классом .ce41ed1b -// внутри общего контейнера. Цепляемся к этому span через :has(). -'body:not(.cuckoo-rgb-off) :is(._6c7e7df, div:has(> svg.ce41ed1b)) > span {', -' background: linear-gradient(90deg, #ff4d4d, #ffa64d, #ffee4d, #4dff88, #4dd2ff, #8a4dff, #ff4dd2, #ff4d4d);', -' background-size: 400% 100%;', -' -webkit-background-clip: text;', -' background-clip: text;', -' -webkit-text-fill-color: transparent;', -' color: transparent;', -' animation: cuckoo-rgb-username 5s linear infinite;', -'}', -// ========== RGB-переливание кнопки «Повторить» (retry под ошибкой ответа) ========== -// Класс ds-button--warning стабильный (не хешированный). Ограничение .ds-message -// гарантирует, что не затронутся warning-кнопки в других местах (например, в настройках). -'.ds-message .ds-button--warning .ds-button__background,', -'.ds-message .ds-button--warning {', -' background: linear-gradient(90deg, #ff4d4d, #ffa64d, #ffee4d, #4dff88, #4dd2ff, #8a4dff, #ff4dd2, #ff4d4d) !important;', -' background-size: 400% 100% !important;', -' animation: cuckoo-rgb-retry 5s linear infinite !important;', -'}', -'@keyframes cuckoo-rgb-retry {', -' 0% { background-position: 0% 50%; }', -' 100% { background-position: 400% 50%; }', -'}', -// ========== Матовое стекло для плашки «Размышление N секунд» (._5ab5d64) ========== -// backdrop-filter не сработает: между плашкой и body стоят непрозрачные предки, -// а виртуализированный список DeepSeek использует transform (ломает fixed-attachment). -// Поэтому стекло рисуем сами: псевдоэлемент показывает тот же фон, что на странице, -// размытый и спозиционированный под вьюпорт. Координаты считает reasoning-glass.js. -'._5ab5d64 {', -' position: relative !important;', -' overflow: hidden !important;', -' border-radius: 10px !important;', -' border: 1px solid rgba(139, 147, 255, 0.22) !important;', -' background: transparent !important;', -' isolation: auto !important;', -' transform: none !important;', -' backdrop-filter: none !important;', -' -webkit-backdrop-filter: none !important;', -'}', -// Гасим фоны у прямого предка и у потомков плашки: DeepSeek рисует свои -// тёмные слои (обёртка _245c867._34a54ec, иконки, span), и они просвечивают -// через полупрозрачную плашку, давая «чёрную» область слева. -'._245c867._34a54ec,', -'._74c0879,', -'._245c867._34a54ec > *,', -'._5ab5d64 * {', -' background: transparent !important;', -' backdrop-filter: none !important;', -' -webkit-backdrop-filter: none !important;', -'}', -'._5ab5d64::before {', -' content: "" !important;', -' position: absolute !important;', -' inset: calc(-1 * var(--cuckoo-plate-pad, 40px)) !important;', -' background-image: var(--cuckoo-bg-image, none) !important;', -' background-size: var(--cuckoo-plate-bg-size, cover) !important;', -' background-repeat: no-repeat !important;', -' background-position: var(--cuckoo-plate-bg-pos, 0 0) !important;', -' filter: blur(var(--cuckoo-reasoning-blur, 12px)) !important;', -' -webkit-filter: blur(var(--cuckoo-reasoning-blur, 12px)) !important;', -' pointer-events: none !important;', -' z-index: 0 !important;', -'}', -'._5ab5d64::after {', -' content: "" !important;', -' position: absolute !important;', -' inset: 0 !important;', -' background: rgba(15, 18, 32, var(--cuckoo-reasoning-opacity, 35%)) !important;', -' pointer-events: none !important;', -' z-index: 0 !important;', -'}', -'._5ab5d64 > * {', -' position: relative !important;', -' z-index: 1 !important;', -'}', -// ========== Панель «Изменения» (git) ========== -'#cuckoo-diff-panel { width: 560px; max-width: 92vw; height: 72vh; }', -'.cuckoo-diff-body { overflow: hidden; padding: 0 !important; }', -'.cuckoo-diff-list { flex: 1; overflow-y: auto; padding: 10px 12px; display: flex; flex-direction: column; gap: 4px; }', -'.cuckoo-diff-file {', -' display: flex; align-items: center; gap: 8px; padding: 7px 10px;', -' border-radius: 8px; cursor: pointer; transition: background 0.15s;', -' font-size: 12px;', -'}', -'.cuckoo-diff-file:hover { background: rgba(139,147,255,0.15); }', -'.cuckoo-diff-file.active { background: rgba(139,147,255,0.22); }', -'.cuckoo-diff-file-badge {', -' width: 18px; text-align: center; font-weight: 700; font-size: 11px;', -' flex-shrink: 0; font-family: "Consolas", monospace;', -'}', -'.cuckoo-diff-file-badge.modified { color: #ffc107; }', -'.cuckoo-diff-file-badge.added { color: #4ade80; }', -'.cuckoo-diff-file-badge.deleted { color: #ff6b7a; }', -'.cuckoo-diff-file-badge.renamed { color: #8b93ff; }', -'.cuckoo-diff-file-badge.untracked { color: #86efac; }', -'.cuckoo-diff-file-badge.changed { color: #8a90b8; }', -'.cuckoo-diff-file-path {', -' flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;', -' font-family: "Consolas", monospace; color: #c8ccff; direction: rtl; text-align: left;', -'}', -// Вкладки «Изменения» / «История» -'.cuckoo-diff-tabs {', -' display: flex; gap: 4px; padding: 8px 12px 0; flex-shrink: 0;', -' border-bottom: 1px solid var(--ck-border);', -'}', -'.cuckoo-diff-tab {', -' background: none; border: none; color: #8a90b8; font-size: 12px; font-weight: 600;', -' padding: 6px 12px; cursor: pointer; border-bottom: 2px solid transparent;', -' transition: color 0.15s, border-color 0.15s;', -'}', -'.cuckoo-diff-tab:hover { color: #c8ccff; }', -'.cuckoo-diff-tab.active { color: #a8afff; border-bottom-color: #8b93ff; }', -'.cuckoo-commit-item {', -' display: flex; flex-direction: column; gap: 2px; padding: 8px 10px;', -' border-radius: 8px; cursor: pointer; transition: background 0.15s;', -'}', -'.cuckoo-commit-item:hover { background: rgba(139,147,255,0.15); }', -'.cuckoo-commit-subject-line { font-size: 12px; color: #c8ccff; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }', -'.cuckoo-commit-meta-line { font-size: 10.5px; color: #5d6280; font-family: "Consolas", monospace; }', -'.cuckoo-commit-meta-line .cuckoo-commit-hash { color: #8b93ff; }', -'.cuckoo-commit-files { display: flex; flex-direction: column; flex: 1; overflow: hidden; }', -'.cuckoo-commit-files-header {', -' display: flex; align-items: center; gap: 8px; padding: 8px 12px;', -' border-bottom: 1px solid var(--ck-border); flex-shrink: 0;', -'}', -'.cuckoo-commit-subject { flex: 1; font-size: 12px; color: #c8ccff; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }', -'.cuckoo-diff-viewer {', -' position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%);', -' z-index: 2147483647; width: 92vw; max-width: 1100px; height: 84vh;', -' background: rgba(17, 19, 34, 0.99);', -' border: 1px solid var(--ck-border); border-radius: 16px;', -' display: flex; flex-direction: column; overflow: hidden;', -' box-shadow: 0 20px 60px rgba(0, 0, 0, 0.65), 0 0 0 1px rgba(139, 147, 255, 0.1);', -' transition: opacity 0.18s ease, transform 0.18s ease;', -'}', -'.cuckoo-diff-viewer.cuckoo-hidden { opacity: 0; pointer-events: none; transform: translate(-50%, -50%) scale(0.96); }', -'.cuckoo-diff-viewer-body { flex: 1; overflow: auto; background: rgba(0,0,0,0.25); padding: 8px 0; }', -'.cuckoo-diff-code {', -' font-family: "Cascadia Code", "Consolas", monospace; font-size: 12px; line-height: 1.55;', -' margin: 0; padding: 0; white-space: pre;', -'}', -'.cuckoo-diff-line { display: flex; }', -'.cuckoo-diff-line .cuckoo-diff-text { flex: 1; padding: 0 12px; white-space: pre; }', -'.cuckoo-diff-line.add { background: rgba(74,222,128,0.10); }', -'.cuckoo-diff-line.add .cuckoo-diff-text { color: #86efac; }', -'.cuckoo-diff-line.del { background: rgba(255,107,122,0.10); }', -'.cuckoo-diff-line.del .cuckoo-diff-text { color: #ff9aa5; }', -'.cuckoo-diff-line.hunk { background: rgba(139,147,255,0.10); }', -'.cuckoo-diff-line.hunk .cuckoo-diff-text { color: #a8afff; }', -'.cuckoo-diff-line.meta .cuckoo-diff-text { color: #5d6280; }', -'.cuckoo-diff-line.ctx .cuckoo-diff-text { color: #8a90b8; }', -'.cuckoo-diff-empty { padding: 20px; color: #8a90b8; font-size: 13px; text-align: center; }', -// ========== Окно «Cookie Code» с задачами (слева) ========== -'.cuckoo-todo-panel {', -' position: fixed; top: 90px; left: 20px; z-index: 2147483646;', -' width: 380px; max-height: 68vh; display: flex; flex-direction: column;', -' background: rgba(17, 19, 34, var(--cuckoo-overlay-bg-opacity, 0.72));', -' backdrop-filter: blur(var(--cuckoo-sidebar-blur, 12px)) saturate(140%);', -' -webkit-backdrop-filter: blur(var(--cuckoo-sidebar-blur, 12px)) saturate(140%);', -' border: 1px solid var(--ck-border); border-radius: 16px;', -' box-shadow: 0 12px 40px rgba(0,0,0,0.45), 0 0 0 1px rgba(139,147,255,0.06);', -' font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;', -' color: var(--ck-text); font-size: 13px; line-height: 1.55; overflow: hidden;', -' transition: opacity 0.4s ease, transform 0.4s ease;', -'}', -'.cuckoo-todo-panel.cuckoo-hidden { display: flex !important; opacity: 0; pointer-events: none; transform: translateX(-10px); }', -'.cuckoo-todo-tab {', -' position: fixed; top: 90px; left: 0; z-index: 2147483646;', -' display: flex; align-items: center; gap: 6px; padding: 8px 9px;', -' color: var(--ck-text); background: rgba(17, 19, 34, var(--cuckoo-overlay-bg-opacity, 0.72));', -' border: 1px solid var(--ck-border); border-left: 0; border-radius: 0 8px 8px 0;', -' box-shadow: 0 8px 26px rgba(0,0,0,0.35); cursor: pointer;', -' font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;', -' font-size: 12px; font-weight: 700; line-height: 1; transition: opacity 0.2s ease, transform 0.2s ease, background 0.15s ease;', -'}', -'.cuckoo-todo-tab:hover { background: rgba(28, 31, 54, 0.9); transform: translateX(2px); }', -'.cuckoo-todo-tab.cuckoo-hidden { display: none !important; }', -'.cuckoo-todo-tab-counter { color: var(--ck-text-dim); font-size: 11px; font-weight: 600; }', -'.cuckoo-todo-header {', -' display: flex; align-items: center; justify-content: space-between; gap: 8px;', -' padding: 12px 16px 10px; flex-shrink: 0; cursor: grab; user-select: none;', -' border-bottom: 1px solid var(--ck-border);', -' background: rgba(255,255,255,0.04);', -' backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px);', -'}', -'.cuckoo-todo-header:active { cursor: grabbing; }', -'.cuckoo-todo-header-right { display: flex; align-items: center; gap: 8px; }', -'.cuckoo-todo-title { font-size: 14px; font-weight: 700; color: #c8ccff; letter-spacing: 0.4px; }', -'.cuckoo-todo-counter { font-size: 11px; color: var(--ck-text-dim); font-weight: 600; }', -'.cuckoo-todo-body { padding: 10px 12px; overflow-y: auto; flex: 1; }', -'.cuckoo-todo-list { display: flex; flex-direction: column; gap: 2px; }', -'.cuckoo-todo-item { display: flex; gap: 7px; align-items: flex-start; padding: 2px 0; }', -'.cuckoo-todo-icon { flex-shrink: 0; width: 14px; text-align: center; color: #8a90b8; }', -'.cuckoo-todo-text { flex: 1; word-break: break-word; }', -'.cuckoo-todo-item.completed .cuckoo-todo-text { color: #8a90b8; text-decoration: line-through; }', -'.cuckoo-todo-item.completed .cuckoo-todo-icon { color: #4ade80; }', -'.cuckoo-todo-item.in_progress .cuckoo-todo-text { color: #fff; font-weight: 600; }', -'.cuckoo-todo-item.in_progress .cuckoo-todo-icon { color: #8b93ff; }', -].join('\n'); + ".cuckoo-label { font-size: 12px; font-weight: 600; color: #c8ccff; text-transform: uppercase; letter-spacing: 0.6px; cursor: pointer; margin-bottom: 2px; }", + ".cuckoo-cmd-preview {", + " background: rgba(15,18,32,0.6); border: 1px solid rgba(255,255,255,0.1);", + " border-radius: 10px; padding: 10px 12px;", + ' font-family: "Cascadia Code", "Fira Code", "Consolas", monospace;', + " font-size: 12px; color: #a7f3c0; line-height: 1.5;", + " max-height: 140px; overflow: auto; white-space: pre-wrap; word-break: break-all; margin: 0;", + "}", + ".cuckoo-actions { display: flex; gap: 8px; width: 100%; flex-wrap: wrap; }", + ".cuckoo-actions .cuckoo-btn { flex: 1 1 auto; min-width: 0; white-space: nowrap; }", + ".cuckoo-btn {", + " flex: 1; padding: 9px 10px; border: 1px solid transparent; border-radius: var(--cuckoo-overlay-btn-radius, 10px);", + " font-size: 12.5px; font-weight: 600; cursor: pointer;", + " transition: all 0.22s ease; letter-spacing: 0.3px;", + " white-space: nowrap; text-overflow: ellipsis; overflow: hidden;", + "}", + ".cuckoo-btn:active { transform: scale(0.98); }", + ".cuckoo-btn-primary {", + " background: var(--cuckoo-overlay-primary-bg, linear-gradient(135deg, #8b93ff, #6d76ff)); color: #fff;", + " box-shadow: 0 6px 18px var(--cuckoo-overlay-primary-shadow, rgba(109, 118, 255, 0.25));", + "}", + ".cuckoo-btn-primary:hover { box-shadow: 0 8px 22px var(--cuckoo-overlay-primary-shadow-hover, rgba(109, 118, 255, 0.4)); transform: translateY(-1px); }", + ".cuckoo-btn-primary:disabled { opacity: 0.5; cursor: not-allowed; transform: none; }", + ".cuckoo-btn-secondary {", + " background: var(--cuckoo-overlay-secondary-bg, rgba(139,147,255,0.12)); color: var(--cuckoo-overlay-secondary-text, #a8afff);", + " border-color: var(--cuckoo-overlay-secondary-border, rgba(139, 147, 255, 0.5));", + "}", + ".cuckoo-btn-secondary:hover { background: var(--cuckoo-overlay-secondary-hover-bg, rgba(139, 147, 255, 0.28)); color: #fff; border-color: var(--cuckoo-overlay-secondary-hover-border, rgba(139, 147, 255, 0.75)); transform: translateY(-1px); }", + ".cuckoo-btn-text {", + " background: none; border: none; color: var(--ck-text-dim); padding: 4px 0;", + " text-align: left; font-size: 11px; cursor: pointer; transition: color 0.2s;", + "}", + ".cuckoo-btn-text:hover { color: var(--ck-red); }", + ".cuckoo-result-status { font-size: 12px; font-weight: 600; padding: 2px 0; }", + "#cuckoo-result-section { background: rgba(255,255,255,0.04); border: 1px solid rgba(255,255,255,0.08); border-radius: 10px; padding: 10px 12px; }", + ".cuckoo-result-status.success { color: var(--ck-green); }", + ".cuckoo-result-status.error { color: var(--ck-red); }", + ".cuckoo-result-output {", + " background: rgba(15,18,32,0.6); border: 1px solid rgba(255,255,255,0.1);", + " border-radius: 10px; padding: 10px 12px;", + ' font-family: "Cascadia Code", "Fira Code", "Consolas", monospace;', + " font-size: 11.5px; color: #c8ccff; line-height: 1.5;", + " max-height: 200px; overflow: auto; white-space: pre-wrap; word-break: break-all; margin: 0;", + "}", + ".cuckoo-history-list { max-height: 140px; overflow-y: auto; display: flex; flex-direction: column; gap: 6px; }", + ".cuckoo-history-item {", + " background: rgba(255,255,255,0.04); border: 1px solid rgba(255,255,255,0.08); border-radius: 10px; padding: 8px 10px;", + " font-size: 12px; cursor: pointer; transition: background 0.2s, border-color 0.2s;", + "}", + ".cuckoo-history-item:hover { background: rgba(139, 147, 255, 0.18); border-color: rgba(139,147,255,0.45); }", + ".cuckoo-history-item .cuckoo-cmd-text {", + ' font-family: "Consolas", monospace; color: #a7f3c0; font-size: 12px;', + " display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;", + "}", + ".cuckoo-history-item .cuckoo-cmd-status { font-size: 11px; margin-top: 2px; display: block; }", + ".cuckoo-history-item .cuckoo-cmd-status.success { color: var(--ck-green); }", + ".cuckoo-history-item .cuckoo-cmd-status.error { color: var(--ck-red); }", + ".cuckoo-history-item .cuckoo-cmd-time { font-size: 10px; color: #5d6280; margin-top: 2px; display: block; }", + "#cuckoo-mcp-manager { width: 780px; max-width: 90vw; }", + "#cuckoo-mcp-manager .cuckoo-mcp-body { flex-direction: row; gap: 14px; align-items: stretch; overflow: hidden; }", + ".cuckoo-mcp-left { width: 220px; flex-shrink: 0; display: flex; flex-direction: column; }", + ".cuckoo-mcp-left .cuckoo-session-list { flex: 1; max-height: none; overflow-y: auto; }", + ".cuckoo-mcp-right { flex: 1; display: flex; flex-direction: column; gap: 10px; }", + ".cuckoo-mcp-json { flex: 1; min-height: 55vh; resize: vertical; }", + ".cuckoo-mcp-right .cuckoo-btn { flex: 0 0 auto; align-self: flex-start; }", + ".cuckoo-window-manager {", + " position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%);", + " z-index: 2147483647; width: 520px; height: 80vh; max-height: 85vh;", + " background: rgba(17, 19, 34, 0.98);", + " border: 1px solid var(--ck-border); border-radius: 16px;", + " display: flex; flex-direction: column; overflow: hidden;", + " box-shadow: 0 16px 50px rgba(0, 0, 0, 0.55), 0 0 0 1px rgba(139, 147, 255, 0.08);", + " transition: opacity 0.22s ease, transform 0.22s ease;", + "}", + ".cuckoo-window-manager.cuckoo-hidden { opacity: 0; pointer-events: none; transform: translate(-50%, -50%) scale(0.95); }", + ".cuckoo-wm-header {", + " display: flex; align-items: center; justify-content: space-between;", + " padding: 12px 16px 10px; flex-shrink: 0;", + " border-bottom: 1px solid var(--ck-border);", + " background: rgba(255, 255, 255, 0.02);", + "}", + ".cuckoo-wm-body { padding: 12px 14px; overflow: hidden; flex: 1; display: flex; flex-direction: column; gap: 12px; }", + ".cuckoo-window-item {", + " display: flex; align-items: center; justify-content: space-between;", + " background: var(--ck-surface); border: 1px solid var(--ck-border);", + " border-radius: 10px; padding: 10px 12px; cursor: pointer;", + " transition: background 0.2s; font-size: 12px;", + " gap: 8px;", + "}", + ".cuckoo-window-item:hover { background: rgba(139, 147, 255, 0.18); }", + ".cuckoo-window-left { display: flex; align-items: center; gap: 6px; flex: 1; min-width: 0; }", + ".cuckoo-window-item .cuckoo-window-name { color: #c8ccff; font-weight: 600; flex-shrink: 0; }", + ".cuckoo-window-item .cuckoo-window-sep { color: #5d6280; flex-shrink: 0; }", + ".cuckoo-window-item .cuckoo-window-status { color: #8a90b8; font-size: 11px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }", + ".cuckoo-window-del {", + " color: #ff6b7a; cursor: pointer; font-size: 11px; padding: 2px 6px;", + " border-radius: 6px; flex-shrink: 0; transition: all 0.2s;", + "}", + ".cuckoo-window-del:hover { background: rgba(255, 107, 122, 0.18); color: #fff; }", + ".cuckoo-window-item.active { border-color: rgba(139, 147, 255, 0.5); background: rgba(139, 147, 255, 0.12); }", + "#cuckoo-window-list { max-height: none; flex: 1; min-height: 200px; overflow-y: auto; }", + "#cuckoo-window-list .cuckoo-window-item { flex-shrink: 0; }", + "#cuckoo-status-badge {", + " position: fixed; bottom: 24px; right: 24px; z-index: 2147483647;", + " width: 48px; height: 48px; border-radius: 50%;", + " background: rgba(139, 147, 255, 0.22); border: 1px solid rgba(139, 147, 255, 0.4);", + " will-change: transform;", + " display: flex; align-items: center; justify-content: center; gap: 0;", + " cursor: pointer;", + " transition: background 0.2s, border-color 0.2s, transform 0.15s;", + "}", + "#cuckoo-status-badge:hover { background: rgba(139, 147, 255, 0.35); border-color: rgba(139, 147, 255, 0.65); transform: scale(1.05); }", + "#cuckoo-status-dot {", + " position: absolute; top: 6px; right: 6px;", + " width: 9px; height: 9px; border-radius: 50%;", + " background: var(--ck-green); animation: cuckoo-pulse 2s infinite;", + "}", + ".cuckoo-fab-icon {", + " display: flex; align-items: center; justify-content: center;", + " width: 26px; height: 22px;", + " color: #a8afff;", + " user-select: none;", + "}", + ".cuckoo-fab-icon svg {", + " width: 100%; height: 100%;", + " display: block;", + "}", + ".cuckoo-toast {", + " position: fixed; top: 18px; left: 50%; transform: translateX(-50%) translateY(-8px);", + " z-index: 2147483648; min-width: 200px; max-width: 380px; text-align: center;", + " background: rgba(22, 24, 44, 0.92); backdrop-filter: blur(18px);", + " -webkit-backdrop-filter: blur(18px);", + " border: 1px solid rgba(139, 147, 255, 0.28); border-radius: 12px;", + " padding: 10px 16px; font-size: 13px; color: #dde1ff;", + " box-shadow: 0 10px 30px rgba(0, 0, 0, 0.35);", + " opacity: 0; pointer-events: none; transition: opacity 0.22s ease, transform 0.22s ease;", + "}", + ".cuckoo-toast.show { opacity: 1; transform: translateX(-50%) translateY(0); }", + ".cuckoo-task-status {", + " display: inline-flex; align-items: center; gap: 5px;", + " color: #a8afff; font-size: 11px; font-weight: 600;", + "}", + ".cuckoo-btn-kill {", + " margin-left: 6px; padding: 2px 8px;", + " border: 1px solid rgba(255, 107, 122, 0.55); border-radius: 7px;", + " background: rgba(255, 107, 122, 0.16); color: #ff8a95;", + " font-size: 10.5px; font-weight: 700; letter-spacing: 0.3px;", + " cursor: pointer; transition: all 0.2s ease; line-height: 1.4;", + "}", + ".cuckoo-btn-kill:hover { background: rgba(255, 107, 122, 0.32); color: #fff; border-color: rgba(255, 107, 122, 0.9); }", + ".cuckoo-btn-kill:active { transform: scale(0.96); }", + ".cuckoo-btn-kill:disabled { opacity: 0.5; cursor: not-allowed; }", + ".cuckoo-spinner {", + " width: 11px; height: 11px; border-radius: 50%;", + " border: 2px solid rgba(139,147,255,0.25); border-top-color: #8b93ff;", + " animation: cuckoo-spin 0.8s linear infinite;", + "}", + "@keyframes cuckoo-spin { to { transform: rotate(360deg); } }", + "@keyframes cuckoo-pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.35; } }", + "@keyframes cuckoo-attention {", + " 0%, 100% { transform: scale(1); }", + " 10%, 30%, 50%, 70%, 90% { transform: scale(1.08); }", + " 20%, 40%, 60%, 80% { transform: scale(0.98); }", + "}", + ".cuckoo-btn-attention {", + " animation: cuckoo-attention 1s ease-in-out 3;", + " background: rgba(139, 147, 255, 0.35) !important;", + " color: #fff !important;", + " border-color: rgba(139, 147, 255, 0.6) !important;", + "}", + ".cuckoo-hidden { display: none !important; }", + ".cuckoo-home-mode .cuckoo-header .cuckoo-btn-icon,", + ".cuckoo-home-mode .cuckoo-body .cuckoo-section,", + ".cuckoo-home-mode .cuckoo-body .cuckoo-actions:not(:has(#cuckoo-btn-init)),", + ".cuckoo-home-mode .cuckoo-divider {", + " display: none !important;", + "}", + ".cuckoo-overlay ::-webkit-scrollbar { width: 6px; }", + ".cuckoo-overlay ::-webkit-scrollbar-track { background: transparent; }", + ".cuckoo-overlay ::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.15); border-radius: 3px; }", + ".cuckoo-overlay ::-webkit-scrollbar-thumb:hover { background: rgba(255,255,255,0.25); }", + ".cuckoo-project-dir-section {", + " background: rgba(255,255,255,0.04); border-radius: 10px;", + " padding: 10px 12px; border: 1px solid rgba(255,255,255,0.08);", + "}", + ".cuckoo-project-dir-row { display: flex; justify-content: space-between; align-items: center; }", + ".cuckoo-btn-change-dir {", + " background: rgba(139,147,255,0.12); border: 1px solid rgba(139,147,255,0.5);", + " color: #a8afff; padding: 3px 10px; border-radius: 8px;", + " cursor: pointer; font-size: 11px; font-weight: 600; transition: all 0.18s;", + "}", + ".cuckoo-btn-change-dir:hover { background: rgba(139,147,255,0.28); color: #fff; border-color: rgba(139,147,255,0.75); }", + '.cuckoo-project-dir-display { margin-top: 4px; font-size: 12px; font-family: "Consolas", monospace; color: #8a90b8; word-break: break-all; }', + ".cuckoo-dir-path { color: #a7f3c0; }", + ".cuckoo-token-section { background: var(--ck-surface); border-radius: 10px; padding: 8px 10px; border: 1px solid var(--ck-border); }", + ".cuckoo-token-display { display: flex; align-items: baseline; gap: 6px; margin-top: 2px; }", + '.cuckoo-token-count { font-size: 20px; font-weight: 700; color: #a7f3c0; font-family: "Consolas", monospace; }', + ".cuckoo-token-unit { font-size: 11px; color: var(--ck-text-dim); }", + ".cuckoo-divider { border-top: 1px solid var(--ck-border); margin: 2px 0; }", + ".cuckoo-session-section {", + " background: rgba(255,255,255,0.04); border-radius: 10px;", + " padding: 10px 12px; border: 1px solid rgba(255,255,255,0.08);", + "}", + ".cuckoo-session-header { display: flex; justify-content: space-between; align-items: center; }", + ".cuckoo-btn-refresh-sessions {", + " background: rgba(139,147,255,0.12); border: 1px solid rgba(139,147,255,0.5);", + " color: #a8afff; padding: 3px 10px; border-radius: 8px;", + " cursor: pointer; font-size: 11px; font-weight: 600; transition: all 0.18s;", + "}", + ".cuckoo-btn-refresh-sessions:hover { background: rgba(139,147,255,0.28); color: #fff; border-color: rgba(139,147,255,0.75); }", + ".cuckoo-session-list { max-height: 100px; overflow-y: auto; margin-top: 4px; display: flex; flex-direction: column; gap: 4px; }", + '.cuckoo-session-item { background: rgba(255,255,255,0.05); border-radius: 8px; padding: 6px 10px; font-size: 12px; font-family: "Consolas", monospace; color: #ccc; cursor: pointer; transition: background 0.2s; }', + ".cuckoo-session-item:hover { background: rgba(139,147,255,0.2); }", + ".cuckoo-session-item .session-id { color: #a8afff; font-size: 11px; }", + ".cuckoo-session-item .session-action { color: #a7f3c0; font-size: 11px; font-weight: 600; }", + ".cuckoo-session-empty { color: #5d6280; font-size: 12px; font-style: italic; padding: 8px 0; text-align: center; }", + ".cuckoo-first-time-dialog {", + " position: fixed; top: 0; left: 0; right: 0; z-index: 2147483647;", + " display: flex; align-items: flex-start; justify-content: center;", + " padding-top: 24px;", + " pointer-events: none;", + "}", + ".cuckoo-first-time-dialog.cuckoo-hidden { display: none !important; }", + ".cuckoo-first-time-box {", + " position: relative; max-width: 420px; width: 90%;", + " background: rgba(17, 19, 34, 0.98);", + " border: 1px solid rgba(139, 147, 255, 0.35);", + " border-radius: 18px;", + " padding: 28px 24px 22px;", + " box-shadow: 0 20px 60px rgba(0, 0, 0, 0.6), 0 0 0 1px rgba(139, 147, 255, 0.08);", + " color: var(--ck-text); font-size: 14px; line-height: 1.6;", + " text-align: center;", + " pointer-events: auto;", + "}", + ".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; }", + ".cuckoo-first-time-box .cuckoo-btn { flex: 0 0 auto; min-width: 150px; }", + // ========== CSS-переменные (управляются из настроек Cookie Code) ========== + ":root {", + " --cuckoo-bg-blur: 0px;", // размытие самой картинки фона + " --cuckoo-header-blur: 12px;", // стекло шапки + " --cuckoo-sidebar-blur: 12px;", // стекло сайдбара + " --cuckoo-header-opacity: 45%;", // плотность фона шапки (0 = прозрачно) + " --cuckoo-sidebar-opacity: 45%;", // плотность фона сайдбара (0 = прозрачно) + " --cuckoo-toolblock-opacity: 55%;", // плотность фона tool-блоков + " --cuckoo-toolblock-blur: 0px;", // стекло tool-блоков + " --cuckoo-input-glass-opacity: 55%;", // плотность фона поля ввода + " --cuckoo-input-glass-blur: 12px;", // стекло поля ввода + "}", + // ========== 页面背景:базовый цвет + стили (картинка ставится через JS) ========== + // При выключенной кастомизации (класс cuckoo-customization-off на ) + // правила не применяются, и фон остаётся родным фоном страницы DeepSeek. + "html:not(.cuckoo-customization-off), body:not(.cuckoo-customization-off) {", + " background-color: #0f1220 !important;", + " background-size: cover !important;", + " background-position: center center !important;", + " background-repeat: no-repeat !important;", + " background-attachment: fixed !important;", + "}", + // ========== Размытие самой фоновой картинки (--cuckoo-bg-blur) ========== + // Отдельный fixed-слой поверх body, чтобы фильтр не затрагивал контент. + "html:not(.cuckoo-customization-off)::before {", + ' content: "";', + " position: fixed;", + " inset: 0;", + " background: inherit;", + " background-image: inherit;", + " background-size: cover;", + " background-position: center center;", + " background-attachment: fixed;", + " filter: blur(var(--cuckoo-bg-blur));", + " transform: scale(1.05);", // скрываем размытые края по периметру + " pointer-events: none;", + " z-index: -1;", + "}", + // ========== 页面顶部标题栏毛玻璃(DeepSeek) ========== + // Прозрачность управляется переменной --cuckoo-header-opacity (0% — полностью прозрачно). + ".the-header {", + " backdrop-filter: blur(var(--cuckoo-header-blur)) saturate(140%) !important;", + " -webkit-backdrop-filter: blur(var(--cuckoo-header-blur)) saturate(140%) !important;", + " background: rgba(15, 18, 32, var(--cuckoo-header-opacity)) !important;", + "}", + // ========== 去掉输入框底部的深色渐变,让背景图透出 ========== + // DeepSeek 在 _871cbca 与其空的子层 d72636e2 上画了从上到下的 + // 线性渐变(底部是不透明的 rgb(21,21,23)),会挡住页面背景图。 + // 用简写 background: transparent !important 一次清掉颜色+渐变。 + "._871cbca,", + "._871cbca > .d72636e2 {", + " background: transparent !important;", + "}", + // ========== 左侧边栏毛玻璃(DeepSeek) ========== + // .b8812f16.a2f3d50e — корень сайдбара. Внутри много слоёв, каждый со своим + // фоном (шапка _262baab, список _3586175/_6d215eb/_77cdc67, низ _7b40dad). + // Делаем прозрачными все промежуточные слои, чтобы стекло работало насквозь. + ".b8812f16.a2f3d50e,", + ".b8812f16.a2f3d50e > div,", + ".b8812f16.a2f3d50e > div > div,", + ".b8812f16.a2f3d50e ._262baab,", + ".b8812f16.a2f3d50e ._3586175,", + ".b8812f16.a2f3d50e ._6d215eb,", + ".b8812f16.a2f3d50e ._77cdc67,", + ".b8812f16.a2f3d50e ._7b40dad,", + ".b8812f16.a2f3d50e ._8a693f3,", + ".b8812f16.a2f3d50e ._1d72f01,", + ".b8812f16.a2f3d50e .f3d18f6a,", + ".b8812f16.a2f3d50e ._3098d02 {", + " background: transparent !important;", + " backdrop-filter: none !important;", + " -webkit-backdrop-filter: none !important;", + "}", + // Стекло применяем один раз — на корень сайдбара. + // Прозрачность — переменная --cuckoo-sidebar-opacity (0% — полностью прозрачно). + ".b8812f16.a2f3d50e {", + " background: rgba(15, 18, 32, var(--cuckoo-sidebar-opacity)) !important;", + " backdrop-filter: blur(var(--cuckoo-sidebar-blur)) saturate(140%) !important;", + " -webkit-backdrop-filter: blur(var(--cuckoo-sidebar-blur)) saturate(140%) !important;", + "}", + // ========== 隐藏底部「Сгенерировано ИИ」免责声明 ========== + "._0fcaa63 {", + " display: none !important;", + "}", + // ========== Ошибка «Слишком частые сообщения» (DeepSeek rate limit) — красный текст ========== + // Хэш _1ce76f5 нестабилен, поэтому цепляемся к нему + страховка через :has() + // и любые элементы с этим текстом внутри сообщений. + "._1ce76f5,", + '[class*="_1ce76f5"] {', + " color: #ff6b7a !important;", + " font-weight: 600 !important;", + "}", + // Страховка: любой span внутри сообщения, содержащий «Слишком частые сообщения» + "p:has(> span):has(._1ce76f5),", + "div:has(> ._1ce76f5) {", + " color: #ff6b7a !important;", + "}", + // ========== Модалка «Настройки» (и другие ds-modal) — матовое стекло ========== + ".ds-modal-content {", + " background: rgba(15, 18, 32, 0.55) !important;", + " backdrop-filter: blur(16px) saturate(140%) !important;", + " -webkit-backdrop-filter: blur(16px) saturate(140%) !important;", + "}", + // Приглушаем фон-маску, чтобы под модалкой просвечивала страница + ".ds-modal-mask {", + " background: rgba(0, 0, 0, 0.35) !important;", + "}", + // ========== Матовое стекло для поля ввода сообщения DeepSeek ========== + // Приём тот же, что у плашки «Размышление»: backdrop-filter не работает, + // потому что контейнер инпута лежит внутри виртуализированного списка с + // transform. Поэтому стекло рисуем сами: ::before показывает тот же фон + // страницы, размытый и спозиционированный под вьюпорт; ::after добавляет + // лёгкую затемняющую плёнку. Координаты считает input-glass.js. + // Селекторы из DOM: внешняя обёртка блока ввода _77cefa5 (у неё непрозрачный + // фон), внутренний контейнер _020ab5b, textarea[name="search"]. + // Внешняя обёртка блока ввода — именно у неё непрозрачный фон (белый/тёмный) + // и border-radius 24px. Именно её делаем стеклянной. + // Дополнительно правило отключается классом cuckoo-input-glass-off на + // (чекбокс «Стекло поля ввода включено» в настройках). + "html:not(.cuckoo-customization-off):not(.cuckoo-input-glass-off) ._77cefa5 {", + " position: relative !important;", + " overflow: hidden !important;", + " border-radius: 24px !important;", + " border: 1px solid rgba(139, 147, 255, 0.22) !important;", + " background: transparent !important;", + " backdrop-filter: none !important;", + " -webkit-backdrop-filter: none !important;", + "}", + // Псевдоэлемент рисует тот же фон страницы, размытый (backdrop-filter не + // работает: предки внутри виртуализированного списка с transform). + "html:not(.cuckoo-customization-off):not(.cuckoo-input-glass-off) ._77cefa5::before {", + ' content: "" !important;', + " position: absolute !important;", + " inset: calc(-1 * var(--cuckoo-input-pad, 40px)) !important;", + " background-image: var(--cuckoo-bg-image, none) !important;", + " background-size: var(--cuckoo-input-bg-size, cover) !important;", + " background-repeat: no-repeat !important;", + " background-position: var(--cuckoo-input-bg-pos, 0 0) !important;", + " filter: blur(var(--cuckoo-input-glass-blur, 12px)) !important;", + " -webkit-filter: blur(var(--cuckoo-input-glass-blur, 12px)) !important;", + " pointer-events: none !important;", + " z-index: 0 !important;", + "}", + "html:not(.cuckoo-customization-off):not(.cuckoo-input-glass-off) ._77cefa5::after {", + ' content: "" !important;', + " position: absolute !important;", + " inset: 0 !important;", + " background: rgba(15, 18, 32, var(--cuckoo-input-glass-opacity, 55%)) !important;", + " pointer-events: none !important;", + " z-index: 0 !important;", + "}", + // Внутренний контейнер ввода — прозрачный, контент поверх стекла. + "html:not(.cuckoo-customization-off):not(.cuckoo-input-glass-off) ._020ab5b {", + " position: relative !important;", + " background: transparent !important;", + " z-index: 1 !important;", + "}", + 'html:not(.cuckoo-customization-off):not(.cuckoo-input-glass-off) ._020ab5b textarea[name="search"],', + "html:not(.cuckoo-customization-off):not(.cuckoo-input-glass-off) ._020ab5b ._24fad49 {", + " background: transparent !important;", + "}", + // ========== Кнопка «Новый чат» в сайдбаре — матовое стекло ========== + "._5a8ac7a.a084f19e {", + " background: rgba(15, 18, 32, 0.5) !important;", + " backdrop-filter: blur(10px) saturate(140%) !important;", + " -webkit-backdrop-filter: blur(10px) saturate(140%) !important;", + " border: 1px solid rgba(139, 147, 255, 0.2) !important;", + " border-radius: 12px !important;", + "}", + // ========== Рендер tool-блоков (cuckoo) — как в dsh-web-dev ========== + ".cuckoo-tool-block {", + " margin: 8px 0;", + " border: 1px solid rgba(139, 147, 255, 0.22);", + " border-radius: 10px;", + " background: rgba(15, 18, 32, var(--cuckoo-toolblock-opacity)) !important;", + " backdrop-filter: blur(var(--cuckoo-toolblock-blur)) !important;", + " -webkit-backdrop-filter: blur(var(--cuckoo-toolblock-blur)) !important;", + " overflow: hidden;", + " min-height: 0 !important;", + ' font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;', + "}", + '.cuckoo-tool-block[data-expanded="false"] > .md-code-block {', + " display: none !important;", + " height: 0 !important;", + " min-height: 0 !important;", + "}", + ".cuckoo-tool-header {", + " display: flex; align-items: center; gap: 8px;", + " padding: 8px 12px;", + " cursor: pointer;", + " user-select: none;", + " font-size: 13px;", + " color: #dde1ff;", + " transition: background 0.15s;", + "}", + ".cuckoo-tool-header:hover { background: rgba(139, 147, 255, 0.09); }", + ".cuckoo-tool-chevron {", + " display: inline-block; width: 12px; text-align: center;", + " color: #8a90b8; font-size: 11px;", + "}", + ".cuckoo-tool-icon {", + " display: inline-flex; align-items: center; justify-content: center;", + " width: 16px; height: 16px;", + " color: #a8afff;", + " flex-shrink: 0;", + "}", + ".cuckoo-tool-icon svg {", + " width: 16px !important; height: 16px !important;", + " display: block;", + " stroke: currentColor;", + "}", + ".cuckoo-tool-label { font-weight: 600; color: #a8afff; }", + ".cuckoo-tool-sep { color: #5d6280; }", + ".cuckoo-tool-file {", + " color: #a7f3c0;", + ' font-family: "Consolas", "Cascadia Code", monospace;', + " font-size: 12px;", + " text-decoration: underline dashed rgba(167, 243, 192, 0.35);", + " cursor: pointer;", + "}", + '.cuckoo-tool-diff { margin-left: auto; display: inline-flex; align-items: center; gap: 4px; font-family: "Consolas", "Cascadia Code", monospace; font-size: 11px; font-weight: 600; white-space: nowrap; }', + ".cuckoo-diff-add { color: #4ade80; }", + ".cuckoo-diff-del { color: #f87171; }", + '.cuckoo-tool-block[data-expanded="true"] > .md-code-block {', + " display: block !important;", + " border-top: 1px solid rgba(255, 255, 255, 0.06);", + " margin: 0 !important;", + " border-radius: 0 !important;", + "}", + // ========== Стиль ошибки tool-блока ========== + ".cuckoo-tool-block.cuckoo-tool-error {", + " border-color: rgba(255, 107, 122, 0.65) !important;", + " background: rgba(60, 15, 22, 0.7) !important;", + "}", + ".cuckoo-tool-block.cuckoo-tool-error .cuckoo-tool-header {", + " background: rgba(255, 107, 122, 0.1);", + "}", + ".cuckoo-tool-block.cuckoo-tool-error .cuckoo-tool-label {", + " color: #ff8e9b !important;", + "}", + ".cuckoo-tool-block.cuckoo-tool-error .cuckoo-tool-chevron {", + " color: #ff6b7a !important;", + "}", + ".cuckoo-tool-error-icon {", + " color: #ff6b7a;", + " font-size: 14px;", + " margin-left: 4px;", + "}", + ".cuckoo-tool-error-msg {", + " padding: 8px 12px;", + " font-size: 12px;", + " color: #ff9aa5;", + " background: rgba(255, 107, 122, 0.1);", + " border-top: 1px solid rgba(255, 107, 122, 0.3);", + " white-space: pre-wrap;", + " word-break: break-word;", + ' font-family: "Consolas", monospace;', + "}", + // Внутренний код-блок внутри tool-блока: фон/стекло по тем же переменным + ".cuckoo-tool-block .md-code-block,", + ".cuckoo-tool-block .md-code-block-banner-wrap,", + ".cuckoo-tool-block .md-code-block-banner,", + ".cuckoo-tool-block pre,", + ".cuckoo-tool-block code {", + " background: rgba(15, 18, 32, var(--cuckoo-toolblock-opacity)) !important;", + " backdrop-filter: blur(var(--cuckoo-toolblock-blur)) !important;", + " -webkit-backdrop-filter: blur(var(--cuckoo-toolblock-blur)) !important;", + "}", + // Сам pre делаем прозрачным поверх фона контейнера — чтобы не дублировать + ".cuckoo-tool-block pre,", + ".cuckoo-tool-block code {", + " background: transparent !important;", + " backdrop-filter: none !important;", + " -webkit-backdrop-filter: none !important;", + "}", + // ========== Мета-информация под ответом AI (время + токены + затронутые файлы) ========== + ".cuckoo-response-meta {", + " display: flex; flex-wrap: wrap; align-items: center; gap: 6px;", + " margin-top: 6px; padding: 3px 8px;", + " font-size: 11px;", + " color: #8a90b8;", + ' font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;', + " border-radius: 6px;", + " user-select: none;", + " opacity: 0.85;", + "}", + ".cuckoo-response-meta-item {", + " display: inline-flex; align-items: center; gap: 4px;", + "}", + ".cuckoo-response-meta-item svg { display: block; }", + ".cuckoo-response-meta-sep { color: #5d6280; }", + ".cuckoo-response-meta-produced { display: inline-flex; align-items: center; gap: 6px; flex-wrap: wrap; }", + ".cuckoo-response-meta-produced-label { color: #6b7280; font-size: 11px; font-weight: 400; }", + ".cuckoo-produced-chip {", + " display: inline-flex; align-items: center; gap: 4px;", + " padding: 1px 8px 1px 6px;", + " background: rgb(44, 44, 46);", + " border: 1px solid rgba(255,255,255,0.06);", + " border-radius: 999px;", + " font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;", + " font-size: 11px; line-height: 18px;", + " color: #c8ccff; cursor: pointer;", + " transition: background .12s ease, border-color .12s ease;", + " user-select: none;", + "}", + ".cuckoo-produced-chip:hover { background: rgba(139,147,255,0.18); border-color: rgba(139,147,255,0.45); }", + ".cuckoo-produced-chip:focus-visible { outline: 1px solid rgba(139,147,255,0.6); outline-offset: 1px; }", + ".cuckoo-produced-chip-icon {", + " display: inline-flex; align-items: center; justify-content: center;", + " width: 14px; height: 14px;", + "}", + ".cuckoo-produced-chip-icon svg {", + " width: 14px; height: 14px; flex: 0 0 14px;", + "}", + '.cuckoo-produced-chip[data-status="error"] { border-color: rgba(255,107,122,0.45); background: rgba(60,15,22,0.65); }', + '.cuckoo-produced-chip[data-status="error"] .cuckoo-produced-chip-icon { opacity: .7; }', + '.cuckoo-produced-chip[data-status="denied"] { border-color: rgba(251,191,36,0.45); background: rgba(60,45,15,0.6); }', + '.cuckoo-produced-chip[data-status="denied"] .cuckoo-produced-chip-icon { opacity: .7; }', + ".cuckoo-produced-chip-name { white-space: nowrap; }", + // ========== RGB-переливание ника пользователя ========== + // Переливание включается, если у нет класса `cuckoo-rgb-off`. + // Управляется настройкой rgbUsername из settings.json. + "body:not(.cuckoo-rgb-off) ._9d8da05 {", + " background: linear-gradient(90deg, #ff4d4d, #ffa64d, #ffee4d, #4dff88, #4dd2ff, #8a4dff, #ff4dd2, #ff4d4d);", + " background-size: 400% 100%;", + " -webkit-background-clip: text;", + " background-clip: text;", + " -webkit-text-fill-color: transparent;", + " color: transparent;", + " animation: cuckoo-rgb-username 5s linear infinite;", + " font-weight: 600;", + "}", + "@keyframes cuckoo-rgb-username {", + " 0% { background-position: 0% 50%; }", + " 100% { background-position: 400% 50%; }", + "}", + // ========== RGB-переливание приветственного текста на стартовой странице ========== + // Приветствие — без класса, всегда идёт сразу после SVG с классом .ce41ed1b + // внутри общего контейнера. Цепляемся к этому span через :has(). + "body:not(.cuckoo-rgb-off) :is(._6c7e7df, div:has(> svg.ce41ed1b)) > span {", + " background: linear-gradient(90deg, #ff4d4d, #ffa64d, #ffee4d, #4dff88, #4dd2ff, #8a4dff, #ff4dd2, #ff4d4d);", + " background-size: 400% 100%;", + " -webkit-background-clip: text;", + " background-clip: text;", + " -webkit-text-fill-color: transparent;", + " color: transparent;", + " animation: cuckoo-rgb-username 5s linear infinite;", + "}", + // ========== RGB-переливание кнопки «Повторить» (retry под ошибкой ответа) ========== + // Класс ds-button--warning стабильный (не хешированный). Ограничение .ds-message + // гарантирует, что не затронутся warning-кнопки в других местах (например, в настройках). + ".ds-message .ds-button--warning .ds-button__background,", + ".ds-message .ds-button--warning {", + " background: linear-gradient(90deg, #ff4d4d, #ffa64d, #ffee4d, #4dff88, #4dd2ff, #8a4dff, #ff4dd2, #ff4d4d) !important;", + " background-size: 400% 100% !important;", + " animation: cuckoo-rgb-retry 5s linear infinite !important;", + "}", + "@keyframes cuckoo-rgb-retry {", + " 0% { background-position: 0% 50%; }", + " 100% { background-position: 400% 50%; }", + "}", + // ========== Матовое стекло для плашки «Размышление N секунд» (._5ab5d64) ========== + // backdrop-filter не сработает: между плашкой и body стоят непрозрачные предки, + // а виртуализированный список DeepSeek использует transform (ломает fixed-attachment). + // Поэтому стекло рисуем сами: псевдоэлемент показывает тот же фон, что на странице, + // размытый и спозиционированный под вьюпорт. Координаты считает reasoning-glass.js. + "._5ab5d64 {", + " position: relative !important;", + " overflow: hidden !important;", + " border-radius: 10px !important;", + " border: 1px solid rgba(139, 147, 255, 0.22) !important;", + " background: transparent !important;", + " isolation: auto !important;", + " transform: none !important;", + " backdrop-filter: none !important;", + " -webkit-backdrop-filter: none !important;", + "}", + // Гасим фоны у прямого предка и у потомков плашки: DeepSeek рисует свои + // тёмные слои (обёртка _245c867._34a54ec, иконки, span), и они просвечивают + // через полупрозрачную плашку, давая «чёрную» область слева. + "._245c867._34a54ec,", + "._74c0879,", + "._245c867._34a54ec > *,", + "._5ab5d64 * {", + " background: transparent !important;", + " backdrop-filter: none !important;", + " -webkit-backdrop-filter: none !important;", + "}", + "._5ab5d64::before {", + ' content: "" !important;', + " position: absolute !important;", + " inset: calc(-1 * var(--cuckoo-plate-pad, 40px)) !important;", + " background-image: var(--cuckoo-bg-image, none) !important;", + " background-size: var(--cuckoo-plate-bg-size, cover) !important;", + " background-repeat: no-repeat !important;", + " background-position: var(--cuckoo-plate-bg-pos, 0 0) !important;", + " filter: blur(var(--cuckoo-reasoning-blur, 12px)) !important;", + " -webkit-filter: blur(var(--cuckoo-reasoning-blur, 12px)) !important;", + " pointer-events: none !important;", + " z-index: 0 !important;", + "}", + "._5ab5d64::after {", + ' content: "" !important;', + " position: absolute !important;", + " inset: 0 !important;", + " background: rgba(15, 18, 32, var(--cuckoo-reasoning-opacity, 35%)) !important;", + " pointer-events: none !important;", + " z-index: 0 !important;", + "}", + "._5ab5d64 > * {", + " position: relative !important;", + " z-index: 1 !important;", + "}", + // ========== Панель «Изменения» (git) ========== + "#cuckoo-diff-panel { width: 560px; max-width: 92vw; height: 72vh; }", + ".cuckoo-diff-body { overflow: hidden; padding: 0 !important; }", + ".cuckoo-diff-list { flex: 1; overflow-y: auto; padding: 10px 12px; display: flex; flex-direction: column; gap: 4px; }", + ".cuckoo-diff-file {", + " display: flex; align-items: center; gap: 8px; padding: 7px 10px;", + " border-radius: 8px; cursor: pointer; transition: background 0.15s;", + " font-size: 12px;", + "}", + ".cuckoo-diff-file:hover { background: rgba(139,147,255,0.15); }", + ".cuckoo-diff-file.active { background: rgba(139,147,255,0.22); }", + ".cuckoo-diff-file-badge {", + " width: 18px; text-align: center; font-weight: 700; font-size: 11px;", + ' flex-shrink: 0; font-family: "Consolas", monospace;', + "}", + ".cuckoo-diff-file-badge.modified { color: #ffc107; }", + ".cuckoo-diff-file-badge.added { color: #4ade80; }", + ".cuckoo-diff-file-badge.deleted { color: #ff6b7a; }", + ".cuckoo-diff-file-badge.renamed { color: #8b93ff; }", + ".cuckoo-diff-file-badge.untracked { color: #86efac; }", + ".cuckoo-diff-file-badge.changed { color: #8a90b8; }", + ".cuckoo-diff-file-path {", + " flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;", + ' font-family: "Consolas", monospace; color: #c8ccff; direction: rtl; text-align: left;', + "}", + // Вкладки «Изменения» / «История» + ".cuckoo-diff-tabs {", + " display: flex; gap: 4px; padding: 8px 12px 0; flex-shrink: 0;", + " border-bottom: 1px solid var(--ck-border);", + "}", + ".cuckoo-diff-tab {", + " background: none; border: none; color: #8a90b8; font-size: 12px; font-weight: 600;", + " padding: 6px 12px; cursor: pointer; border-bottom: 2px solid transparent;", + " transition: color 0.15s, border-color 0.15s;", + "}", + ".cuckoo-diff-tab:hover { color: #c8ccff; }", + ".cuckoo-diff-tab.active { color: #a8afff; border-bottom-color: #8b93ff; }", + ".cuckoo-commit-item {", + " display: flex; flex-direction: column; gap: 2px; padding: 8px 10px;", + " border-radius: 8px; cursor: pointer; transition: background 0.15s;", + "}", + ".cuckoo-commit-item:hover { background: rgba(139,147,255,0.15); }", + ".cuckoo-commit-subject-line { font-size: 12px; color: #c8ccff; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }", + '.cuckoo-commit-meta-line { font-size: 10.5px; color: #5d6280; font-family: "Consolas", monospace; }', + ".cuckoo-commit-meta-line .cuckoo-commit-hash { color: #8b93ff; }", + ".cuckoo-commit-files { display: flex; flex-direction: column; flex: 1; overflow: hidden; }", + ".cuckoo-commit-files-header {", + " display: flex; align-items: center; gap: 8px; padding: 8px 12px;", + " border-bottom: 1px solid var(--ck-border); flex-shrink: 0;", + "}", + ".cuckoo-commit-subject { flex: 1; font-size: 12px; color: #c8ccff; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }", + ".cuckoo-diff-viewer {", + " position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%);", + " z-index: 2147483647; width: 92vw; max-width: 1100px; height: 84vh;", + " background: rgba(17, 19, 34, 0.99);", + " border: 1px solid var(--ck-border); border-radius: 16px;", + " display: flex; flex-direction: column; overflow: hidden;", + " box-shadow: 0 20px 60px rgba(0, 0, 0, 0.65), 0 0 0 1px rgba(139, 147, 255, 0.1);", + " transition: opacity 0.18s ease, transform 0.18s ease;", + "}", + ".cuckoo-diff-viewer.cuckoo-hidden { opacity: 0; pointer-events: none; transform: translate(-50%, -50%) scale(0.96); }", + ".cuckoo-diff-viewer-body { flex: 1; overflow: auto; background: rgba(0,0,0,0.25); padding: 8px 0; }", + ".cuckoo-diff-code {", + ' font-family: "Cascadia Code", "Consolas", monospace; font-size: 12px; line-height: 1.55;', + " margin: 0; padding: 0; white-space: pre;", + "}", + ".cuckoo-diff-line { display: flex; }", + ".cuckoo-diff-line .cuckoo-diff-text { flex: 1; padding: 0 12px; white-space: pre; }", + ".cuckoo-diff-line.add { background: rgba(74,222,128,0.10); }", + ".cuckoo-diff-line.add .cuckoo-diff-text { color: #86efac; }", + ".cuckoo-diff-line.del { background: rgba(255,107,122,0.10); }", + ".cuckoo-diff-line.del .cuckoo-diff-text { color: #ff9aa5; }", + ".cuckoo-diff-line.hunk { background: rgba(139,147,255,0.10); }", + ".cuckoo-diff-line.hunk .cuckoo-diff-text { color: #a8afff; }", + ".cuckoo-diff-line.meta .cuckoo-diff-text { color: #5d6280; }", + ".cuckoo-diff-line.ctx .cuckoo-diff-text { color: #8a90b8; }", + ".cuckoo-diff-empty { padding: 20px; color: #8a90b8; font-size: 13px; text-align: center; }", + // ========== Окно «Cookie Code» с задачами (слева) ========== + ".cuckoo-todo-panel {", + " position: fixed; top: 90px; left: 20px; z-index: 2147483646;", + " width: 380px; max-height: 68vh; display: flex; flex-direction: column;", + " background: rgba(17, 19, 34, var(--cuckoo-overlay-bg-opacity, 0.72));", + " backdrop-filter: blur(var(--cuckoo-sidebar-blur, 12px)) saturate(140%);", + " -webkit-backdrop-filter: blur(var(--cuckoo-sidebar-blur, 12px)) saturate(140%);", + " border: 1px solid var(--ck-border); border-radius: 16px;", + " box-shadow: 0 12px 40px rgba(0,0,0,0.45), 0 0 0 1px rgba(139,147,255,0.06);", + ' font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;', + " color: var(--ck-text); font-size: 13px; line-height: 1.55; overflow: hidden;", + " transition: opacity 0.4s ease, transform 0.4s ease;", + "}", + ".cuckoo-todo-panel.cuckoo-hidden { display: flex !important; opacity: 0; pointer-events: none; transform: translateX(-10px); }", + ".cuckoo-todo-tab {", + " position: fixed; top: 90px; left: 0; z-index: 2147483646;", + " display: flex; align-items: center; gap: 6px; padding: 8px 9px;", + " color: var(--ck-text); background: rgba(17, 19, 34, var(--cuckoo-overlay-bg-opacity, 0.72));", + " border: 1px solid var(--ck-border); border-left: 0; border-radius: 0 8px 8px 0;", + " box-shadow: 0 8px 26px rgba(0,0,0,0.35); cursor: pointer;", + ' font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;', + " font-size: 12px; font-weight: 700; line-height: 1; transition: opacity 0.2s ease, transform 0.2s ease, background 0.15s ease;", + "}", + ".cuckoo-todo-tab:hover { background: rgba(28, 31, 54, 0.9); transform: translateX(2px); }", + ".cuckoo-todo-tab.cuckoo-hidden { display: none !important; }", + ".cuckoo-todo-tab-counter { color: var(--ck-text-dim); font-size: 11px; font-weight: 600; }", + ".cuckoo-todo-header {", + " display: flex; align-items: center; justify-content: space-between; gap: 8px;", + " padding: 12px 16px 10px; flex-shrink: 0; cursor: grab; user-select: none;", + " border-bottom: 1px solid var(--ck-border);", + " background: rgba(255,255,255,0.04);", + " backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px);", + "}", + ".cuckoo-todo-header:active { cursor: grabbing; }", + ".cuckoo-todo-header-right { display: flex; align-items: center; gap: 8px; }", + ".cuckoo-todo-title { font-size: 14px; font-weight: 700; color: #c8ccff; letter-spacing: 0.4px; }", + ".cuckoo-todo-counter { font-size: 11px; color: var(--ck-text-dim); font-weight: 600; }", + ".cuckoo-todo-body { padding: 10px 12px; overflow-y: auto; flex: 1; }", + ".cuckoo-todo-list { display: flex; flex-direction: column; gap: 2px; }", + ".cuckoo-todo-item { display: flex; gap: 7px; align-items: flex-start; padding: 2px 0; }", + ".cuckoo-todo-icon { flex-shrink: 0; width: 14px; text-align: center; color: #8a90b8; }", + ".cuckoo-todo-text { flex: 1; word-break: break-word; }", + ".cuckoo-todo-item.completed .cuckoo-todo-text { color: #8a90b8; text-decoration: line-through; }", + ".cuckoo-todo-item.completed .cuckoo-todo-icon { color: #4ade80; }", + ".cuckoo-todo-item.in_progress .cuckoo-todo-text { color: #fff; font-weight: 600; }", + ".cuckoo-todo-item.in_progress .cuckoo-todo-icon { color: #8b93ff; }", +].join("\n"); module.exports = { buildOverlayHTML, OVERLAY_CSS };