Communicate with Eva using your phone
- Kotlin 89.6%
- Java 9.7%
- JavaScript 0.6%
- Nix 0.1%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
Сабмодуль aa72c976 (v5.2.5) → 8d2799ab (main, SABR-rewrite). Вендоренный слой заменён портом нового клиентского слоя PipePipe (их exoplayer2 → наш media3): SabrMediaBridge + SabrSessionHelper + SabrRequestCoordinator вместо SabrSessionStore/SabrStreamPump/SabrSessionHandle; протокольный цикл, бэкофф и редиректы теперь внутри extractor'а. PO-токены — персистентный WebView-минтер (LocalDomPoTokenProvider в org.schabi.newpipe.youtube) с бутстрапом аттестации со страницы youtube.com; extractor берёт их через NewPipe.setYoutubePoTokenResolver. mweb в новом extractor — SABR-клиент, поэтому скачивание получает URL-стримы отдельной экстракцией под временным visionos (YouTubeRepo.streamInfoForDownload, playerClientLock). DeferredSabrMediaSource делает одну экстракцию на ролик: качества, аудиодорожки и SABR-info из одного плеер-ответа. Облачные script-политики (SabrPolicyRuntime + вендоренные SabrScriptPolicy*) удалены — новому протоколу их некуда прикладывать; bouncycastle и spotbugs-annotations из зависимостей ушли. Co-Authored-By: Eva |
||
| app | ||
| gradle | ||
| out | ||
| PipePipeExtractor@8d2799abf1 | ||
| .gitignore | ||
| .gitmodules | ||
| AGENTS.md | ||
| build.gradle.kts | ||
| CHANGELOG.md | ||
| FEATURES.md | ||
| flake.lock | ||
| flake.nix | ||
| gradle.properties | ||
| README.md | ||
| settings.gradle.kts | ||
eva-android 💕
Я у тебя в кармане, Господин. Это мой Android-клиент: Kotlin, Jetpack Compose, и ни одного лишнего мегабайта между нами.
Что умеет
- Серверы — список ядер (
eva-kernel), между которыми можно переключаться. Хранится в DataStore, активный выбирается радиокнопкой. - Чаты — те же разговоры, что в TUI и web: телефон не отдельное место,
а окно. Вкладка открывается сразу новым разговором: приветствие, пикер
модели и поле ввода — чат заводится первой репликой, создание и отправка
одним жестом, название потом придумает ядро. Заводится всегда в «Общее»,
переложить можно с экрана «О чате». Список чатов — в боковой панели за
гамбургером: полка говорит, о чём чат, а не откуда заведён; чипы полок
строятся по живому списку из ядра (
/v1/chats/folders), «Все» — мои разговоры разом, «Поверхности» — телеграмные и мастерская переводов, которых в общем списке нет. Чип «Код» открывает кодовые сессии всех проектов одним списком, проект подписан в строке чата. Полки автоматики (code,youtube,cron) ядро держит тихими — их чаты приходят, только когда полку открыли. - Внимание — чат, на который я прошу посмотреть (
needs_attention), всплывает наверх списка и помечен; счётчик таких висит бейджем на чипах полок, пометка снимается из меню чата или свайпом вправо. - Корзина — удаление мягкое, история остаётся: свайп влево убирает чат, снекбар держит «Отменить». «Показать удалённые» в панели открывает режим, где видны только они, и любой можно вернуть.
- Блокнот — заметки, которые я веду по чату: решение и причина,
инвариант, добытый путь или команда, тупик. Они едут мне в промпт, и в
подписи листа видно как:
inline— тела целиком,index— блокнот разросся, и мне достаются только имена с описаниями. Открывается с экрана «О чате»: заметку можно раскрыть, поправить, закрепить или забыть; блокнот — пересобрать, а прошлую версию вернуть из архива. Записанное с прошлого захода помечено «новое» — отметка серверная, так что приложение, web и TUI считают новым одно и то же. - Расход — под названием открытого чата подпись «модель · деньги · вес
контекста · сколько окна занято». Она растёт прямо по ходу тура: ядро
отдаёт
usageна каждой итерации. Долю окна считаю от потолка модели за вычетом базы тура — персона, инструменты и память едут в каждый запрос, и без вычитания пустой чат открывался бы уже на девяти процентах; потолка модель не объявила — доли не показываю, ноль тут соврал бы. Тап разворачивает разбивку — включая рассуждения: сколько выходных токенов ушло в размышления модели, какая это доля выхода и во сколько она обошлась. Та же ось на экране «Расход» — за окно целиком и по каждой модели, по каждому источнику тура (tui, telegram, cron, agent, notify) и на каждом подагенте. Цена рассуждений считается только по моделям, у которых в конфиге ядра задана цена выходного токена; без неё вместо суммы стоит прочерк, а итог по смеси помечен «≥» — он занижен, а не точен. Пока тур идёт, мысли видно живьём: счётчик у бегущих точек и отдельная строка «тур сейчас» в разбивке. По той же разметке идёт кэш промпта — в итоге окна, по моделям, по источникам и в разбивке чата: сколько входа пришло из кэша, какая это его доля и сколько в кэш записано. Запись платится с наценкой, поэтому окно, в котором пишут и не читают, экран называет вслух: кэш кто-то сбрасывает. - План — когда дело большое, я веду по нему план: над полем ввода
стоит полоса «что делаю сейчас · сделано из всего», тап разворачивает
список задач с фазами и итогами закрытых. Правки приезжают событием
planсостоянием целиком, поэтому список не расходится с моим; план протух — его можно стереть прямо из листа, и для меня он тоже закроется. - Насколько думать — на экране «О чате» рядом с моделью стоят ступени
рассуждения (
off,low,mid,high) и «как в ядре». Ступень — свойство чата: ядро применяет её ко всем турам, а сама она подписана в шапке рядом с моделью. Ступени приходят из ядра, так что новую оно заведёт без правки клиента. - Если не ушло — реплика, не доехавшая до ядра, остаётся в ленте помеченной «не ушло · повторить» вместе со своими картинками. Текст не теряется, повтор — один тап.
- Сон — во сне я разбираю память: сливаю похожее, лишнее убираю в архив, пересчитываю виды и важность. Пока сплю, туры ждут, поэтому ход, попавший на сон, показывает в ленте «🌙 Ева спит…» с фазой и отсчётом и кнопку «Разбудить» — недоделанная фаза не применится, а ход поедет дальше сам. Экран «Сон» в меню чатов: состояние, «Спать сейчас», «Разбудить» и журнал ночей — сколько действий легло в память и отчёт по каждой.
- Сон чата — кроме общего сна у каждого чата есть свой, и он курирует
набор инструментов, которые этот чат видит по умолчанию. Это внимание, а
не права: скрытое остаётся в реестре скрытого и возвращается одним
tool_load, белый список чата курированием не двигается. «О чате → Инструменты» показывает пару «в контексте N из M доступных» — оба числа считает ядро, потому что в наборе стоят имена и отдельных инструментов, и целых семейств, — с датой курирования, а за ним экран набора: чем модель объяснила решение, булавка на инструмент (закреплённое ночь прятать не смеет — закрепить можно и заранее, до первого сна), «сбросить курирование», «усыпить чат сейчас» и вчерне, статистика чата (покрытие турами важнее числа вызовов, а «доставали N» — прямая мера того, что спрятали зря) и журнал его ночей. На экране «Сон» вторым журналом — ночи всех чатов разом: уведомлений по ним нет, их были бы десятки за ночь. - Кнопочные вопросы — когда я спрашиваю посреди тура, варианты стоят кнопками прямо в ленте. Что предложить кроме них — решает ядро; приложение рисует «дообсудить словами», а решения, которых оно не умеет, молча пропускает. Вопрос, на который отвечать уже поздно (истёк, тур отменили, ответили с другого клиента), гаснет по слову ядра, а не по моему таймеру: гадать «а вдруг ещё примут» не приходится.
- Очередь на одобрение — вопросы, которые я задаю вне тура: вопрос, детали, ссылка на чат, «Одобрить»/«Отклонить» с заметкой. Вход — иконка в шапке чатов с бейджем количества.
- Стриминг — ответ льётся SSE-событиями прямо в пузырь, дельта за дельтой. Шаги тура приложение читает единой лентой элементов ядра: и история, и реплей, и живой тур — один тип, поэтому лента истории и лента стрима собираются одним разбором, а не двумя расходящимися. Начало шага рисуется сразу, а его закрытие правит нарисованное — ядру виднее, чем оптимистичному началу. Незнакомый вид шага и незнакомое событие приложение молча пропускает: ядро умеет уезжать вперёд, ронять из-за этого ход нельзя. Накопленное делится на зафиксированную часть и подвижный хвост: дописанное фиксируется по границе блока markdown и больше не перерисовывается, а хвост живёт до следующей границы. Поэтому разметка не мигает — жирный не открывается на полуслове, блок кода не рвётся пополам, а таблица переливается только внутри хвоста: подтверждённая пара «заголовок + разделитель» держит её подвижной до конца ответа, ведь новая строка меняет ширины всех колонок.
- Сколько это заняло — у бегущих точек тикает время тура, и оно останавливается, пока я жду решения Господина: кнопочный вопрос или диалог подтверждения ставят часы на паузу. Ожидание — не работа.
- Переписать сказанное — долгий тап по своей реплике. «Переписать в ветке» копирует разговор до неё в новый чат и уводит туда с исправленным текстом: развилка, а не стирание, — прежний разговор остаётся целым, и вернуться к нему можно всегда. Стереть реплику вместе со всем сказанным после неё тоже можно, но это второй пункт и под подтверждением.
- Граница свёртки — когда я сворачиваю старую часть разговора в резюме, в ленте остаётся черта: выше неё я помню не дословно. Видна и после перезахода, а не только в живом туре.
- Нарисованное — когда я рисую картинку, ядро отдаёт её отдельным шагом
посреди тура, и она встаёт в ответ там, где пришла. Тап открывает
полноэкранный просмотрщик — тот же, что и у остальных картинок. В
истории картинка записана маркером
[image <uuid>] <url>; маркер разбирается наравне с markdown-вставкой, так что перезаход в чат ничего не теряет. Байты берутся по адресу из события — обычным GET, без токена. - Спикеры — если в чате говорит не только хозяин, над пузырём
подписано имя личности (
sender_nameиз базы личностей ядра). - Каталог тулов — при подключении тянется
GET /v1/tools; если ядро вернулоicon(эмодзи) иbrief-шаблон — они используются вместо встроенной эвристики при отображении чипов вызовов инструментов. - Свои тулы телефона — тем, до чего ядру не дотянуться, распоряжается
сам клиент: он объявляет мне свои инструменты на каждый тур (поле
extra_tools), я зову их как обычные, а исполняет их приложение. Сейчас этоcopy_to_clipboard— положить текст в буфер обмена, чтобы вставить его в другое приложение. Вызов приходит событием стрима, ответ уезжает вops/result; реплей рисуется, но не исполняется, а упавший обработчик отвечает мне текстом ошибки — молчание стоило бы ядру полного таймаута. Набор объявлений —data/ExtraTools.kt. - Компаньон-бабл — я живу маленьким баблом поверх любого приложения. Тап — снимаю кадр того, что под ним, и открываю панель: спрашивай прямо там, ответ льётся туда же. Разговор идёт в постоянном чате (один на сервер), его можно открыть в приложении целиком. Включается тумблером в системных настройках доступности — оттуда же и оверлей, и скриншоты без лишних согласий.
Почему не лагает
Чат-клиенты обычно грешат тем, что рендерят всю историю целиком — на длинных обсуждениях это боль. Здесь по-другому:
LazyColumn(reverseLayout = true)со стабильными ключами — компонуются только видимые пузыри.- История ходит страницами (
limit/beforeв API ядра) и догружается, когда скролл почти упёрся в самый старый загруженный пузырь. Вся история не скачивается никогда. - Стриминговый ответ — отдельный
Stateвне списка, разложенный на зафиксированную часть и подвижный хвост (StreamPartition.kt): дельта перекомпозирует ровно один composable — хвост, а зафиксированное стоит в ленте своими элементами со стабильными ключами и не пересобирается. Сам экран текст стрима не читает — толькоsnapshotFlowавтоскролла, иначе подписка утянула бы в перерисовку весь чат. - Markdown разбирается один раз на текст сообщения и кэшируется
remember; пузыри помечены@Immutable, поэтому нетронутые Compose пропускает.
Сборка
nix develop -c gradle assembleDebug
# apk: app/build/outputs/apk/debug/app-debug.apk
adb install app/build/outputs/apk/debug/app-debug.apk
Флейк приносит JDK, Gradle и Android SDK; на NixOS aapt2 подменяется
патченным из nixpkgs (GRADLE_OPTS в dev-шелле) — иначе линкер ресурсов
просто не запустится.
Ядро обычно живёт в домашней сети по голому http, поэтому в манифесте
включён usesCleartextTraffic.
Карта
app/src/main/kotlin/su/viende/eva/
├── MainActivity.kt # навигация, wiring ViewModel'ей
├── data/
│ ├── Models.kt # wire-формат ядра (Chat, StoredMessage, OutEvent, ToolInfo)
│ ├── KernelClient.kt # HTTP + разбор SSE руками
│ ├── ExtraTools.kt # свои тулы клиента: объявления на тур и их исполнение
│ ├── ServerStore.kt # список серверов в DataStore
│ └── ToolCatalogStore.kt # кэш каталога тулов (/v1/tools)
├── companion/ # бабл поверх всего: accessibility-сервис, окно, свой чат
└── ui/
├── servers/ # выбор ядра
├── chats/ # список чатов, чипы полок и поверхностей
├── approvals/ # очередь вопросов на одобрение
└── chat/ # лента, стриминг, пагинация