# agentik-desktop — десктопный клиент агента agentik Клиент агента **agentik** на Kotlin Multiplatform + **Compose Desktop** (цель сборки — только JVM). UI общается с агентом через готовую библиотеку `pw.binom.agentik:client`: она даёт `Agent` / `Conversation`, живой поток событий (SSE), авто-reconnect, кэш списка диалогов и `interrupt()` для кнопки «Стоп». Проект уже **собирается и запускается** (`./gradlew run`), а не только спроектирован. ## Что где лежит ``` sketches/index.html # ВСЕ экраны одним документом: список диалогов, чат, запись, # группы, агенты, проверка связи, пустые состояния. # Это и черновик, и «одобренное» — отдельного approved/ больше нет. sketches/style.css # палитра и стили макетов (цвета числами — так и надо, это макет) src/jvmMain/kotlin/... # код приложения src/jvmTest/kotlin/... # тесты ``` Документы рядом: - `REQUIREMENTS.md` — требования (накиданы, ни один пункт не обязателен в текущей форме). - `STORAGE.md` — что и где хранится на клиенте (SQLite + JSON). - `CACHE.md` — кэш сообщений: что есть в библиотеке, схема «только новое». - `THEMES.md` — цвета не пишем в коде, только из семантической схемы. - `MARKDOWN-SOURCE.md` — откуда взята отрисовка Markdown. - `BORROW-FROM-ASSISTENT.md` — что берём из `ai/assistent`, а что нет. - `MIC-ASR-SEARCH.md` — библиотеки микрофона/распознавания (`mic-kmp`, `asr-kmp`, `vad-kmp`). ## Структура кода ``` pw.binom.agentik.desktop ├── Main.kt # точка входа: окно, реестр, состояние ├── settings/ # AppPaths, AppSettings/AgentConfig, SettingsRepository ├── persistence/ # ConversationMetaRepository (группы/lastSeen/preview/watermark) ├── session/ # LiveTurn, ChatSession, AgentConnection, AgentRegistry ├── model/ # UiModels + Mappers (журнал/стрим → UI) ├── media/ # AvatarStorage (копия картинки агента) ├── net/ # ConnectionChecker (проверка связи) ├── markdown/ # вендоренный парсер Markdown (из assistent) └── ui/ # App, AppState, theme/, components/, screens/ ``` Ключевые решения по слоям — в `STORAGE.md`; кратко: - **Транспорт целиком в `:client`.** HTTP/JSON/SSE/Bearer, reconnect, кэш списка диалогов — не переписываем. - **Сообщения** — `KsqliteJournalStore` из `:journal-ksqlite` (offline-история). - **Группы, `lastSeen`, превью, watermark синхронизации** — наш `ConversationMetaRepository` (сервер про это не знает). - **Отдельная SQLite-база на агента**: id диалогов серверные, у двух агентов могут совпасть; плюс группы локальны для агента. ## Текущее состояние реализации Готово и покрыто тестами: - **Настройки** — `settings.json` (агенты, тема, выбранная группа, последний диалог), `clientId` генерируется один раз. Атомарная запись. - **Живой список диалогов** — `AgentConnection.conversationsFlow` опрашивает встроенный кэш либы (`conversationStore.listFlow`, warmup-цикл на старте) и дополнительно подписан на SSE-поток агента: диалог, созданный с другого клиента (или самим сервером), появляется в списке сам; свои создания и удаления отражаются сразу. Ошибки сервера не считаются «пустым списком» — последний известный список сохраняется. - **Реестр агентов** — добавление/правка/удаление, подключение, деградация в «не отвечает» без падения приложения. - **Стриминг** — `LiveTurn` применяет события `Working → StartReasoning → AppendText → ToolCall/ToolResult → End/Interrupted/Error`; `ChatSession` догоняет историю (`after = synced_at`), пишет в локальный кэш, склеивает live + history, обновляет превью и `lastSeen`. - **История/UI-модели** — мапперы `MessageRecord → UiMessage` со склейкой `ToolCall`+`ToolResult` в одну карточку. - **Группы** — CRUD групп + раскладка диалогов (каскад: удаление группы не удаляет диалоги). - **Тема** — семантические цвета (`AgentikTheme.colors`), тёмная + светлая схемы; отдельно пользовательские цвета агентов. - **Markdown** — вендоренный рендер из `ai/assistent` (см. `MARKDOWN-SOURCE.md`). - **Аватары** — копирование выбранного файла в `~/.agentik-desktop/avatars/`. - **Проверка связи** — «ответ сервера» + создание/удаление временного диалога, с таймингами. - **UI (Compose)** — рейка агентов, список диалогов (поиск, папки, бейджи), чат с живым стримингом, композер, окна «Агенты», «Группы», «Новый диалог» (при нескольких агентах — выбор, при одном — сразу) и «Параметры диалога» (кнопка `⋯` в шапке чата: имя, обслуживающий агент, признак «Временный», даты создания/обновления, удаление), пустые состояния, адаптивная раскладка (узкое окно — список или чат). - **Голос** — микрофон → VAD → распознавание (`voice/`): кнопка микрофона или `Ctrl+M` из чата запускает запись (`mic-api`), Silero VAD (`vad-kmp`) отмечает наличие речи, Vosk (`asr-vosk`, модель внутри артефакта) отдаёт промежуточный текст над полем ввода, финал дописывается в черновик. Тишина текстом не становится. Нативные модели считаются на одном выделенном потоке. На старте `main()` делает [`VoiceNative.warmUp()`] — обязательный прогрев Vosk/ONNX ДО инициализации skia/Compose (иначе создание модели после отрисовки окна роняет JVM в C++-логах; подробности в `MIC-ASR-SEARCH.md`). - **Реальная интеграция** — интеграционные тесты гоняются против живого `:standalone`-агента (Qwen через vLLM), а не только in-memory-фейков: `RealServerIT` (диалог → отправка → живой ответ LLM → локальный кэш) и `RealAppStateIT` (весь `AppState`: реестр → подключение → живой список → создание диалога → стриминг → превью/lastSeen), `RealSpeechIT` (речь: прогрев нативных ASR → инициализация Compose → распознавание реального wav → «раз два три четыре пять»). См. `AGENTIK_E2E_URL`. Проверено вживую (окно приложения + живой агент): список диалогов подтягивается с сервера, кнопка «+» создаёт диалог, сообщение уходит и ответ приходит в UI, `Ctrl+M` переводит микрофон в запись и обратно без падения JVM. Не начато / заглушки: - **Свёрнутый вид хода работы агента** — сейчас живой ход показывается развёрнуто (reasoning, карточки ToolCall/ToolResult, спиннер). Как его складывать — вопрос дизайна, не решён (REQUIREMENTS §9). - **Кусок прерванного ответа в кэше** — R39 выполнен (прерванный ответ в историю не пишется), а вот хранить ли его обрывок в кэше — вопрос не решён (REQUIREMENTS §9). - **Светлая тема визуально не проверялась глазами** (`THEMES.md`); в `ui/ThemeRenderTest.kt` проверено программно: фон код-блоков берётся из слота темы, порядок яркостей в светлой палитре корректный. - **Голос: движок зашит на Vosk** (`VoskVoiceRecognizer`) — Qwen3/Whisper из `asr-kmp` подключаются заменой `VoiceRecognizerFactory`; выбор движка/модели в настройках не сделан. ## Как запустить ```bash ./gradlew run # окно приложения ./gradlew jvmTest # тесты (реальная SQLite, фейковый транспорт) # Интеграционные тесты против живого агента (по умолчанию пропускаются): AGENTIK_E2E_URL=http://127.0.0.1:8080/agentik ./gradlew jvmTest \ --tests '*RealServerIT' --tests '*RealAppStateIT' ./gradlew jvmJar # собрать jar ``` ## Что решено - **Сначала десктоп, затем Android — по тем же лекалам.** Десктоп делаем образцом: понятный и читаемый по коду. Потом Android повторяет решения. - **Несколько агентов** — различение цветом или картинкой (выбирает пользователь). Картинку клиент **копирует себе**, а не берёт по адресу. - **Markdown** — показывать разметкой. Готовый рендер взят из `ai/assistent`. - **Папки над списком диалогов** — сохранены. - **Проверка связи** — отдельным окном, с таймингами шагов. - **Ориентир по размеру:** ~9 небольших кусков логики поверх библиотеки, а не собственная инфраструктура (см. `STORAGE.md`, `CACHE.md`). ## Что предстоит решить 1. **Запись** — по нажатию или на удержание. 2. Форма показа хода работы агента в свёрнутом виде. 3. Различение агентов: одного цвета может оказаться мало. 4. **Что делать с куском прерванного ответа** в кэше (`CACHE.md`, п. 3.2). 5. Где показывать несколько агентов в одном списке диалогов (сейчас список — у активного агента).