# Требования к десктопному клиенту agentik **Статус:** накидываем. Ни один пункт не обязателен к исполнению в том виде, как записан. Позже пересматриваем. **Основа дизайна:** вариант 1 (`approved/001-sidebar-utility.html`) — выбран, лежит в `approved/`. **Сейчас описываем только поведение.** Кода не пишем, пока требования не устоятся. --- ## 1. Общее - **R1.** Стиль — тёмная тема, уже принятая в клиентах пользователя (фон `#121218`, панели `#17212B`, текст `#EBEBEB`). **Это текущая тема, а не единственная** — см. R1.1. - **R1.1.** **Цвета берём из цветовой схемы, а не пишем в коде числом.** Предусматриваем, что тем будет **несколько** (тёмная, светлая и другие). В коде не должно быть цвета вида `#121218` — вместо этого берём цвет из текущей схемы по смыслу: «фон», «панель», «текст», «акцент», «ошибка», «успех». Смена темы тогда ничего не ломает — меняется схема, код остаётся. - **R1.2.** **Имена берём по смыслу, а не по виду.** Не «тёмно-серый» и не «синий», а «фон», «панель», «акцент». Смысл не меняется от смены темы, а название цвета — меняется. - **R1.3.** В макетах (`sketches/`, `approved/`) цвета стоят числами — это нормально, макет должен выглядеть как задумано. **На этом основан и способ проверки темы:** имена в коде — из R1.1, числа — только в макетах. - **R1.4.** Отдельно проверяем, что при смене темы **ничего не осталось вписанным числом**: ни текст, ни фон, ни границы, ни тени, ни цвет «наведения» курсора. Если цвет не из схемы — он не переключится и будет выбиваться. - **R1.5.** Особый случай — **цвета для различения ассистентов** (цвет агента из настроек). Они **не из схемы**: это пользовательские цвета, они заданы сознательно и в другой теме остаются собой. Их надо отличать от цветов темы, чтобы не перепутать при смене. - **R2.** Что видно на экране в любой момент: список диалогов, выбранный диалог и поле ввода. Больше ничего обязательного нет. ## 2. Раскладка и адаптивность - **R3.** **Широкое окно** — две части: список диалогов слева, открытый диалог справа. - **R4.** **Узкое окно** — на экране что-то одно: либо список диалогов, либо конкретный диалог. Переключение — как в Телеграме. - **R5.** В узком окне у открытого диалога сверху слева — кнопка «Назад». Нажатие возвращает к списку диалогов на всю ширину. - **R6.** Порог «широкое/узкое» — по ширине окна, одна величина. Какой именно размер считать порогом — решим позже. ## 3. Список диалогов - **R7.** Группировка диалогов — «Работа», «Дом» и подобное. **Управляемая:** группы можно добавлять, переименовывать и удалять. - **R8.** В строке диалога: название, последнее сообщение, время, счётчик непрочитанных. - **R9.** Есть поиск по диалогам. - **R9.1.** Над списком — **лента групп** (круглые кнопки): «Все чаты», «Работа», «Дом» и другие. Это часть одобренного варианта 1, не забывать при доработках. - **R9.2.** Справа в этой ленте — **кнопка «Группы» с карандашом (✎)**, прижата к правому краю, отдельно от чипов. **Прежний «+» там больше не стоит.** Причина: рядом чипы групп, и всё похожее на «+» читается как «добавить группу», хотя диалог создаётся кнопкой в шапке списка. Макет — `sketches/006-groups-manage`. - **R9.3.** Кнопка открывает окно **«Группы»**: список групп, у каждой «изменить» и «удалить», внизу «+ Добавить группу». - **R9.4.** **«Все чаты» — встроенная группа**, не удаляется и не переименовывается (в макете помечена «встроенная», пунктирная рамка). Нужна как надёжное место, где виден весь список: иначе при удалении группы диалоги пропадут из виду. - **R9.5.** У группы **только название**. Цвет, значок, порядок — не делаем, пока не попросили. - **R9.6.** **Пустая группа допустима** — можно создать заранее и разложить диалоги потом. - **R9.7.** **Удаление группы диалоги НЕ удаляет.** В окне подтверждения это сказано первым делом: диалоги останутся и будут видны в «Все чаты». Диалоги обязаны быть где-то видны — «корзины» у них нет. - **R9.8.** Удаление группы — **отдельным блоком «опасное»** в окне изменения и **красной залитой кнопкой** в подтверждении. Не в одном ряду с «Сохранить». - **R9.9.** Макеты групп — **отдельные файлы по состояниям, без интерактивности**: `1-list`, `2-add`, `3-edit`, `4-delete` в `sketches/006-groups-manage`. - **R9.2.** В строке диалога — круглый значок непрочитанных, если есть. ## 4. Открытый диалог - **R10.** Показ сообщений: мои и ассистента, с временем. - **R11.** Ответ ассистента можно прервать — на видном месте кнопка «Стоп». - **R12.** Сообщения приходят вживую, по мере генерации: текст дотекает куском за куском, а не появляется целиком в конце. Отображать именно так. - **R13.** **Разметка (markdown).** Если я или агент пишем текст, похожий на разметку, показывать его разметкой, а не сырыми символами: списки, выделение, код, таблицы, ссылки. Код для этого писать не надо — готовый рендер уже есть в репозитории `ai/assistent`, брать оттуда. - **R13.1.** **Откуда брать — точно.** См. `MARKDOWN-SOURCE.md`: репозиторий `ai/assistent`, ветка `main`, коммит `2c5b3f9`. Копировать 4 файла: модель разметки, парсер, отрисовку и десктопную обёртку открытия ссылок. Одна внешняя зависимость — `ru.otpbank.ai:markdown:0.51.0` (есть в Nexus). ## 5. Панель ввода - **R14.** Поле ввода, кнопка «Отправить» и **отдельная кнопка записи микрофона**. - **R15.** Подсказка о горячих клавишах под полем **убирается** — её не должно быть. - **R16.** Пока идёт запись, должно быть понятно, что сейчас пишется голос. - **R17.** Отмена записи — явная. - **R18.** Голос: кнопка включает микрофон и останавливает его. Точная механика (по нажатию или на удержание) — решим позже. - **R19.** Распознавание речи — **отдельная тема, здесь не решается.** Важно только: где кнопка, что видно во время записи, что происходит после. - **R19.1.** **Как читать микрофон — решено, пишем не мы.** Подключаем свою готовую библиотеку `mic-kmp` (`pw.binom.mic:mic-api-jvm`, версия `1.0.0`, есть в Nexus). Отдаёт звук потоком кусочков, а не файлом: можно распознавать сразу, не дожидаясь конца записи. Микрофон освобождается сам при остановке. Формат — 16 кГц, моно. Код из `ai/assistent` для этого **не берём**. Подробности — `MIC-ASR-SEARCH.md`. ## 6. Несколько агентов - **R20.** Предусмотреть возможность подключать **несколько агентов** и видеть их в одном клиенте. Не «много людей», а один клиент — несколько агентов. - **R21.** Возможный способ различения — **по аватару**: у каждого агента свой аватар, по нему и отличаем. Тема аватаров не продумана, это накидка. - **R22.** Другие возможные способы (на выбор, обсудить): - **Цветовой акцент** — у каждого агента свой цвет: полоса у пузыря ответа, цвет имени в шапке; - **Группировка в списке** — диалоги сгруппированы по агентам, с заголовком группы (как секции каналов); - **Переключатель агента** — как выбор аккаунта в Телеграме: все диалоги одного агента разом; - **Метка в строке диалога** — маленький значок/буква агента рядом с названием. - **R23.** У библиотеки уже есть всё для этого: агент создаётся вызовом с двумя значениями — имя и адрес. Имя клиент знает сам, сервер его не хранит. Каждому агенту — своё имя и адрес. - **R23.1.** Экран настроек агентов — **МАКЕТ ОДОБРЕН**, лежит в `approved/004-settings-agents.html` (черновик — `sketches/004-settings-agents`). **Окно поверх чата**, не отдельная страница. Два состояния: список агентов и форма нового. - **R23.2.** В строке агента видно: имя, адрес, состояние («на связи» / «не отвечает»), кнопки изменить и удалить. - **R23.3.** Форма нового агента: название, адрес сервера, и **чем отличать в списке — цвет или картинка**. Под каждым полем — пояснение простым языком. - **R23.3.1.** Переключатель «Цвет / Картинка»: выбирается что-то одно. **Цвет** — простой выбор из готовых, им помечаются диалоги агента в списке (полоска у строки, точка у группы). **Картинка** — файл с диска; показывается круглым превью, рядом «Выбрать файл…» и «Убрать». - **R23.3.2.** **Картинку копировать себе, а не брать по исходному адресу.** Выбранный файл сохраняется внутрь клиента (в свою папку), и дальше используется только эта копия. Ссылку на исходный путь не хранить: файл могут удалить, переименовать, флешку вынуть — и значок у агента пропадёт. Работает и для картинки на диске, и для картинки по ссылке: скачать и положить себе. - **R23.3.3.** Выбранная картинка показывается кружком там же, где был бы цвет: в списке агентов, в списке диалогов, в шапке чата. Убрать её можно в любой момент — тогда снова становится цвет. - **R23.4.** **Проверка связи до сохранения** — в **отдельном окне поверх настроек**, не строкой внутри формы. Нажал «Проверить связь» — открылось второе окно, в нём идёт проверка. Пока идёт: «спрашиваю агента…» с крутящимся значком. Результат — зелёный значок «отвечает» или красный «не отвечает», и под ним подробности: ответ на запрос, адрес, число диалогов, отклик в миллисекундах. Кнопка «Добавить агента» становится доступной только при успехе. Есть «Проверить снова». Закрытие окна возвращает к настройкам. Основа: агент отвечает на запрос состояния и отдаёт список диалогов; оба запроса ничего не меняют. - **R23.5.** Цвет агента работает не только в настройках: тем же цветом помечаются его диалоги в списке (полоска у строки, точка у группы). - **R23.6.** Список агентов сохраняется между запусками. Где именно — не решено. - **R23.7.** Доступа по паролю/ключу у библиотеки нет — поля для него не делать. - **R23.8.** **Новый диалог: выбор ассистента** — **МАКЕТ ОДОБРЕН**, лежит в `approved/005-new-chat-picker.html`. Черновик со всеми состояниями — `sketches/005-new-chat-picker`. По «+» спрашиваем, **в каком ассистенте** создать диалог — потому что диалог живёт внутри агента, отдельно его не создать. - **R23.9.** **Окно показывается только если ассистентов больше одного.** Один — диалог создаётся сразу в нём, спрашивать не о чем, окна нет и в помине. - **R23.10.** **При нуле ассистентов кнопки «+» нет вообще.** Создавать диалог не в ком. Вместо списка — объяснение, что нужно сначала добавить ассистента. - **R23.11.** В списке выбора видно: значок (цвет или картинка), имя, адрес сервера, состояние связи. Отмечен первый по умолчанию — создать диалог можно одним нажатием. - **R23.12.** **Ассистента без связи выбирать можно** — окно предупреждает словами, но не мешает. Связь может пропасть на секунду, а диалог человек создаёт осознанно. Ассистентов без связи из списка не выкидываем. ## 7. Состояние агента: что показывать Сверка с исходниками здесь важнее всего. Ниже — **по факту из кода**, не по догадкам. ### 7.1. Что есть в самой библиотеке `client` (что клиент реально получает) - **У каждого события и каждого сообщения есть дата.** Значит доступно: длительность хода, сколько времени агент думал, когда пришёл ответ. - **Есть события:** начало ответа, рассуждение, текст по мере генерации, картинка, вызов инструмента (имя, аргументы, заголовок), результат инструмента, конец хода, прерывание, ошибка. - **Есть события о диалогах:** создан, удалён, переименован. - **По диалогу:** название, время последнего изменения, умеет ли принимать и отдавать картинки, временный он или постоянный. - **Токенов, модели, цены, предела контекста — в библиотеке НЕТ.** Ни одного числа. Проверено сплошным поиском: числовых полей в протоколе не существует вообще. ### 7.2. Решение: счётчиков токенов и кольца в интерфейсе НЕ будет - **R24.** Показывать израсходованные токены, долю от предела и кольцо заполнения **не нужно**. Данных для этого в библиотеке нет — значит и рисовать нечего. Кольцо из макета удалено. - **R25.** Никаких полей наугад: **нет данных — нет цифры.** Не подставлять правдоподобные числа вместо отсутствующих. - **R26.** Если когда-нибудь агент начнёт отдавать такие числа — вернёмся к вопросу. До тех пор тема закрыта. ### 7.3. Ход работы агента - **R27.** Показывать не только ответы ассистента, но и происходящее вокруг них: вызовы инструментов, рассуждения, ошибки. - **R28.** Свёрнутый вид — одна строка: кратко, что происходит (например, «вызов инструмента: такой-то»). - **R29.** Развёрнутый вид — подробности шага: аргументы, результат, время. - **R30.** Сворачивание и разворачивание — по нажатию на строку. ## 8. Простота кода и кэш сообщений ### 8.1. Цель: клиент должен читаться - **R31.** **Главная цель — чтобы клиент был максимально прост по коду.** Я должен открыть исходники и понять, что там происходит. Это важнее, чем сэкономить строки или вынести лишнее. - **R31.1.** **Сначала десктоп, затем Android по тем же лекалам.** Десктопный клиент — образец. Когда он получится удобным и понятным, Android делается по его решениям. Поэтому простота кода здесь — не пожелание, а условие. - **R32.** Распознавание голоса, микрофон и детекция речи — **готовые библиотеки**, свои (`mic-kmp`, `asr-kmp`, `vad-kmp`). Писать заново ничего не надо, и это уже не «вынос в отдельное место» — библиотеки и есть отдельное место. - **R33.** Материал найден, разобран: `MIC-ASR-SEARCH.md`. Там же сказано, почему код микрофона из `ai/assistent` брать **не** надо. - **R34.** Из `ai/assistent` берём только приёмы отрисовки, плагины и версии — не архитектуру, не экраны, не дизайн. Разбор: `BORROW-FROM-ASSISTENT.md`. ### 8.2. Кэш сообщений на клиенте — решение принято **Задача:** не тянуть всю историю диалога заново при каждом открытии окна. **Схема:** 1. Открыли диалог — смотрим свой кэш. 2. Берём из кэша **последнее сообщение** и его дату. 3. Спрашиваем библиотеку `client`: есть ли сообщения **новее** этой даты. 4. Были — забираем только новые, добавляем в кэш, рисуем. 5. Не были — ничего не делаем. - **R35.** **Это работает: в библиотеке `client` всё нужное есть.** Функция `Conversation.getMessages(after, offset, limit)` отдаёт сообщения **строго новее** указанной даты (проверено: в отборе `created_at > ?`). Пустой ответ = после нашего сообщения ничего не появилось. Есть и версия потоком, страницами по 100 (`PAGE_SIZE = 100`). Отдельно создавать диалог для проверки не нужно — только если открываем новый. - **R36.** **Дубли отсекаются по `id` сообщения.** Идентификатор один и тот же и в живом потоке, и в истории (`Message.id`). То есть при догрузке не нужно угадывать, что уже нарисовано — сверяем по идентификатору. - **R37.** **Самого кэша в библиотеке нет** — это пишем мы. Библиотека даёт только «спроси, что новее». **Три места, где схема может порваться (найдено в коде, не предположения):** - **R38.** **Одинаковые даты.** Отбор строго «новее» (`created_at > ?`), поэтому если два сообщения получили **ровно одну и ту же дату** — второе в ответ не попадёт. Порядок внутри одной даты сервер задаёт сам (сортировка по времени, затем по `id`). Реальный риск невелик (дата с точностью до миллисекунд), но при сверке по `id` (R36) он не страшен вовсе. - **R39.** **Прерванный ответ в историю не попадает.** Если ответ прервали кнопкой «Стоп», кусок ответа остаётся только в живом потоке, в историю он не пишется (`Event.Interrupted`). В кэше окажется то, чего на сервере нет. - **R40.** **Живой поток тоже надо просить «с этого момента».** У подписки на события есть тот же параметр «после»; без него после переподключения пропустим события. - **R41.** **Решение:** кэш делаем. Схема — из R35. Где хранится (файл или лёгкая база) — **не решено**, см. раздел 9. ### 8.3. Что не выносим - **R42.** Экраны, пузырь сообщения, хранение списка агентов — **остаются в клиенте**. Это и есть клиент, выносить их некуда. - **R43.** Полезный признак, что что-то не вынесено: **в экране накопилась логика длиннее пары десятков строк.** Значит, это место жить в экране не должно. ## 9. Решения, которые ещё не приняты - Запись: по нажатию или на удержание. - Нужны ли вложения (картинки) в этом клиенте. - Как именно различать агентов (см. R21–R22): цвета или картинки достаточно? - Нужен ли картинке размер/форма — показывать кружком без изменений или обрезать. - Где хранится список агентов — в файле на диске или спрашивать сервер. - Как выглядит показ хода работы агента в свёрнутом виде. - **Где хранится кэш сообщений** — обычный файл на диске или лёгкая база (R41). - Сколько держать в кэше и когда чистить (старые диалоги). - Что делать с куском прерванного ответа в кэше (R39): хранить или выбрасывать. ## 10. Как проверяем Проверка — **живой прогон**, а не «собралось». Правило пользователя: перед показом гонять сценарий руками; на экран смотреть, а не верить описанию.