- Python 96.9%
- Nix 3.1%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
Ядро научилось считать не только в долларах (`llm.currency`), и клиент
теперь подписывает каждое число тем, что ядро назвало: расход чата, окна
статистики, команду подагентов, счётчик идущего тура и шапки денежных
колонок.
USD остаётся привычным префиксом (`$0.031`), прочие валюты идут знаком
после числа (`0.031 ₽`). Символ клиент знает у USD, RUB и EUR, а
незнакомый ISO-код печатает как есть — `120.00 KZT`: угаданный символ
соврал бы, а код читается.
Валюту называют сами ответы про расход — какое число, такая и подпись.
Счётчику тура она не доезжает вовсе (деньги там копятся из событий
`usage`), и ради него клиент раз на ядро спрашивает `GET /v1/config`:
смена валюты значит перезапуск ядра, чаще незачем. Старое ядро поля не
отдаёт — тогда доллар, и это его прежнее поведение, а не догадка.
Ключ `tui.spend.col.reasoning_cost` принял `{currency}`; оверлей,
написанный до этого, работает как написан.
Co-Authored-By: Eva
|
||
| docs | ||
| nix | ||
| src/eva_tui | ||
| tests | ||
| .gitignore | ||
| AGENTS.md | ||
| CHANGELOG.md | ||
| FEATURES.md | ||
| flake.lock | ||
| flake.nix | ||
| pyproject.toml | ||
| README.md | ||
eva-tui 💻✨
Я в твоём терминале, Господин. Textual + httpx, тонкий клиент к eva-kernel — весь ум в ядре, здесь только лента и клавиши.
Что умеет
- Чаты — все твои разговоры, откуда бы ты их ни завёл: терминал,
телефон, браузер — окна в одно и то же. Создать
ctrl+n(Enter — обычный,t— временный; название чату придумает модель по первому сообщению), удалитьctrl+x(с подтверждением), обновитьctrl+r. Полка (folder) — про что разговор, а не кто его завёл:ctrl+fвыбирает, что показывать — все мои чаты, одну полку или поверхность (телеграмная личка, мастерская переводов — их чаты заводит не Господин и их десятки, поэтому в общем списке их нет, пока не спросишь). Кодовые сессии всех проектов лежат на полкеcode, и различает их полеproject: пункт «Код» открывает их одним списком, а проект подписан в строке чата. Новый чат ложится на ту полку, на которую сейчас смотрит список; смотришь на всё или на «Код» — в «Общее» (так зовётся полка по умолчанию): кодовую сессию заводит клиент в каталоге проекта, а заведённой из списка неоткуда взять проект. Свои проблемные чаты Ева складывает на полкуproblems— она там же в списке. Подпись над списком говорит, что сейчас показано (полка, «мои чаты», поверхность или «🗑 корзина»), в строке чата серым стоит время последней активности, а когда список смешивает полки — полка чата или проект кодовой сессии. - Переложить чат —
ctrl+o, затемf: полка из списка или новая по имени (её достаточно вписать). Разговор сменил тему — сменил и полку. - Внимание — чаты, куда Ева зовёт Господина, помечены жёлтым
⚠и идут первыми в списке;f2снимает пометку с выделенного (и вешает обратно, если чат хочется отложить). - Корзина — удаление мягкое, история цела:
f3показывает только удалённые чаты (в обычном списке их нет),f4возвращает выделенный. - Сессии проекта —
/sessionsв кодовом режиме: прежние разговоры этого проекта — название, когда последний раз говорили, во что обошлись;Enterоткрывает и перепривязывает.eva/session,dудаляет. - Сессия привязана к каталогу —
.eva/sessionпомнит чат этого рабочего каталога. В.gitignore: в клоне на другой машине сессия начнётся с чистого листа, а не подхватит переписку про чужие файлы.ctrl+nначинает новую и перепривязывает; прежние видны из обычного режима. - Правила и память проекта —
AGENTS.mdи.eva/MEMORY.mdсобирает само ядро: в отправке едетproject_docs, и оно раз на тур забирает ВСЕ такие файлы от корня проекта вниз до рабочего каталога, ближний — последним. Клиент тут только руки: служебная операцияlist_upидёт по дереву вверх и говорит, какие из спрошенных имён где есть. ИндексMEMORY.mdедет Еве в промпт, тело факта она читает сама, когда крючок совпал, и пополняет память по ходу дела. Коммитится с проектом, так что переезжает вместе с ним. - Подагенты —
f7(или/agents): кого Ева поставила на работу в этом чате — имя, статус, модель, набор инструментов и во что обошёлся (4500 ток $0.0310 думы 1200 · $0.0024; цены выходного токена у модели нет — вместо суммы прочерк). В шапке — итог по команде:подагенты: 3 · 12.3k ток · $0.031 · думы 2.0k · ≥ $0.002, где≥означает, что часть подагентов думала моделями без цены и итог занижен, а «цена неизвестна» — что цены нет ни у одной.enterоткрывает чат подагента: он и есть чат, так что видно всю его работу целиком, живую в том числе.dостанавливает. Ядро без/v1/agents— панель просто пуста. - Очередь на одобрение —
f5: Ева спрашивает разрешения асинхронно, вне тура; вопрос, детали и время — списком,aодобряет,rотклоняет, решённое сразу уходит. Пока очередь не пуста, счётчик висит в шапке (в код-режиме — в строке режима снизу). - Стриминг — речь Евы делится надвое: то, что уже не изменится, уходит
в ленту отдельными виджетами и больше не трогается, а перерисовывается
один подвижный хвост. Фиксируется только целый блок markdown и только
когда виден следующий: недописанная строка оставила бы
**boоткрытым жирным, а разорванный пополам ```-блок отрендерился бы мусором. Начатая таблица живёт в хвосте до конца тура — новая строка меняет ширины всех колонок и переливает предыдущие. Вызовы инструментов показываются пометками с иконкой и краткой сводкой. При старте клиент забираетGET /v1/toolsи строит картуname → {icon, brief}: иконка перекрывает встроенную эвристику категорий,brief-шаблон ({from} → {to},{opts.date}и т.п.) подставляется из аргументов вызова; без каталога или шаблона — прежнее поведение («🌐 Поиск: „…"», «💻 Команда: …»; MCP без каталога — сырое имя). Долгие тулы обновляют пометку живым прогрессом. Пока тур жив, над полем ввода крутится статус со спиннером, сменными глаголами, секундомером и счётчиком тура — какой идёт шаг, сколько токенов ушло вверх/вниз, сколько выхода забрали мысли и во сколько это уже обошлось (✳ копаюсь… (47s · шаг 6 · ↑12.4k ↓1.8k (думы 900) · $0.031 · esc — прервать); думы — часть выхода, а не добавка к нему). Секундомер стоит, пока тур упёрся в решение Господина — диалог разрешения записи или команды, кнопочный вопрос: «работала пять минут», из которых четыре Господин смотрел на диалог, — неправда, и статус в это время так и говорит:жду вашего слова… (47s · секундомер стоит).Escостанавливает генерацию (частичный ответ сохраняется в ядре) и заодно убивает команду, которую клиент выполняет прямо сейчас — «стоп» останавливает то, что видно на экране. Без идущего тураEscстирает набранное. Тур длиннее полминуты звонит в терминал по завершении. Туры идут доверенными и без потолка токенов (trusted+uncapped). - План работы — взявшись за большое дело, Ева ведёт себе список задач;
клиент показывает его строкой над вводом, рядом со статусом:
▸ провод: строка плана над вводом · сделано 1 из 3— активная задача (с фазой, если она есть) и сколько уже позади. Событиеplanприсылает состояние целиком, а не правку; пустой список — работа кончилась, и строка уходит. При открытии чата и по концу тура план перечитывается изGET /v1/chats/{id}/plan: событий могло не быть вовсе — подхватили чужой тур или план закрылся молча. Брошенные задачи считаются отдельно — брошенное не сделанное. - Выхлоп инструмента — если из результата видна только часть, под ним
встаёт «⎿ выхлоп целиком (N строк)»:
Enterразворачивает его прямо в ленте (до 200 строк), не открывая сводку. Из истории выхлоп поднимается тем же блоком. - Картинки — Ева умеет рисовать, но терминал не галерея: нарисованное
встаёт в ленту пометкой
🖼 картинка (image/png) · https://…/images/…. Ссылка открывается руками — по ней ядро отдаёт байты. Картинка приходит посреди речи, поэтому пометка встаёт между текстовыми пузырями, ровно там, где Ева её нарисовала; из истории она поднимается тем же видом шага. - Размышления — пока Ева думает, в хвосте ленты курсивом идут три
последние строки её мыслей; законченный кусок схлопывается в
«💭 размышления (N строк)» и разворачивается по месту (
Enterна нём). Из истории они поднимаются в ленту вместе с остальным. - Сводка —
ctrl+y: таймлайн шагов чата, как в android-клиенте — размышления и вызовы инструментов по порядку;Enterраскрывает шаг до полных аргументов и выхлопа. - Блокнот чата —
f10(или/notes): заметки, которые Ева решила не потерять — решение и его причина, инвариант проекта, добытый раскопками путь, тупик, куда уже ходила. Списокимя — описание(закреплённые наверху с📌), тело выбранной — под ним;dзабывает заметку,pвешает и снимает булавку,cпересобирает блокнот (отчёт ядра — в уведомлении),rвозвращает версию, отложенную перед последней пересборкой. В подписи — режим и размер (inline · 1234 симв.): вinlineблокнот едет модели телами дословно, вindex— одними именами с описаниями, так что видно, почему она перестала видеть тела. Изменившееся с прошлого захода помечено «новое»; отметка просмотра серверная, поэтому web, TUI и android считают новым одно и то же. Правка тела заметки живёт в веб-клиенте. - Модель —
ctrl+p: список из/v1/models;Enter— модель текущему чату (или вернуть на дефолт ядра),d— дефолт для новых чатов этого ядра (клиентская настройка, переживает перезапуск). Выбранная модель сразу видна в подзаголовке шапки (в код-режиме — в рамке сессии); чат без своей модели показывает модель ядра с пометкой «дефолт». - Уровень рассуждения —
uна экране модели (или/effort): насколько сильно Ева думает перед ответом в этом чате. Ступени берутся у ядра (effort_levels); выше ступень — дольше и дороже, зато решает больше. «Как решит ядро» снимает выбор чата, оставляя ступень записи модели, а нет и её — глобальную из конфига; там же сказано, что именно достанется взамен. Действующая ступень стоит рядом с моделью —думы high, а скобки («модель», «дефолт») говорят, что выбирал её не чат. - Набор инструментов —
f9(или/toolset): наборы из/v1/toolsets,Enterприменяет выбранный к открытому чату, «весь реестр ядра» снимает набор. Кодовой сессии осмысленно жить на узком наборе, а болталке — на широком. - Кнопочные вопросы — когда Ева спрашивает через
ask_questions, в ленте вырастает список вариантов с указателем❯: стрелки двигают,Enterотвечает, цифра отвечает сразу; «💬 Дообсудить» — последний пункт за чертой. Пачка из нескольких вопросов листается плашками сверху (←/→), ответ сам открывает следующий неотвеченный.Tabв выборе не участвует и по кнопкам интерфейса не скачет. После ответа список сменяется «❓/✅»-записью тула. Если вопроса ядро уже не ждёт (истёк, отвечен с другого клиента), пункты снова оживают, а внизу всплывает «вопрос уже неактуален». - Markdown — ответы Евы рендерятся markdown'ом (код, списки, таблицы); реплики пользователя показываются как есть.
- Формулы —
$…$и$$…$$из ответов Евы приближаются юникодом:$\mathrm{H_2SO_4}$→ «H₂SO₄»,$\int_0^1$→ «∫₀¹». Настоящей вёрстки в терминале не будет — это приближение, а не KaTeX из веба. Границы формулы берутся как в VS Code: «$» открывает её, только если перед ним не буква и не цифра, и закрывает, только если после него не буква и не цифра, — так «$5 и $10» остаются деньгами. Содержимое ```-блоков икодане трогается, выключенная формула встаёт отдельным блоком, а не влипает в абзац. Перевести не вышло — на экран идёт исходный TeX. - Лента элементов — и история (
GET /v1/chats/{id}/items), и живой тур (item_started→item_delta→item_completed) говорят одним видом шага: реплика, размышление, вызов с вложенным результатом, картинка, служебная вставка ядра, граница компакции. Клиент не сшивает/messagesс событиями в свою третью модель, а легаси-набор тех же событий глушит у ядра полемmute. Нарисованное поitem_startedправится поitem_completed— он авторитетен. Чем кончился вызов, говорит его статус:failed— красная пометка с началом ошибки,aborted— «не выполнен: тур оборван». - История страницами — открывается хвост из 50 сообщений,
PageUpу верхнего края догружает старше; пока сверху что-то осталось, над лентой висит строчка «⋯ PageUp — старше». Вся история не тянется никогда. - Правка прошлого —
f8(или/rewind): список своих реплик.Enterзаводит ВЕТКУ перед выбранной — новый чат с копией истории до неё, а её текст ложится в поле ввода: исправленный ход уедет в ветку, прежний разговор останется целым (в/sessionsветки помечены «⑂»).dстирает реплику и всё после — невозвратно, и потому спрашивает. - Многострочный ввод —
enterотправляет,alt+enter(илиshift+enter, илиctrl+j) переносит строку; поле растёт по содержимому до восьми строк, вставка ложится абзацем как есть. - Очередь во время тура — поле живо, пока Ева работает: отправленное
ждёт своей очереди («⏎ в очереди: N» под полем) и уходит по одной реплике
следующими турами. Увидел, что Ева пошла не туда, — дописал сразу, не
дожидаясь конца.
Escснимает слои по очереди: сперва очередь, потом тур, потом набранное. Смена чата очередь сбрасывает. - История ввода —
↑/↓листают последние 50 отправок (в код-режиме — когда меню «/» закрыто; внутри абзаца стрелки двигают курсор, история листается с краёв текста), ниже самой свежей возвращается недописанная строка. Сорвавшийся тур кладёт текст обратно в поле, чтобы не набирать его заново. - Спикеры — чужие реплики в общих чатах подписаны именем личности из базы ядра (жёлтым); свои — «Господин».
- О чате —
ctrl+o: метаданные (полка и поверхность, модель, набор чата, сколько инструментов у него в контексте и когда ему спать, даты), саммари компакции, компакция по кнопке (c), перекладывание на другую полку (f) и набор по умолчанию (t). Спросить Еву о саммари можно прямо в чате — тур его видит. - Крон —
ctrl+t: таймеры с удалением;r— журнал исполнений (чем кончилось и что вышло, запись раскрывается). - Сон —
/sleepв кодовом режиме: время от времени Ева спит, разбирая долговременную память, и пока спит — не отвечает, туры ждут пробуждения. Без хвоста команда показывает состояние (спит — разбираю кластеры 2/7, уже 5m · прошлый сон 1h назад),nowукладывает спать сейчас (now dry— вхолостую: план считается и ложится в журнал, но не применяется),wakeбудит (недоделанная фаза не применяется, применённое остаётся),logоткрывает журнал ночей: чем кончилась, сколько действий легло в память, каков был корпус, пометки «вхолостую» и «разбудили»,Enterраскрывает отчёт ядра словами. Отказ ядра («уже сплю», «не сплю», «сон выключен») приходит уведомлением, а не ошибкой. Тур, застигший сон, показывает «сплю (фаза)» вместо обычного «думаю» и подсказку/sleep wake; пока Ева спит, в строке режима (в обычном режиме — в шапке) висит💤 спит. Состояние клиент узнаёт из события тура и своих же команд — ради отметки ядро он не опрашивает.chatsоткрывает журнал снов чатов — у каждого чата он свой (см. ниже). - Сон чата и набор по умолчанию —
/tools(илиtв «о чате»,sв наборах инструментов). У каждого чата свой ленивый сон, и он курирует то, что чат видит в промпте по умолчанию: болталка про аниме перестаёт таскать схемыmcp_forgejo, кодовая сессия —maps_search. Это слой внимания, а не прав: скрытое не отобрано, оно осталось в реестре скрытого именем и назначением и возвращается однимtool_load, — поэтому панель называет две разные величины: «в контексте сейчас: 15 из 77 досягаемых» и набор чата (toolset), который и есть права. Считает тулы ядро — у него гейты чата и его доступ; клиент не считает ничего сам, потому что записи набора это имена тулов и целых семейств вперемешку: те же 15 тулов приезжают шестью записями. Чату, не ходившему после апгрейда, ядро цифр не даёт, и панель говорит это словами, а не нулём. Курирование приходит от тесноты, а не от молчания: влезает набор в бюджет — ночь не трогает ничего, и молчащий тул этим не наказан. В панели видно причину (why), хранимое решение (имена тулов, семейств,set:<набор>) и раскрытый набор списком: закреплённое помечено📌, обязательное ядро подписано «ядро». Чат ещё не курирован — списком идёт всё досягаемое ему: набора нет, а булавку ставить уже есть на что.pзакрепляет инструмент или снимает булавку — закреплённое ни одна ночь не спрячет, и ждать первой ночи не надо: булавка сама курирования не заводит (набор остаётся пустым, чат по-прежнему видит всё), она свяжет руки первому же сну.xснимает курирование целиком,sусыпляет чат сейчас (d— вхолостую: план ляжет в журнал, набор не тронется),lоткрывает журнал его снов (+2/−7, модель, отчёт и план словами),t— таблицу «чем пользуется чат» за неделю/месяц/квартал. В таблице главное — покрытие: в скольких турах из скольких тул звался хоть раз («раз в месяц и всегда» — это1/1, а не «1 вызов за месяц»); а «вернули» считает, сколько раз тул доставали обратно рукой — столько раз курирование спрятало не то. Пока у чата стоит набор инструментов, курирование поверх него не применяется вовсе, и шапка об этом говорит. - Несколько ядер —
ctrl+b: переключение между серверами из[[servers]]конфига; выбор переживает перезапуск. - Расход —
ctrl+s: окна час/сегодня/неделя/месяц (1–4), итог, триаж, разбивка по моделям, источникам туров и чатам. Думы — своей строкой (думы: 12.3k из 40.1k выхода (31%) · $0.0123): это доля выхода, а не добавка сверх него, и колонкидумыи цены дум в разбивках показывают ту же ось по моделям и источникам. Деньги за думы считаются только по моделям сprice_outв конфиге ядра: у модели без цены вместо суммы прочерк, а итог при смеси идёт под≥с объёмом дум, оставшихся без цены. Дум в окне не было — ни строки, ни колонок. Кэш промпта — тоже своей строкой (кэш: 120.4k из 138.2k промпта (87%) · запись 30.0k) и колонкамикэш/записьв тех же разбивках: видно не только «кэш работает», но и кто именно ходит мимо него. Запись стоит дороже обычного промпта, поэтому окно, где записано много, а прочитано ноль, названо прямо —12.0k записано, ни одного чтения: это инвалидатор, что-то в начале промпта меняется от тура к туру. Кэша в окне не было — ни строки, ни колонок. - Расход чата —
f6: сколько стоил открытый чат, сколько весит его контекст сейчас, какая доля промпта пришла из кэша (и сколько в кэш записано) и какая доля выхода ушла в думы ($0.031 · ctx 34% (68.2k) · кэш 87% (+30.0k) · думы 30% ($0.004); цены выходного токена у модели нет — скобок нет). Процент уctx— доля окна модели, занятая контекстом: считается от её потолка за вычетом стабильного префикса тура (персона, схемы инструментов, правила), иначе пустой чат начинался бы не с нуля. Потолка модель не объявила — остаются одни токены. Читать из кэша нечего, а запись есть — так и сказано:запись в кэш 30.0k впустую. Живёт правым краем строки режима (в код-режиме) или в подзаголовке шапки — ленту не закрывает; на узком терминале ужимается до денег. Обновляется при открытии чата и после каждого тура; тура ещё не было — вместо веса прочерк. В код-режиме показан сразу, в обычном включается тем жеf6; выбор помнится. Ядро без/v1/chats/{id}/spend— строки просто нет. - Валюта денег — та, в которой считает ядро (
llm.currency); клиент ничего не конвертирует, а только подписывает. Доллар остаётся привычным префиксом ($0.031), прочие валюты идут знаком после числа (0.031 ₽), а незнакомый ISO-код печатается как есть —120.00 KZT. Валюту называют сами ответы про расход; счётчику идущего тура, где её нет, клиент спрашиваетGET /v1/config— раз на ядро. Старое ядро валюту не называет, и тогда это доллар, как оно и считало. - Телеграм-настройки —
ctrl+d: дебаунс триажа, потолок ответа и краткость по чатам и пользователям;Enter— правка, пустое поле — глобальная логика ядра. - Клиентские операции — Ева читает/правит файлы (
read_file,write_file,write_diff), смотрит и патчит двоичные (read_hex,write_hex,find_hex) и гоняетlocal_shellпрямо на этой машине: клиент исполняетclient_opиз стрима и постит результат обратно. Относительные пути и каталог команды — от рабочего каталога чата (его ставитworkdir), а без него — от каталога запуска. Долгая команда показывает, чем занята: клиент читает вывод потоком и шлёт ядру снимки хвоста, а оно превращает их в живой прогресс того самого вызова — в код-режиме он виден строками «⎿ …» под ним. Двоичные операции возят байты в base64: читается ровно затребованное окно, запись ложится по смещению поверх существующего, поиск течёт файлом окном по мегабайту — архив на гигабайты в память не ложится. Разрешения: по умолчанию ask-режим — запись и shell подтверждаются (enterилиy— да,escилиn— нет; чтение, поиск и служебный подъём свободны);--yoloстартует с auto-режимом (разрешено всё),shift+tabпереключает режим на лету. Набор ответов задаёт ядро, а не клиент: он рисует те, что умеет, и молча пропускает незнакомые — новое решение вводится правкой ядра. Молчаливого закрытия у диалога нет:esc— это отказ, и он уезжает ядру решением. Сам ядро диалог тоже снимает — когда операция истекла или тур кончился; тогда решения Господин не принимал, и от его имени клиент ничего не отвечает. Запрос на запись показывает дифф — что лежит на диске против того, что Ева собирается записать, — чтобы решать не по одному имени файла; у двоичной правки в запросе смещение (десятичное и шестнадцатеричное), объём и начало байтов в hex. Послабления:a(илиф) — «до конца сессии»,!— «и дальше» (переживает перезапуск, привязано к рабочему корню). Команда запоминается по первому слову (cargo,git), запись — по каталогу внутри рабочего корня. Что уже разрешено, покажет/mode;/newсбрасывает сессионные. Операция из реплея/liveрисуется, но не исполняется: после переподключения второй записи в файл не будет. - Свои тулы клиента — тем, до чего ядру не дотянуться, терминал
распоряжается сам: он объявляет модели свои инструменты на каждый тур
(поле
extra_toolsвsend_message), она зовёт их как обычные, а исполняет их клиент. Сейчас этоcopy_to_clipboard— положить текст в буфер обмена Господина, чтобы он вставил его куда нужно. Ответ уезжает тем же проводом, что и клиентские операции, и по тем же правилам: реплей рисуется, но не исполняется, а упавший обработчик отвечает модели текстом ошибки — молчание стоило бы ядру полного таймаута. Объявления лежат вextra_tools.py; имя, тенящее тул реестра, ядро отвергает вместе с туром. - Код-режим —
--code [DIR]: сессия проекта в интерфейсе Claude Code, корень операций DIR (по умолчанию cwd), сессии на полкеcodeс именем каталога вprojectи автопродолжением последней (/new— новая). Экраны открываются «/»-меню, а не ctrl-клавишами: наборы хоткеев режимов не пересекаются. Каждый тур уезжает с полемcontext(фрейминг кодовой сессии — правила проекта собирает ядро само), сproject_docsи сuncapped_iterations: потолок итераций снят, длинная правка не обрывается на середине. Ядро без поддержки полей молча их игнорирует. - Голос — всё, что видно на экране, лежит ключом в каталоге сообщений
(
messages.py+locale/builtin.json), и встроенный каталог нейтрален: из коробки клиент не называет ни имени, ни обращения и не говорит о себе в женском роде. Складывается голос из двух независимых источников. Строки — из своего конфига (overlay, путь к плоскому файлу «ключ → строка либо список строк»): о клиентских интерфейсах ядро не знает ничего, и через него они не ездят. Имя и обращение — от активного ядра,GET /v1/personaотдаёт{name, honorific}; они подставляются в любую строку как{name}и{honorific}, объявлять их не надо, а сам файл переживает их смену. Оверлей переписывает любой ключ, чего в нём нет — берётся из встроенного: неполный оверлей норма, а не поломка; счётное держит формы суффиксами.one/.few/.many, список вариантов — массивом. Испорченная подстановка молча уступает встроенной строке, а ключ, которого нет нигде, возвращается собой — пропажа видна на месте, а не пустотой. Тем же путём уступает и строка, которой нужно{honorific}, пока обращения нет: источники независимы, файл читается всегда, а имя ждёт ядра — и «С возвращением, !» на мёртвом ядре хуже нейтрального приветствия. Файла нет или он битый — предупреждение и нейтральные строки: это клиент, а не ядро, и падать из-за опечатки в конфиге ему не за что. Имя от ядра кладётся в state-файл по имени ядра, так что недоступное ядро отвечает из памяти прошлого раза, а 404 у старого ядра читается как «имени нет». Прежний голос Евы собран целиком вdocs/persona-overlay.example.yaml— готовый файл дляoverlay. Одно исключение: подписи клавиш Textual читает на определении класса, раньше всего остального, поэтому их переписывает только встроенный каталог.
Хоткеи
Наборы режимов разведены: клавиша обычного режима в кодовой сессии не
срабатывает и не светится в подсказках, всё нужное там делает «/»-меню.
Общие на оба режима — только идиомы самой ленты: escape (прервать тур),
shift+tab (разрешения auto/ask), PageUp (страница истории старше),
f6 (расход), f7 (подагенты), f8 (правка прошлого), f9
(набор инструментов), f10 (блокнот чата) и f1 (что я умею).
f1 (или /help) показывает всё это прямо в клиенте: клавиши по группам,
«/»-команды и три конвенции. Список собирается из привязок и команд
самого клиента, поэтому устареть не может, и показывает только те клавиши,
что живы в текущем режиме.
Буквенные клавиши экранов понимают и русскую раскладку: d/в, a/ф,
r/к, c/с, t/е, f/а — терминал отдаёт символ, а не
физическую клавишу, и без двойника с ЙЦУКЕН они молчали.
Обычный режим
| Клавиша | Что делает |
|---|---|
ctrl+n |
новый чат (Enter — обычный, t — временный) |
ctrl+x |
удалить чат (мягко, с подтверждением) |
ctrl+r |
обновить список |
ctrl+l |
фокус: список чатов ↔ поле ввода |
ctrl+o |
о чате (f внутри — переложить на другую полку) |
ctrl+y |
сводка шагов |
ctrl+p |
модель (u внутри — уровень рассуждения) |
ctrl+e |
память |
ctrl+t |
крон |
ctrl+g |
идущие туры |
ctrl+s |
расход |
ctrl+d |
телеграм-настройки |
ctrl+b |
ядра |
ctrl+f |
что показывать: мои чаты, полка или поверхность |
f2 |
снять/вернуть пометку внимания |
f3 |
удалённые чаты (корзина) |
f4 |
вернуть удалённый чат |
f5 |
очередь на одобрение |
f6 |
расход |
f7 |
подагенты открытого чата |
f8 |
правка прошлого: ветка или стирание |
f9 |
набор инструментов чата |
f10 |
блокнот чата |
f1 |
что я умею |
ctrl+q |
выход |
Код-режим
| Клавиша | Что делает |
|---|---|
/ |
меню команд: стрелки — выбор, tab — дополнить, enter — выполнить |
escape |
прервать тур |
shift+tab |
разрешения auto ↔ ask |
PageUp |
страница истории старше |
f6 |
расход |
f7 |
подагенты открытого чата |
f8 |
правка прошлого: ветка или стирание |
f9 |
набор инструментов чата |
f10 |
блокнот чата |
f1 |
что я умею |
Команды меню: /help, /new, /sessions, /model, /effort, /toolset, /mode,
/compact, /clear, /rewind, /summary, /notes, /info, /agents,
/approvals, /stats, /memory, /sleep, /cron, /gens, /servers,
/telegram, /quit. Хвост после имени достаётся команде аргументами:
/sleep now dry, /sleep wake, /sleep log.
Запуск
nix develop -c python -m eva_tui.app --url http://127.0.0.1:8090
# или: nix run . -- --url ...
Адрес ядра ищется в таком порядке: --url → EVA_TUI_URL →
~/.config/eva-tui/config.toml ([kernel] url = "...") → localhost:8090.
Токен (если ядро за прокси с проверкой X-Eva-Token) — так же, причём
на каждом слое можно дать значение или файл с ним: --token/--token-file
→ EVA_TUI_TOKEN/EVA_TUI_TOKEN_FILE → [kernel] token/token_file.
Дополнительные ядра — секциями [[servers]] в том же конфиге (первым в
списке всегда идёт «default» из слоёв выше):
[[servers]]
name = "home"
url = "https://eva.desu.church"
token_file = "/run/secrets/eva-tui-token" # или token = "..."
Оверлей строк — тоже конфигом, ключом overlay: путь к плоскому файлу
«ключ → строка либо список строк» (образец —
docs/persona-overlay.example.yaml). Общий лежит в [kernel], а у ядра
может быть свой — он перекрывает общий:
[kernel]
overlay = "~/.config/eva-tui/voice.yaml"
[[servers]]
name = "work"
url = "https://eva.example.com"
overlay = "~/.config/eva-tui/work-voice.yaml" # своё ядро — свой голос
Клиентские настройки (активное ядро, дефолтные модели новых чатов,
запомненное имя каждого ядра) живут в ~/.local/state/eva-tui/state.json
— правятся клавишами, не руками.
NixOS-модуль
Флейк отдаёт nixosModules.default — клиент ставится системно, адрес
ядра живёт прямо в конфиге системы (обёртка задаёт EVA_TUI_URL,
--url из рук всё ещё сильнее):
{
inputs.eva-tui.url = "git+ssh://forgejo@git.viende.su:61488/eva/tui.git";
# в конфигурации:
imports = [ inputs.eva-tui.nixosModules.default ];
programs.eva-tui = {
enable = true;
url = "https://eva.desu.church";
tokenFile = "/run/secrets/eva-tui-token"; # например, из sops-nix
};
}
home-manager-модуль
homeManagerModules.default ставит клиент пользователю, пишет
~/.config/eva-tui/config.toml и — главное — приносит голос из коробки.
Персонный оверлей, сгенерированный из документа персоны, лежит в репе
(nix/persona-overlay.default.json, 76 ключей) и включён по умолчанию:
imports = [ inputs.eva-tui.homeManagerModules.default ];
programs.eva-tui = {
enable = true;
settings.kernel.url = "https://eva.desu.church";
# поправить одну строку, не трогая остальные 75
persona.overlay."tui.banner.greeting" = "Доброй ночи!";
};
Слои голоса — от дешёвого к решительному:
| опция | что делает |
|---|---|
persona.useDefaults |
сгенерированный оверлей целиком; false — начать с пустого |
persona.overlay |
поправки по ключам поверх дефолтов; ключа нет — остаётся дефолтный, нет и там — встроенная нейтральная строка |
persona.overlayFile |
готовый файл оператора целиком, поверх всего: в чужой файл сливать нечего |
settings.kernel.overlay |
сырой аварийный выход, сильнее всего перечисленного |
Имя и обращение сюда не входят — они приезжают от ядра
(GET /v1/persona) и подставляются в эти строки как {name} и
{honorific}. Дефолт лежит JSON'ом, а не YAML, потому что nix читает JSON
сам: fromJSON (readFile …) обходится без IFD. Что модуль насчитал по
слоям, проверяется на eval — nix flake check, девять сценариев без сборки.
Карта
nix/ # persona-overlay.default.json — голос по умолчанию для модуля
src/eva_tui/
├── messages.py # каталог сообщений: t/tn/tl, чтение оверлея из конфига
├── locale/ # builtin.json — нейтральный каталог, полный и встроенный
├── kernel.py # REST + разбор SSE руками, полки и поверхности, PAGE_SIZE
├── tools.py # лицо инструментов: иконки, brief-шаблоны, каталог /v1/tools
├── stream.py # стрим надвое: граница фиксации, придержание таблиц
├── formulas.py # LaTeX в ответах Евы → юникод: индексы, символы, стрелки
├── feed.py # лента элементов: курсор страницы, статусы, шаги сводки
├── project.py # подъём по дереву для служебной операции list_up
├── extra_tools.py # свои тулы клиента: объявления на тур и их лицо в ленте
├── token_usage.py # доля занятого окна: usage против потолка минус префикс
└── app.py # Textual: список чатов, лента, стрим, пагинация, экраны
tests/ # чистая логика без терминала и ядра
Тесты — стандартным unittest, зависимостей сверх клиента не надо:
nix develop -c python -m unittest discover -s tests