Поселил ИИ-агента в чёлку макбука: он слушает мой день и раздаёт задачи сессиям Claude Code

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

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

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

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

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

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

Где

Что делает

Подробности

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

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

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

Чат на сайте

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

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

Telegram

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

—

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

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

эта статья

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

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

  • Слушает. ⌥⌘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 секунд, значит, сессии закончили работу и можно размяться.

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

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

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

  • Сутки от «хочу себе чёлку» до публикации. Из 4900 строк 3300 на Swift, 1200 на JavaScript и 400 в скриптах.

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

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

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

Попробовать

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

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

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

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

Источник: https://habr.com/ru/articles/1097006/


Интересные статьи

Интересные статьи

В результате проведенного мной исследования безопасности 3000 российских frontend-приложений было обнаружено, что CMS Битрикс более 11 лет передает персональные данные посетителей сайтов на территорию...
Привет, Хабр! Сегодня поговорим о том, как расшифровывать звонки с клиентами через CRM-систему Битрикс24 вместо CoPilot. Для автоматизации подключим платформу МТС Exolve. Вы сможете получать все запис...
Можно было купить готовый модуль, несмотря на его совершенно конскую стоимость в 6-7 тыс. рублей. Однако он не предоставлял наиболее востребованную для меня функциональность: не позволял открывать вор...
Если с тестированием привычных программных продуктов более-менее ясно, то вот с BigData возникает множество вопросов. Если у вас Java - у вас есть как минимум JUnit, а абсолютное большинство фрей...
Основанная в 1998 году компания «Битрикс» заявила о себе в 2001 году, запустив первый в России интернет-магазин программного обеспечения Softkey.ru.