У меня есть ИИ‑агент Андреич. Он берёт трубку на моей телефонной линии, отвечает в чате на сайте и сидит в Telegram. На днях он переехал ещё и в вырез экрана макбука, который в народе зовут чёлкой. Там он стал пушистым 3 D‑енотом. Слушает меня весь день, ведёт протокол, вытаскивает из болтовни задачи и по моему «ок» отправляет их в нужную сессию Claude Code. Заодно показывает, сколько недельного лимита подписки можно потратить сегодня.

Приложение открытое, лицензия MIT. Внутри Swift без Xcode и three.js, около 4 900 строк. От первой просьбы до публикации на GitHub прошли сутки, и весь код написан в диалоге с Claude Code. Руками я не правил ни строчки. Ниже о том, как оно устроено, и о трёх местах, где я наступил на грабли. Самые дорогие из них стоили мне часа записи: распознавание речи отвалилось молча, а микрофон продолжал гореть.

Андреич-кругляш: так он выглядывает из чёлки
Андреич‑кругляш: так он выглядывает из чёлки

Один агент, четыре места

Имя и характер у Андреича одни, а начинка в каждом месте своя.

Где

Что делает

Подробности

Телефонная линия

Берёт трубку, когда я занят. Выслушивает, записывает заявку, срочное переводит на меня. Запись и расшифровка уходят в Telegram

как он понимает, что собеседник договорил: первая статья; как читает числа и даты: вот здесь

Чат на сайте

Отвечает про услуги. Цены и клиентов под NDA не называет: это правило проверяет код

«Промпт — не контракт»

Telegram

Сессия Claude Code на моём маке, подключённая к боту официальным плагином‑каналом. Пишу ему откуда угодно, а он работает в моих проектах

—

Чёлка макбука

Слушает, ведёт протокол, раздаёт задачи, следит за лимитами

эта статья

Что умеет енот в чёлке

  • Живёт в вырезе. Наводишь мышь, и вырез раскрывается в панель. В покое из «крыла» выглядывает 3 D‑морда.

  • Слушает. ⌥⌘M включает и выключает микрофон из любого приложения. Русская речь распознаётся прямо на маке, протокол дня пишется в markdown, звук параллельно сохраняется в m4a.

  • Превращает слова в дела. Раз в пару минут Андреич перечитывает свежий протокол и предлагает карточки. Сказал «надо в семь поставить будильник» — галочка кладёт напоминание в системные «Напоминания». Сказал «в проекте сайта надо поправить форму» — он пишет готовый промпт и спрашивает в Telegram, отправлять ли.

  • Сводка дня собирается по кнопке, а в 21:30 приходит в Telegram сама, если за день что‑то было сказано: о чём говорили, что решили, что повисло.

  • Показывает лимиты подписки: сколько недельного лимита осталось и сколько можно потратить сегодня.

  • Держит полку для файлов. Перетащил файл на чёлку — он лежит там копией, вытащил — скопировался.

  • Гуляет. Когда все сессии Claude Code затихли, енот выпрыгивает из чёлки и идёт вдоль дока. Началась работа — молча возвращается.

Окно поверх строки меню

Система не даёт приложениям никакого места в вырезе. Чёлка — обычное окно без рамки, которое лежит выше строки меню и сливается с вырезом по цвету и форме. Приём я подсмотрел в открытом Atoll (MIT), код написан заново:

panel = NotchPanel(contentRect: .zero, styleMask: [.borderless, .nonactivatingPanel], backing: .buffered, defer: true)
panel.collectionBehavior = [.canJoinAllSpaces, .fullScreenAuxiliary, .stationary, .ignoresCycle]
panel.level = NSWindow.Level(rawValue: NSWindow.Level.mainMenu.rawValue + 3)

nonactivatingPanel нужен, чтобы клик по чёлке не забирал фокус у приложения, в котором я работаю. canJoinAllSpaces и fullScreenAuxiliary держат её на всех рабочих столах и поверх полноэкранных окон. На маках без выреза панель живёт просто у верхнего края экрана.

Грабли первые: распознавание умерло, а микрофон горел

Речь распознаёт системный SFSpeechRecognizer с флагом requiresOnDeviceRecognition. Это тот же движок, что у диктовки macOS: звук никуда не уходит. Один запрос на распознавание нельзя держать бесконечно (система ограничивает его примерно минутой), поэтому приложение закрывает его и открывает новый раз в 50 секунд, а законченные фразы дописывает в протокол.

В первой версии я включил микрофон перед длинным разговором и проговорил около часа. В протоколе оказалось пять строк за первую минуту, дальше пусто. Звук тогда никуда не сохранялся, так что восстановить было нечего.

Причина нашлась в коде. При смене запроса старая задача распознавания не снималась. Задачи копились, и через несколько циклов система перестала отдавать результаты новым, причём без ошибки и без вызова обработчика. Снаружи всё выглядело живым: индикатор горит, уровень звука прыгает.

Починка в одну строку:

req.requiresOnDeviceRecognition = true
task?.cancel()                 // старую задачу обязательно снимаем, иначе новые перестают запускаться
task = recognizer?.recognitionTask(with: req) { [weak self] result, error in

Одной строки мне было мало. Если сломалось один раз молча, сломается и ещё, поэтому вокруг появилась проверка. Раз в 0,4 секунды она смотрит, что звук идёт и текст приходит:

// звук есть, а результатов нет дольше 15 секунд — значит задача умерла молча
if partial.isEmpty, now.timeIntervalSince(sink.lastLoudAt) < 3, now.timeIntervalSince(lastResult) > 15 {
    deadRestarts += 1
    if deadRestarts >= 3 { deadRestarts = 0; restartEngine(reason: "распознавание молчит при живом звуке") }
    else { VoiceLog.write("распознавание молчит при живом звуке — новый запрос"); beginRequest() }
}

Что ещё поменялось после этого часа:

  • Звук пишется в файл параллельно с распознаванием, около 20 МБ в час. Даже если распознавание снова споткнётся, запись останется, и расшифровать её можно потом.

  • Появился журнал voice.log: включение, выключение, ошибки, перезапуски. Гадать больше не приходится.

  • В самом протоколе курсивом видно, когда микрофон включили, выключили или распознавание перезапустилось. Пропуск в записи заметен сразу.

  • Микрофон переживает перезапуск приложения: если он был включён, после старта включится снова и оставит отметку.

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

Как болтовня становится задачей

Раз в пару минут приложение берёт свежие строки протокола и список открытых сессий Claude Code. Названия сессий оно достаёт из файлов переписок в ~/.claude/projects. Всё это уходит в claude -p --model haiku с просьбой вернуть карточки строго в таком виде:

{"type":"напоминание"|"задача"|"идея"|"промт","title":"до 6 слов","detail":"суть одним предложением","time":"HH:MM или пусто","session":"имя сессии или пусто","prompt":"готовый промт или пусто"}

Модель haiku идёт через мою же подписку Claude Code и почти не тратит лимит. И это единственное место, где что‑то уходит в сеть: кусок текста протокола. Звук остаётся на диске.

Карточку с промптом для сессии Андреич не запускает сам. Он присылает её в Telegram:

Андреич услышал задачу для сессии «сайт»:

<готовый промпт>

Ответь «ок 3» — отправлю в работу. «нет 3» — забуду.

Подтверждение принимается только из моего чата. Остальные сообщения бот пропускает. После «ок» промпт уходит в ту самую сессию, в её рабочую папку:

p.arguments = ["--resume", sessionId, "-p", promptText]
p.currentDirectoryURL = URL(fileURLWithPath: cwd)

Если задача не закончилась за 30 минут, процесс снимается. Подтверждение нужно потому, что распознавание и модель иногда ошибаются. Услышанная краем уха фраза не должна сама менять код в проекте, пока я не прочитал, что именно уйдёт в работу.

Если тот же Telegram‑бот уже слушает другая программа, getUpdates отвечает ошибкой 409, и подтверждения перестают доходить. Приложение пишет это в журнал прямо так: бота уже слушает кто‑то другой.

Лимиты: сколько можно потратить сегодня

Я работаю на подписке, и недельный лимит у меня обнуляется в понедельник в шесть утра. Мне нужен был ответ на вопрос «сколько можно потратить сегодня, чтобы хватило до конца недели», причём так, чтобы к воскресенью оставалось 3–5% на всякий случай.

Дневная норма считается так:

норма на сегодня = (100 − потрачено к 6:00 сегодняшнего дня − 4 % запаса) / суток до сброса

Пример с того дня, когда я это делал: к утру потрачено 18%, осталось 82%. Минус 4% запаса — 78% на семь суток, то есть 11,1% в день. Что потрачено сегодня, считается от уровня на шесть утра. Если мак ночью спал, за точку отсчёта берётся последнее значение, которое приложение видело накануне.

Цифры те же, что показывает команда /usage в Claude Code. Скрипт берёт токен Claude Code из связки ключей и спрашивает тот же адрес, что и она. Адрес не документирован, так что сломаться это может в любой день. Обновление раз в 90 секунд.

Блок лимитов в панели и полоска в закрытой чёлке
Блок лимитов в панели и полоска в закрытой чёлке

Видно лимиты в трёх местах:

  • в закрытой чёлке это полоска: длина показывает остаток недели, а цвет — как идёт сегодняшний день (бирюзовый — в норме, жёлтый — норма почти выбрана, красный — перерасход);

  • в раскрытой панели это блок с недельной и дневной шкалой, пятичасовым окном и временем до сброса;

  • в самом Claude Code это строка статуса под полем ввода.

Грабли вторые: файлы «падали» в чёлку, но не долетали

Полка для файлов какое‑то время вроде бы работала. Потом я пожаловался, что мешает соседнее приложение Dropover, его выключили, и перетаскивание сломалось. Claude поднял журнал с самого начала и показал неприятное: ни один файл ни разу не долетал до чёлки. Их всё это время перехватывала шторка Dropover, и казалось, что приём работает.

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

/// Невидимое окно-ловушка для перетаскивания: появляется над чёлкой только пока
/// в системе тащат что-то с зажатой кнопкой, и исчезает сразу после. Обычное окно
/// без хитрых флагов — системе не к чему придраться.
window = NSWindow(contentRect: .zero, styleMask: [.borderless], backing: .buffered, defer: true)
window.level = NSWindow.Level(rawValue: NSWindow.Level.mainMenu.rawValue + 4)
window.collectionBehavior = [.canJoinAllSpaces, .fullScreenAuxiliary]

Грабли третьи: мне сделали тамагочи

Я попросил поселить в чёлке Андреича, который бегает по экрану. Первая версия получилась игрой: енота надо было кормить и гладить, у него были потребности. Игру я не просил, мне был нужен помощник. Кормёжку убрали, главными стали голос, протокол и задачи.

Потом был енот из анимированной картинки, но она качалась целиком, как наклейка на ветру. В итоге его собрали кодом на three.js, без готовых моделей:

  • Мех сделан оболочками (shell fur). Каждая часть тела рисуется одним InstancedMesh из 12–32 слоёв. Каждый слой чуть больше предыдущего, а шейдер по шуму решает, где на этом слое волосинка, а где пусто;

  • Плащ считается как ткань на verlet, хвост как пружинная цепочка, позы процедурные с плавными переходами;

  • Обликов три: пушистый кругляш, полный рост в куртке с эмблемой и плоский конструктивист из плашек для сайта.

function furMesh(geo, mat) {
  const layers = mat.uniforms.uLayers.value;
  const m = new THREE.InstancedMesh(geo, mat, layers + 1);
  m.frustumCulled = false;
  return m;
}

В покое всё это ест 2–3% одного ядра и около 90 МБ памяти.

Гулять енот уходит по простому признаку. Приложение смотрит, когда последний раз обновлялись файлы переписок в ~/.claude/projects. Если ни один не менялся 18 секунд, значит, сессии закончили работу и можно размяться.

Плоский облик заодно поселился на моём сайте, над кнопкой чата, но только на широких экранах. На телефонах 3 D не грузится, чтобы не тормозить страницу.

Полный рост, прогулка вдоль дока и панель с протоколом
Полный рост, прогулка вдоль дока и панель с протоколом

Что получилось

  • Сутки от «хочу себе чёлку» до публикации. Из 4 900 строк 3 300 на Swift, 1 200 на JavaScript и 400 в скриптах.

  • Самое полезное, что я делал, — жаловался конкретно: «я час говорил, где протокол», «файлы не перетаскиваются». После каждой такой жалобы в коде появлялись журнал и автоматическая проверка.

  • Приватность: речь распознаётся на маке, звук и протоколы лежат в ~/Library/Application Support/AndreichNotch/. В сеть уходит только текст для карточек и сводок, через мой же Claude Code.

Чего пока нет. Вызова по имени («Андреич, запиши…»): для этого микрофон пришлось бы держать включённым постоянно, и оранжевая точка в меню горела бы весь день. И голосовых ответов: енот пока молчит, говорит только тот Андреич, что на телефоне.

Попробовать

Нужны macOS 14+ и Swift из Command Line Tools, Xcode не нужен. Для сборки 3 D — Node.js. Карточки и сводки работают через установленный Claude Code с подпиской; без него остаются протокол и полка.

git clone https://github.com/OAIWEB/andreich.git
cd andreich
./scripts/build-app.sh
open Андреич.app

Репозиторий: github.com/OAIWEB/andreich.

Если вы тоже селили что‑то в вырез экрана, расскажите в комментариях, как у вас приём файлов. Подозреваю, что моё окно‑ловушка — не единственный способ.

Комментарии (1)


  1. Wesha
    21.10.2026 02:49

    (Зачарованно:) Так вот она какая, e-нотная запись!