Маскот для Claude Code: как узнать, что ассистент ждёт ответа, и не пропустить уведомление на Windows

Я часто даю Claude Code длинную задачу и ухожу в соседнее окно. Через десять-пятнадцать минут возвращаюсь и вижу, что он давно упёрся в вопрос «можно выполнить эту команду?» и всё это время просто ждал. Системное уведомление Windows, конечно, показывалось, но на пять секунд в углу, пока я смотрел в другую сторону.

Подсказывать себе таймером не хотелось, нужна была штука, которую невозможно не заметить. В итоге получился маскот: маленький оранжевый человечек, который выглядывает из правого нижнего угла экрана, а если его игнорировать, выходит целиком, краснеет и начинает топать по панели задач. Ниже расскажу, как это устроено. Основные сложности оказались не в анимации, а в двух вещах: как вообще узнать, что Claude ждёт, и как нарисовать окно, которое видно поверх всего, но ничему не мешает.

Код открыт, MIT: https://github.com/OlegChe77/claude-mascot. Стек: Electron и немного WinAPI через koffi.

Как узнать, что Claude ждёт

Это главный вопрос, и ответ зависит от того, чем вы пользуетесь.

Claude Code в терминале: хуки

У Claude Code есть хуки: в ~/.claude/settings.json можно описать команду, которую запустят на определённом событии, а на stdin ей придёт JSON с описанием. Нужное событие называется Notification: оно срабатывает, когда Claude хочет привлечь внимание. Хук можно сделать совсем лёгким:

{ "hooks": { "Notification": [ { "hooks": [ { "type": "command", "command": "curl -s -m 2 --data-binary @- http://127.0.0.1:47821/hook || exit 0" } ] } ] }}

Программа слушает HTTP на 127.0.0.1:47821, и хук просто отправляет ей то, что пришло на stdin. Часть || exit 0 нужна для того, чтобы при незапущенном маскоте команда молча завершалась с нулём: иначе у Claude Code на каждом событии вылезала бы ошибка хука. Заодно такая команда одинаково работает и в bash, и в cmd.

Запросы с заголовком Origin сервер отбрасывает. Это защита от того, чтобы произвольная веб-страница в браузере не могла постучаться в локальный порт и вызвать маскота. Ничего критичного там нет, но привычка проверять такие вещи не повредила.

Десктопный Claude: события, которых нет

С терминальной версией всё просто. Десктопное приложение (вкладка Code) устроено хитрее: когда оно просит разрешение на инструмент или задаёт вопрос, событие Notification оно не отправляет. Об этом я узнал не сразу, потому что в терминале всё работало, а на десктопе маскот молчал.

Пришлось добавить ещё два события. PermissionRequest ловит запрос разрешения, а PreToolUse с matcher AskUserQuestion|ExitPlanMode ловит инструменты, которые сами задают вопрос или просят утвердить план. Есть и необязательный Stop, он срабатывает после каждого ответа, кому-то это нужно.

Хуки маскот ставит сам при запуске. Перед записью он кладёт рядом копию settings.json.mascot-*.bak (хранятся пять последних) и не трогает чужие хуки. Ещё нюанс: уже открытые сессии могут не подхватить новые хуки до перезапуска, поэтому для десктопа нужен второй источник сигнала.

Уведомления Windows как источник правды

Второй источник — сами уведомления. Windows пишет каждую всплывающую плашку в SQLite-базу %LOCALAPPDATA%MicrosoftWindowsNotificationswpndatabase.db. Достаточно раз в секунду открывать её на чтение и смотреть, не появилась ли запись с идентификатором приложения вида Claude_*!Claude:

const { DatabaseSync } = require('node:sqlite');const db = new DatabaseSync(DB, { readOnly: true });const ids = db.prepare( "select PrimaryId as id from NotificationHandler where PrimaryId glob 'Claude_*!Claude'").all();

Модуль node:sqlite встроен в Node, поэтому нативных зависимостей и сборки не потребовалось. Такой способ работает и для уже запущенного Claude, ничего в него не внедряя.

Окно, которое видно, но не мешает

Это вторая половина задачи. Нужно окно, которое:

  • прозрачное и без рамки;

  • лежит поверх всех окон, в том числе полноэкранных приложений;

  • не появляется в панели задач;

  • не забирает фокус (иначе во время набора текста ввод уйдёт в маскота);

  • пропускает клики на всё, кроме самого маскота.

В Electron большая часть этого — настройки BrowserWindow:

win = new BrowserWindow({ frame: false, transparent: true, backgroundColor: '#00000000', hasShadow: false, skipTaskbar: true, alwaysOnTop: true, focusable: false, /* ... */});win.setAlwaysOnTop(true, 'screen-saver');win.setIgnoreMouseEvents(true, { forward: true });

Самое интересное — последняя строка. setIgnoreMouseEvents(true, { forward: true }) делает окно «прозрачным» для кликов, но продолжает пересылать события движения мыши странице. Поэтому страница узнаёт, что курсор наведён на маскота (событие mouseenter), и тогда клики включаются обратно; когда курсор уходит, снова выключаются. Без этого пришлось бы вычислять форму окна вручную.

Окно растянуто на всю ширину рабочей области и имеет высоту 190 пикселей над панелью задач. Маскот просто «стоит» на панели задач, а сама анимация с топаньем и паром из головы — обычный SVG и CSS. Звук тоже ничего не весит: через WebAudio синтезируется «бормотание», а рот открывается по слогам. Чтобы звук играл без жеста пользователя, пришлось включить autoplayPolicy: ‘no-user-gesture-required’.

Что делает koffi

Чтобы понять, активно ли окно Claude, и вернуть его на передний план по клику, нужен WinAPI: GetForegroundWindow, поиск окна, SetForegroundWindow. Для этого я взял koffi: он вызывает функции из user32.dll без нативной сборки и без node-gyp. Правило «Claude активен» простое: процесс claude.exe или слово «claude» в заголовке окна. Если оно спрятано в трей, клик запускает claude.exe ещё раз, и приложение разворачивает существующее окно.

Активное окно опрашивается каждые 300 мс. Для таймера такой частоты нагрузка незаметна.

Уведомления Windows мешают сами себе

Если маскот показывается, а Windows параллельно рисует обычную плашку, получается два напоминания сразу, и это раздражает. Поэтому есть опция «Заменять плашку Claude маскотом», включённая по умолчанию. Она выключает баннеры только для Claude. Для этого в реестре ставится ShowBanner = 0 в HKCUSoftwareMicrosoftWindowsCurrentVersionNotificationsSettings. Записи в центре уведомлений при этом остаются, и база wpndatabase.db продолжает пополняться, так что маскот их по-прежнему видит.

Тут важно вернуть всё как было. Прежнее значение (или факт, что значения не было) сохраняется в конфиге и возвращается при выключении опции, при выходе, при завершении сеанса Windows и при следующем старте, если программа до этого упала. Прямо в реестр я пишу через reg.exe, так проще, чем тянуть ещё одну зависимость.

Поведение

Сценарий взят из небольшого HTML-демо, которое я нарисовал раньше самой программы, чтобы отладить характер:

  1. Три выглядывания из угла, в каждом по три фразы, пауза между выглядываниями 7 секунд. В первых двух маскот спокоен, на третьем хмурится.

  2. Потом он выходит целиком на 20 секунд: краснеет, ходит по низу экрана, топает, трясёт кулаками.

  3. Уходит и повторяет всё заново, пока пользователь не откликнется.

  4. Как только окно Claude стало активным или вы кликнули по маскоту, он убегает, цикл сбрасывается.

Фразы выбираются случайно, без повторов подряд.

Обновления

Программа собирается electron-builder в установщик NSIS и portable-файл. Установщик использует electron-updater с GitHub Releases: проверка через 15 секунд после запуска и потом раз в 6 часов, новая версия скачивается в фоне и ставится при перезапуске. Portable заменить себя не может (он запущен из временной распаковки), поэтому только сообщает о новой версии и открывает страницу выпуска.

Выпуск делается тегом v: GitHub Actions собирает и публикует релиз. Тут была своя ловушка: electron-builder загружает файлы параллельно и иногда создаёт дубли релиза, так что workflow сначала создаёт релиз командой gh release create, а потом уже отправляет файлы.

Автозапуск для portable тоже оказался с подвохом: process.execPath указывает на временную папку распаковки, поэтому путь берётся из переменной PORTABLE_EXECUTABLE_FILE и перерегистрируется при каждом старте.

Что в меню

Размер (1, 1.5, 2; по умолчанию 1.5), звук, автозапуск, язык (русский, английский или как в Windows), все переключатели сигналов, проверка обновлений. Для страховки есть пункт «Показать тест».

Установка

Ставить нужно либо установщик, либо один portable-файл. Оба на странице релиза:

  • https://github.com/OlegChe77/claude-mascot/releases/latest

  • исходники: https://github.com/OlegChe77/claude-mascot

Программа бесплатная, MIT. Если у вас есть идеи, какие ещё события Claude стоит ловить или какие фразы добавить, пишите в комментариях или создавайте Issue.

Источник: habr.com

0 0 голоса
Рейтинг новости
1
0
Подписаться
Уведомить о
0 комментариев