У меня есть ИИ‑агент Андреич. Он берёт трубку на моей телефонной линии, отвечает в чате на сайте и сидит в 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.
Если вы тоже селили что‑то в вырез экрана, расскажите в комментариях, как у вас приём файлов. Подозреваю, что моё окно‑ловушка — не единственный способ.
Wesha
(Зачарованно:) Так вот она какая, e-нотная запись!